tools: static string definitions, no runtime JSON construction
Tool definitions are now &'static str (name, description, parameters_json) instead of runtime-constructed serde_json::Value. No more json!() macro, no more ToolDef::new() for tool definitions. The JSON schema strings are written directly as string literals. When sent to the API, they can be interpolated without serialization/deserialization. Multi-tool modules return fixed-size arrays instead of Vecs: - memory: [Tool; 12], journal: [Tool; 3] - channels: [Tool; 4] - control: [Tool; 3] - web: [Tool; 2] ToolDef/FunctionDef remain for backward compat (API wire format, summarize_args) but are no longer used in tool definitions. Co-Authored-By: Proof of Concept <poc@bcachefs.org> Signed-off-by: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
ed150df628
commit
53ad8cc9df
13 changed files with 205 additions and 561 deletions
|
|
@ -1,77 +1,28 @@
|
|||
// tools/edit.rs — Search-and-replace file editing
|
||||
//
|
||||
// The edit tool performs exact string replacement in files. This is the
|
||||
// same pattern used by Claude Code and aider — it's more reliable than
|
||||
// line-number-based editing because the model specifies what it sees,
|
||||
// not where it thinks it is.
|
||||
//
|
||||
// Supports replace_all for bulk renaming (e.g. variable renames).
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use super::ToolDef;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Args {
|
||||
file_path: String,
|
||||
old_string: String,
|
||||
new_string: String,
|
||||
#[serde(default)]
|
||||
replace_all: bool,
|
||||
}
|
||||
|
||||
pub fn tool() -> super::Tool {
|
||||
super::Tool { def: definition(), handler: |_a, v| Box::pin(async move { edit_file(&v) }) }
|
||||
super::Tool {
|
||||
name: "edit_file",
|
||||
description: "Perform exact string replacement in a file. The old_string must appear exactly once (unless replace_all is true). Use read_file first to see current contents.",
|
||||
parameters_json: r#"{"type":"object","properties":{"file_path":{"type":"string","description":"Absolute path to the file to edit"},"old_string":{"type":"string","description":"The exact text to find and replace"},"new_string":{"type":"string","description":"The replacement text"},"replace_all":{"type":"boolean","description":"Replace all occurrences (default false)"}},"required":["file_path","old_string","new_string"]}"#,
|
||||
handler: |_a, v| Box::pin(async move { edit_file(&v) }),
|
||||
}
|
||||
}
|
||||
|
||||
fn definition() -> ToolDef {
|
||||
ToolDef::new(
|
||||
"edit_file",
|
||||
"Perform exact string replacement in a file. The old_string must appear \
|
||||
exactly once in the file (unless replace_all is true). Use read_file first \
|
||||
to see the current contents.",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file to edit"
|
||||
},
|
||||
"old_string": {
|
||||
"type": "string",
|
||||
"description": "The exact text to find and replace"
|
||||
},
|
||||
"new_string": {
|
||||
"type": "string",
|
||||
"description": "The replacement text"
|
||||
},
|
||||
"replace_all": {
|
||||
"type": "boolean",
|
||||
"description": "Replace all occurrences (default false)"
|
||||
}
|
||||
},
|
||||
"required": ["file_path", "old_string", "new_string"]
|
||||
}),
|
||||
)
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Args { file_path: String, old_string: String, new_string: String, #[serde(default)] replace_all: bool }
|
||||
|
||||
fn edit_file(args: &serde_json::Value) -> Result<String> {
|
||||
let a: Args = serde_json::from_value(args.clone())
|
||||
.context("invalid edit_file arguments")?;
|
||||
|
||||
if a.old_string == a.new_string {
|
||||
anyhow::bail!("old_string and new_string are identical");
|
||||
}
|
||||
let a: Args = serde_json::from_value(args.clone()).context("invalid edit_file arguments")?;
|
||||
if a.old_string == a.new_string { anyhow::bail!("old_string and new_string are identical"); }
|
||||
|
||||
let content = std::fs::read_to_string(&a.file_path)
|
||||
.with_context(|| format!("Failed to read {}", a.file_path))?;
|
||||
|
||||
let count = content.matches(&*a.old_string).count();
|
||||
if count == 0 {
|
||||
anyhow::bail!("old_string not found in {}", a.file_path);
|
||||
}
|
||||
if count == 0 { anyhow::bail!("old_string not found in {}", a.file_path); }
|
||||
|
||||
if a.replace_all {
|
||||
let new_content = content.replace(&*a.old_string, &a.new_string);
|
||||
|
|
@ -80,11 +31,7 @@ fn edit_file(args: &serde_json::Value) -> Result<String> {
|
|||
Ok(format!("Replaced {} occurrences in {}", count, a.file_path))
|
||||
} else {
|
||||
if count > 1 {
|
||||
anyhow::bail!(
|
||||
"old_string appears {} times in {} — use replace_all or provide more context \
|
||||
to make it unique",
|
||||
count, a.file_path
|
||||
);
|
||||
anyhow::bail!("old_string appears {} times in {} — use replace_all or provide more context", count, a.file_path);
|
||||
}
|
||||
let new_content = content.replacen(&*a.old_string, &a.new_string, 1);
|
||||
std::fs::write(&a.file_path, &new_content)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue