tool: add execution context
This commit is contained in:
+45
-11
@@ -101,7 +101,11 @@ impl Drop for BashTool {
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for BashTool {
|
||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
||||
async fn execute(
|
||||
&self,
|
||||
input_json: &str,
|
||||
_ctx: llm_worker::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let params: BashParams = serde_json::from_str(input_json)
|
||||
.map_err(|e| ToolError::InvalidArgument(format!("invalid Bash input: {e}")))?;
|
||||
let timeout_secs = params
|
||||
@@ -394,7 +398,10 @@ mod tests {
|
||||
assert_eq!(meta.name, "Bash");
|
||||
|
||||
let inp = serde_json::json!({ "command": "echo hello" });
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(out.summary, "$ echo hello");
|
||||
assert_eq!(out.content.as_deref().map(str::trim), Some("hello"));
|
||||
}
|
||||
@@ -407,7 +414,10 @@ mod tests {
|
||||
let inp = serde_json::json!({
|
||||
"command": "echo out; echo err 1>&2",
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
assert!(body.contains("out"));
|
||||
assert!(body.contains("err"));
|
||||
@@ -419,7 +429,10 @@ mod tests {
|
||||
let tool = make_tool(&h);
|
||||
|
||||
let inp = serde_json::json!({ "command": "exit 7" });
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(out.summary.contains("exit 7"), "summary: {}", out.summary);
|
||||
assert!(
|
||||
out.content.is_none(),
|
||||
@@ -441,12 +454,16 @@ mod tests {
|
||||
"command": format!("cd {}", sub.to_str().unwrap()),
|
||||
})
|
||||
.to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let pwd_out = tool
|
||||
.execute(&serde_json::json!({ "command": "pwd" }).to_string())
|
||||
.execute(
|
||||
&serde_json::json!({ "command": "pwd" }).to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let body = pwd_out.content.unwrap();
|
||||
@@ -467,7 +484,10 @@ mod tests {
|
||||
"command": "sleep 30",
|
||||
"timeout": 1,
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
out.summary.contains("timed out"),
|
||||
"summary: {}",
|
||||
@@ -480,7 +500,10 @@ mod tests {
|
||||
let h = setup();
|
||||
let tool = make_tool(&h);
|
||||
|
||||
let err = tool.execute("not json").await.unwrap_err();
|
||||
let err = tool
|
||||
.execute("not json", Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ToolError::InvalidArgument(_)));
|
||||
}
|
||||
|
||||
@@ -494,7 +517,10 @@ mod tests {
|
||||
let inp = serde_json::json!({
|
||||
"command": "for i in $(seq 1 200); do echo line $i; done",
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.expect("expected content");
|
||||
|
||||
assert!(
|
||||
@@ -523,7 +549,10 @@ mod tests {
|
||||
let inp = serde_json::json!({
|
||||
"command": "printf 'x%.0s' {1..20480}",
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
assert!(
|
||||
body.contains(spill_dir.to_str().unwrap()),
|
||||
@@ -542,7 +571,10 @@ mod tests {
|
||||
"command": "(sleep 0.05; echo bg) &",
|
||||
"timeout": 5,
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
!out.summary.contains("timed out"),
|
||||
"summary: {}",
|
||||
@@ -559,7 +591,9 @@ mod tests {
|
||||
let inp = serde_json::json!({
|
||||
"command": "for i in $(seq 1 200); do echo $i; done",
|
||||
});
|
||||
tool.execute(&inp.to_string()).await.unwrap();
|
||||
tool.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The spill dir should now contain exactly one bash-*.log file.
|
||||
let files_before: Vec<_> = std::fs::read_dir(&spill_dir)
|
||||
|
||||
@@ -36,7 +36,11 @@ pub(crate) struct EditTool {
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for EditTool {
|
||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
||||
async fn execute(
|
||||
&self,
|
||||
input_json: &str,
|
||||
_ctx: llm_worker::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let params: EditParams = serde_json::from_str(input_json)
|
||||
.map_err(|e| ToolError::InvalidArgument(format!("invalid Edit input: {e}")))?;
|
||||
|
||||
@@ -169,7 +173,10 @@ mod tests {
|
||||
let def = read_tool(fs.clone(), tracker.clone());
|
||||
let (_, reader) = def();
|
||||
let inp = serde_json::json!({ "file_path": file.to_str().unwrap() });
|
||||
reader.execute(&inp.to_string()).await.unwrap();
|
||||
reader
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -188,7 +195,10 @@ mod tests {
|
||||
"old_string": "foo bar",
|
||||
"new_string": "foo baz",
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(out.summary.contains("1 replacement"));
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&file).unwrap(),
|
||||
@@ -212,7 +222,10 @@ mod tests {
|
||||
"new_string": "y",
|
||||
"replace_all": true,
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(out.summary.contains("3 replacements"));
|
||||
assert_eq!(std::fs::read_to_string(&file).unwrap(), "y y y\n");
|
||||
}
|
||||
@@ -231,7 +244,10 @@ mod tests {
|
||||
"old_string": "a",
|
||||
"new_string": "b",
|
||||
});
|
||||
let err = tool.execute(&inp.to_string()).await.unwrap_err();
|
||||
let err = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ToolError::InvalidArgument(_)));
|
||||
}
|
||||
|
||||
@@ -249,7 +265,10 @@ mod tests {
|
||||
"old_string": "world",
|
||||
"new_string": "x",
|
||||
});
|
||||
let err = tool.execute(&inp.to_string()).await.unwrap_err();
|
||||
let err = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ToolError::InvalidArgument(_)));
|
||||
}
|
||||
|
||||
@@ -266,7 +285,10 @@ mod tests {
|
||||
"old_string": "foo",
|
||||
"new_string": "bar",
|
||||
});
|
||||
let err = tool.execute(&inp.to_string()).await.unwrap_err();
|
||||
let err = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ToolError::InvalidArgument(_)));
|
||||
}
|
||||
|
||||
@@ -287,7 +309,10 @@ mod tests {
|
||||
"old_string": "foo",
|
||||
"new_string": "bar",
|
||||
});
|
||||
let err = tool.execute(&inp.to_string()).await.unwrap_err();
|
||||
let err = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
let msg = format!("{err}");
|
||||
assert!(msg.contains("modified externally"), "{msg}");
|
||||
}
|
||||
|
||||
@@ -35,7 +35,11 @@ pub(crate) struct GlobTool {
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for GlobTool {
|
||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
||||
async fn execute(
|
||||
&self,
|
||||
input_json: &str,
|
||||
_ctx: llm_worker::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let params: GlobParams = serde_json::from_str(input_json)
|
||||
.map_err(|e| ToolError::InvalidArgument(format!("invalid Glob input: {e}")))?;
|
||||
|
||||
@@ -239,7 +243,10 @@ mod tests {
|
||||
assert_eq!(meta.name, "Glob");
|
||||
|
||||
let inp = serde_json::json!({ "pattern": "**/*.rs" });
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(out.summary.contains("2 file(s)"));
|
||||
let body = out.content.unwrap();
|
||||
assert!(body.contains("a.rs"));
|
||||
@@ -261,7 +268,10 @@ mod tests {
|
||||
let def = glob_tool(fs);
|
||||
let (_, tool) = def();
|
||||
let inp = serde_json::json!({ "pattern": "*.rs" });
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
let new_pos = body.find("new.rs").unwrap();
|
||||
let old_pos = body.find("old.rs").unwrap();
|
||||
@@ -274,7 +284,10 @@ mod tests {
|
||||
let def = glob_tool(fs);
|
||||
let (_, tool) = def();
|
||||
let inp = serde_json::json!({ "pattern": "**/*.nonexistent" });
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(out.summary.contains("No files"));
|
||||
assert!(out.content.is_none());
|
||||
}
|
||||
@@ -285,7 +298,10 @@ mod tests {
|
||||
let def = glob_tool(fs);
|
||||
let (_, tool) = def();
|
||||
let inp = serde_json::json!({ "pattern": "[unterminated" });
|
||||
let err = tool.execute(&inp.to_string()).await.unwrap_err();
|
||||
let err = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ToolError::InvalidArgument(_)));
|
||||
}
|
||||
|
||||
@@ -317,7 +333,10 @@ mod tests {
|
||||
let def = glob_tool(fs);
|
||||
let (_, tool) = def();
|
||||
let inp = serde_json::json!({ "pattern": "**/*.rs" });
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap_or_default();
|
||||
assert!(body.contains("visible.rs"));
|
||||
assert!(
|
||||
@@ -335,7 +354,10 @@ mod tests {
|
||||
let def = glob_tool(fs);
|
||||
let (_, tool) = def();
|
||||
let inp = serde_json::json!({ "pattern": "*.rs" });
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
assert!(body.contains(".hidden.rs"));
|
||||
assert!(body.contains("visible.rs"));
|
||||
@@ -358,7 +380,10 @@ mod tests {
|
||||
"path": link.to_str().unwrap(),
|
||||
"pattern": "**/*.rs",
|
||||
});
|
||||
let err = tool.execute(&inp.to_string()).await.unwrap_err();
|
||||
let err = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
let msg = format!("{err}");
|
||||
assert!(
|
||||
msg.contains("Glob does not follow symlink directories"),
|
||||
|
||||
+65
-16
@@ -82,7 +82,11 @@ pub(crate) struct GrepTool {
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for GrepTool {
|
||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
||||
async fn execute(
|
||||
&self,
|
||||
input_json: &str,
|
||||
_ctx: llm_worker::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let params: GrepParams = serde_json::from_str(input_json)
|
||||
.map_err(|e| ToolError::InvalidArgument(format!("invalid Grep input: {e}")))?;
|
||||
|
||||
@@ -563,7 +567,10 @@ mod tests {
|
||||
let def = grep_tool(scoped);
|
||||
let (_, tool) = def();
|
||||
let inp = serde_json::json!({ "pattern": "needle" });
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap_or_default();
|
||||
assert!(body.contains("visible.txt"));
|
||||
assert!(
|
||||
@@ -583,7 +590,10 @@ mod tests {
|
||||
assert_eq!(meta.name, "Grep");
|
||||
|
||||
let inp = serde_json::json!({ "pattern": "bravo" });
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(out.summary.contains("1 file"));
|
||||
assert!(out.content.unwrap().contains("a.txt"));
|
||||
}
|
||||
@@ -599,7 +609,10 @@ mod tests {
|
||||
"pattern": "two",
|
||||
"output_mode": "content",
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
assert!(body.contains(":2:two"));
|
||||
}
|
||||
@@ -616,7 +629,10 @@ mod tests {
|
||||
"pattern": "x",
|
||||
"output_mode": "count",
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
assert!(body.contains("a.txt:3"));
|
||||
assert!(body.contains("b.txt:1"));
|
||||
@@ -635,7 +651,10 @@ mod tests {
|
||||
"-i": true,
|
||||
"output_mode": "content",
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(out.content.unwrap().contains("HELLO"));
|
||||
}
|
||||
|
||||
@@ -654,7 +673,10 @@ mod tests {
|
||||
"output_mode": "content",
|
||||
"-C": 1,
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
// should contain: line2 (before context), MATCH, line4 (after context)
|
||||
assert!(body.contains("line2"));
|
||||
@@ -677,7 +699,10 @@ mod tests {
|
||||
"multiline": true,
|
||||
"output_mode": "content",
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
assert!(body.contains("foo"));
|
||||
}
|
||||
@@ -694,7 +719,10 @@ mod tests {
|
||||
"pattern": "target",
|
||||
"glob": "*.rs",
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
assert!(body.contains("a.rs"));
|
||||
assert!(!body.contains("b.txt"));
|
||||
@@ -712,7 +740,10 @@ mod tests {
|
||||
"pattern": "target",
|
||||
"type": "rust",
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
assert!(body.contains("a.rs"));
|
||||
assert!(!body.contains("b.py"));
|
||||
@@ -731,7 +762,10 @@ mod tests {
|
||||
"pattern": "x",
|
||||
"head_limit": 2,
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
assert_eq!(body.lines().count(), 2);
|
||||
assert!(out.summary.contains("truncated at 2"));
|
||||
@@ -752,7 +786,10 @@ mod tests {
|
||||
"offset": 3,
|
||||
"head_limit": 10,
|
||||
});
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
// We skipped 3, so only 2 should remain.
|
||||
assert_eq!(body.lines().count(), 2);
|
||||
@@ -769,7 +806,10 @@ mod tests {
|
||||
let def = grep_tool(fs);
|
||||
let (_, tool) = def();
|
||||
let inp = serde_json::json!({ "pattern": "needle" });
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
assert!(body.contains("b.txt"));
|
||||
assert!(!body.contains("a.bin"));
|
||||
@@ -781,7 +821,10 @@ mod tests {
|
||||
let def = grep_tool(fs);
|
||||
let (_, tool) = def();
|
||||
let inp = serde_json::json!({ "pattern": "(" });
|
||||
let err = tool.execute(&inp.to_string()).await.unwrap_err();
|
||||
let err = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ToolError::InvalidArgument(_)));
|
||||
}
|
||||
|
||||
@@ -794,7 +837,10 @@ mod tests {
|
||||
"pattern": "x",
|
||||
"type": "nonexistent",
|
||||
});
|
||||
let err = tool.execute(&inp.to_string()).await.unwrap_err();
|
||||
let err = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ToolError::InvalidArgument(_)));
|
||||
}
|
||||
|
||||
@@ -805,7 +851,10 @@ mod tests {
|
||||
let def = grep_tool(fs);
|
||||
let (_, tool) = def();
|
||||
let inp = serde_json::json!({ "pattern": "zzz" });
|
||||
let out = tool.execute(&inp.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&inp.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(out.summary, "No files matched");
|
||||
assert!(out.content.is_none());
|
||||
}
|
||||
|
||||
@@ -36,7 +36,11 @@ pub(crate) struct ReadTool {
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for ReadTool {
|
||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
||||
async fn execute(
|
||||
&self,
|
||||
input_json: &str,
|
||||
_ctx: llm_worker::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let params: ReadParams = serde_json::from_str(input_json)
|
||||
.map_err(|e| ToolError::InvalidArgument(format!("invalid Read input: {e}")))?;
|
||||
let offset = params.offset.unwrap_or(0);
|
||||
@@ -155,7 +159,10 @@ mod tests {
|
||||
assert_eq!(meta.name, "Read");
|
||||
|
||||
let input = serde_json::json!({ "file_path": file.to_str().unwrap() });
|
||||
let out = tool.execute(&input.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&input.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(out.summary.contains("Read 3 line(s)"));
|
||||
let body = out.content.unwrap();
|
||||
assert!(body.contains(" 1\talpha"));
|
||||
@@ -178,7 +185,10 @@ mod tests {
|
||||
"offset": 1,
|
||||
"limit": 2,
|
||||
});
|
||||
let out = tool.execute(&input.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&input.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(out.summary.contains("[2..3] of 5"));
|
||||
let body = out.content.unwrap();
|
||||
assert!(body.contains(" 2\t2"));
|
||||
@@ -193,7 +203,10 @@ mod tests {
|
||||
let input = serde_json::json!({
|
||||
"file_path": dir.path().join("nope.txt").to_str().unwrap()
|
||||
});
|
||||
let err = tool.execute(&input.to_string()).await.unwrap_err();
|
||||
let err = tool
|
||||
.execute(&input.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ToolError::ExecutionFailed(_)));
|
||||
}
|
||||
|
||||
@@ -202,7 +215,10 @@ mod tests {
|
||||
let (_dir, fs, tracker) = setup();
|
||||
let def = read_tool(fs, tracker);
|
||||
let (_, tool) = def();
|
||||
let err = tool.execute("not json").await.unwrap_err();
|
||||
let err = tool
|
||||
.execute("not json", Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ToolError::InvalidArgument(_)));
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -146,7 +146,11 @@ struct WebFetchTool {
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for WebSearchTool {
|
||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
||||
async fn execute(
|
||||
&self,
|
||||
input_json: &str,
|
||||
_ctx: llm_worker::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let input: WebSearchInput = serde_json::from_str(input_json)
|
||||
.map_err(|e| ToolError::InvalidArgument(format!("invalid WebSearch input: {e}")))?;
|
||||
self.web.run_search(input).await
|
||||
@@ -193,7 +197,11 @@ impl WebTools {
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for WebFetchTool {
|
||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
||||
async fn execute(
|
||||
&self,
|
||||
input_json: &str,
|
||||
_ctx: llm_worker::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let input: WebFetchInput = serde_json::from_str(input_json)
|
||||
.map_err(|e| ToolError::InvalidArgument(format!("invalid WebFetch input: {e}")))?;
|
||||
self.web.run_fetch(input).await
|
||||
|
||||
@@ -30,7 +30,11 @@ pub(crate) struct WriteTool {
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for WriteTool {
|
||||
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
|
||||
async fn execute(
|
||||
&self,
|
||||
input_json: &str,
|
||||
_ctx: llm_worker::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let params: WriteParams = serde_json::from_str(input_json)
|
||||
.map_err(|e| ToolError::InvalidArgument(format!("invalid Write input: {e}")))?;
|
||||
|
||||
@@ -118,7 +122,10 @@ mod tests {
|
||||
"file_path": file.to_str().unwrap(),
|
||||
"content": "hello\n",
|
||||
});
|
||||
let out = tool.execute(&input.to_string()).await.unwrap();
|
||||
let out = tool
|
||||
.execute(&input.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(out.summary.contains("Created"));
|
||||
assert_eq!(std::fs::read_to_string(&file).unwrap(), "hello\n");
|
||||
}
|
||||
@@ -135,7 +142,10 @@ mod tests {
|
||||
"file_path": file.to_str().unwrap(),
|
||||
"content": "new",
|
||||
});
|
||||
let err = tool.execute(&input.to_string()).await.unwrap_err();
|
||||
let err = tool
|
||||
.execute(&input.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ToolError::InvalidArgument(_)));
|
||||
}
|
||||
|
||||
@@ -148,7 +158,10 @@ mod tests {
|
||||
let read_def = read_tool(fs.clone(), tracker.clone());
|
||||
let (_, reader) = read_def();
|
||||
let read_in = serde_json::json!({ "file_path": file.to_str().unwrap() });
|
||||
reader.execute(&read_in.to_string()).await.unwrap();
|
||||
reader
|
||||
.execute(&read_in.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let write_def = write_tool(fs, tracker);
|
||||
let (_, writer) = write_def();
|
||||
@@ -156,7 +169,10 @@ mod tests {
|
||||
"file_path": file.to_str().unwrap(),
|
||||
"content": "new\n",
|
||||
});
|
||||
let out = writer.execute(&write_in.to_string()).await.unwrap();
|
||||
let out = writer
|
||||
.execute(&write_in.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(out.summary.contains("Overwrote"));
|
||||
assert_eq!(std::fs::read_to_string(&file).unwrap(), "new\n");
|
||||
}
|
||||
@@ -171,7 +187,10 @@ mod tests {
|
||||
let read_def = read_tool(fs.clone(), tracker.clone());
|
||||
let (_, reader) = read_def();
|
||||
reader
|
||||
.execute(&serde_json::json!({ "file_path": file.to_str().unwrap() }).to_string())
|
||||
.execute(
|
||||
&serde_json::json!({ "file_path": file.to_str().unwrap() }).to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -187,6 +206,7 @@ mod tests {
|
||||
"content": "new",
|
||||
})
|
||||
.to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
@@ -205,7 +225,10 @@ mod tests {
|
||||
"file_path": outside.path().join("x.txt").to_str().unwrap(),
|
||||
"content": "x",
|
||||
});
|
||||
let err = tool.execute(&input.to_string()).await.unwrap_err();
|
||||
let err = tool
|
||||
.execute(&input.to_string(), Default::default())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ToolError::InvalidArgument(_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,13 +66,17 @@ async fn unicode_path_and_content() {
|
||||
"content": content,
|
||||
})
|
||||
.to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let read = reg.get("Read");
|
||||
let out = read
|
||||
.execute(&json!({ "file_path": file.to_str().unwrap() }).to_string())
|
||||
.execute(
|
||||
&json!({ "file_path": file.to_str().unwrap() }).to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let body = out.content.unwrap();
|
||||
@@ -98,7 +102,10 @@ async fn symlink_to_outside_scope_is_rejected_for_write() {
|
||||
// target sits outside the scope.
|
||||
let read = reg.get("Read");
|
||||
let read_err = read
|
||||
.execute(&json!({ "file_path": link.to_str().unwrap() }).to_string())
|
||||
.execute(
|
||||
&json!({ "file_path": link.to_str().unwrap() }).to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
@@ -119,6 +126,7 @@ async fn symlink_to_outside_scope_is_rejected_for_write() {
|
||||
"content": "overwritten",
|
||||
})
|
||||
.to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
@@ -147,7 +155,10 @@ async fn broken_symlink_reports_target_and_repair_hint() {
|
||||
|
||||
let read = reg.get("Read");
|
||||
let err = read
|
||||
.execute(&json!({ "file_path": link.to_str().unwrap() }).to_string())
|
||||
.execute(
|
||||
&json!({ "file_path": link.to_str().unwrap() }).to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let msg = format!("{err}");
|
||||
@@ -165,7 +176,10 @@ async fn empty_file_read_and_edit() {
|
||||
|
||||
let read = reg.get("Read");
|
||||
let out = read
|
||||
.execute(&json!({ "file_path": file.to_str().unwrap() }).to_string())
|
||||
.execute(
|
||||
&json!({ "file_path": file.to_str().unwrap() }).to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(out.summary.contains("0 line"));
|
||||
@@ -180,6 +194,7 @@ async fn empty_file_read_and_edit() {
|
||||
"new_string": "bar",
|
||||
})
|
||||
.to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
@@ -196,7 +211,10 @@ async fn very_long_single_line() {
|
||||
|
||||
let read = reg.get("Read");
|
||||
let out = read
|
||||
.execute(&json!({ "file_path": file.to_str().unwrap() }).to_string())
|
||||
.execute(
|
||||
&json!({ "file_path": file.to_str().unwrap() }).to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// Should return exactly 1 line
|
||||
@@ -208,7 +226,10 @@ async fn relative_path_is_rejected() {
|
||||
let (_dir, _spill, reg) = setup();
|
||||
let read = reg.get("Read");
|
||||
let err = read
|
||||
.execute(&json!({ "file_path": "relative.txt" }).to_string())
|
||||
.execute(
|
||||
&json!({ "file_path": "relative.txt" }).to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(format!("{err}").contains("absolute"));
|
||||
@@ -219,7 +240,10 @@ async fn directory_target_is_rejected_for_read() {
|
||||
let (dir, _spill, reg) = setup();
|
||||
let read = reg.get("Read");
|
||||
let err = read
|
||||
.execute(&json!({ "file_path": dir.path().to_str().unwrap() }).to_string())
|
||||
.execute(
|
||||
&json!({ "file_path": dir.path().to_str().unwrap() }).to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(format!("{err}").contains("directory"));
|
||||
@@ -237,6 +261,7 @@ async fn deeply_nested_new_file_is_created() {
|
||||
"content": "deep\n",
|
||||
})
|
||||
.to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -250,9 +275,12 @@ async fn replace_preserves_unicode() {
|
||||
std::fs::write(&file, "🦀 rust 🦀\n").unwrap();
|
||||
|
||||
let read = reg.get("Read");
|
||||
read.execute(&json!({ "file_path": file.to_str().unwrap() }).to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
read.execute(
|
||||
&json!({ "file_path": file.to_str().unwrap() }).to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let edit = reg.get("Edit");
|
||||
edit.execute(
|
||||
@@ -262,6 +290,7 @@ async fn replace_preserves_unicode() {
|
||||
"new_string": "ラスト",
|
||||
})
|
||||
.to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -282,6 +311,7 @@ async fn grep_handles_unicode_pattern() {
|
||||
"output_mode": "content",
|
||||
})
|
||||
.to_string(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -66,13 +66,13 @@ fn setup() -> (TempDir, TempDir, Registry) {
|
||||
}
|
||||
|
||||
async fn call(tool: &Arc<dyn Tool>, input: serde_json::Value) -> llm_worker::tool::ToolOutput {
|
||||
tool.execute(&input.to_string())
|
||||
tool.execute(&input.to_string(), Default::default())
|
||||
.await
|
||||
.expect("tool execution failed")
|
||||
}
|
||||
|
||||
async fn call_err(tool: &Arc<dyn Tool>, input: serde_json::Value) -> llm_worker::tool::ToolError {
|
||||
tool.execute(&input.to_string())
|
||||
tool.execute(&input.to_string(), Default::default())
|
||||
.await
|
||||
.expect_err("expected error")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user