tool: add execution context

This commit is contained in:
2026-06-09 19:31:11 +09:00
parent b21fab82fc
commit d8aed7befe
39 changed files with 1212 additions and 259 deletions
+9 -5
View File
@@ -54,7 +54,11 @@ struct WriteExtractedTool {
#[async_trait]
impl Tool for WriteExtractedTool {
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 payload: ExtractedPayload = serde_json::from_str(input_json).map_err(|e| {
ToolError::InvalidArgument(format!("invalid write_extracted input: {e}"))
})?;
@@ -122,7 +126,7 @@ mod tests {
"requests": []
})
.to_string();
let out = tool.execute(&input).await.unwrap();
let out = tool.execute(&input, Default::default()).await.unwrap();
assert!(out.summary.contains("decisions=1"));
let payload = ctx.take_payload().unwrap();
assert_eq!(payload.decisions.len(), 1);
@@ -137,7 +141,7 @@ mod tests {
let first =
serde_json::json!({"decisions": [], "discussions": [], "attempts": [], "requests": []})
.to_string();
tool.execute(&first).await.unwrap();
tool.execute(&first, Default::default()).await.unwrap();
let second = serde_json::json!({
"decisions": [],
@@ -146,7 +150,7 @@ mod tests {
"requests": []
})
.to_string();
tool.execute(&second).await.unwrap();
tool.execute(&second, Default::default()).await.unwrap();
let payload = ctx.take_payload().unwrap();
assert_eq!(payload.attempts.len(), 1);
@@ -157,7 +161,7 @@ mod tests {
async fn invalid_json_returns_invalid_argument() {
let ctx = Arc::new(ExtractWorkerContext::new());
let tool: Arc<dyn Tool> = Arc::new(WriteExtractedTool { ctx: ctx.clone() });
let res = tool.execute("not json").await;
let res = tool.execute("not json", Default::default()).await;
assert!(matches!(res, Err(ToolError::InvalidArgument(_))));
assert!(ctx.take_payload().is_none());
}
+9 -2
View File
@@ -29,7 +29,11 @@ struct MemoryDeleteTool {
#[async_trait]
impl Tool for MemoryDeleteTool {
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: DeleteParams = serde_json::from_str(input_json)
.map_err(|e| ToolError::InvalidArgument(format!("invalid MemoryDelete input: {e}")))?;
let path = params
@@ -139,7 +143,10 @@ mod tests {
let (_, tool) = delete_tool(layout.clone())();
let out = tool
.execute(r#"{"kind":"decision","slug":"obsolete"}"#)
.execute(
r#"{"kind":"decision","slug":"obsolete"}"#,
Default::default(),
)
.await
.unwrap();
assert!(out.summary.contains("Deleted"));
+21 -5
View File
@@ -47,7 +47,11 @@ 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 MemoryEdit input: {e}")))?;
@@ -316,7 +320,10 @@ mod tests {
"old_string": "body body",
"new_string": "edited",
});
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"));
let after = std::fs::read_to_string(&path).unwrap();
assert!(after.contains("edited"));
@@ -335,7 +342,10 @@ mod tests {
"old_string": "status: open\n",
"new_string": "",
});
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("status") || msg.contains("missing"));
@@ -354,7 +364,10 @@ mod tests {
"old_string": "x",
"new_string": "y",
});
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::ExecutionFailed(_)));
}
@@ -369,7 +382,10 @@ mod tests {
"old_string": "x",
"new_string": "y",
});
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(_)));
}
}
+64 -17
View File
@@ -126,7 +126,11 @@ struct KnowledgeQueryTool {
#[async_trait]
impl Tool for MemoryQueryTool {
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: MemoryQueryParams = serde_json::from_str(input_json)
.map_err(|e| ToolError::InvalidArgument(format!("invalid MemoryQuery input: {e}")))?;
let needle = match params.query.as_deref() {
@@ -240,7 +244,11 @@ impl Tool for MemoryQueryTool {
#[async_trait]
impl Tool for KnowledgeQueryTool {
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: KnowledgeQueryParams = serde_json::from_str(input_json).map_err(|e| {
ToolError::InvalidArgument(format!("invalid KnowledgeQuery input: {e}"))
})?;
@@ -568,7 +576,10 @@ mod tests {
write_decision(dir.path(), "beta", "no match here\n");
let (_, tool) = memory_query_tool(layout, QueryConfig::default())();
let inp = serde_json::json!({ "query": "ollama" });
let out = tool.execute(&inp.to_string()).await.unwrap();
let out = tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
let records: Vec<OwnedMemoryRecord> = parse_records(&out);
assert_eq!(records.len(), 1);
assert_eq!(records[0].slug, "alpha");
@@ -596,7 +607,7 @@ mod tests {
.unwrap();
let (_, tool) = memory_query_tool(layout, QueryConfig::default())();
let out = tool.execute("{}").await.unwrap();
let out = tool.execute("{}", Default::default()).await.unwrap();
let records: Vec<OwnedMemoryRecord> = parse_records(&out);
let mut slugs: Vec<&str> = records.iter().map(|r| r.slug.as_str()).collect();
slugs.sort();
@@ -616,7 +627,10 @@ mod tests {
.unwrap();
let (_, tool) = memory_query_tool(layout, QueryConfig::default())();
let inp = serde_json::json!({ "query": "needle" });
let out = tool.execute(&inp.to_string()).await.unwrap();
let out = tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
let records: Vec<OwnedMemoryRecord> = parse_records(&out);
assert_eq!(records.len(), 1);
assert_eq!(records[0].slug, "summary");
@@ -633,7 +647,10 @@ mod tests {
let (_, tool) = memory_query_tool(layout, QueryConfig::default())();
let inp = serde_json::json!({ "query": "needle" });
let out = tool.execute(&inp.to_string()).await.unwrap();
let out = tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
let records: Vec<OwnedMemoryRecord> = parse_records(&out);
assert!(records.is_empty(), "got records: {:?}", out.content);
}
@@ -653,8 +670,14 @@ mod tests {
let (_, memory_tool) = memory_query_tool(layout.clone(), QueryConfig::default())();
let (_, knowledge_tool) = knowledge_query_tool(layout.clone(), QueryConfig::default())();
let inp = serde_json::json!({ "query": "needle" });
memory_tool.execute(&inp.to_string()).await.unwrap();
knowledge_tool.execute(&inp.to_string()).await.unwrap();
memory_tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
knowledge_tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
let report = crate::usage::build_usage_report(&layout).unwrap();
assert!(report.records.is_empty());
@@ -673,7 +696,10 @@ mod tests {
};
let (_, tool) = memory_query_tool(layout, cfg)();
let inp = serde_json::json!({ "query": "needle" });
let out = tool.execute(&inp.to_string()).await.unwrap();
let out = tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
let records: Vec<OwnedMemoryRecord> = parse_records(&out);
assert_eq!(records.len(), 3);
}
@@ -692,7 +718,10 @@ mod tests {
};
let (_, tool) = memory_query_tool(layout, cfg)();
let inp = serde_json::json!({ "query": "needle" });
let out = tool.execute(&inp.to_string()).await.unwrap();
let out = tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
let records: Vec<OwnedMemoryRecord> = parse_records(&out);
assert_eq!(records.len(), 1);
let e = records[0].excerpt.as_deref().unwrap();
@@ -708,7 +737,10 @@ mod tests {
let (_dir, layout) = setup();
let (_, tool) = memory_query_tool(layout, QueryConfig::default())();
let inp = serde_json::json!({ "query": " " });
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(_)));
}
@@ -724,7 +756,10 @@ mod tests {
);
let (_, tool) = knowledge_query_tool(layout, QueryConfig::default())();
let inp = serde_json::json!({ "query": "ollama" });
let out = tool.execute(&inp.to_string()).await.unwrap();
let out = tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
let records: Vec<OwnedKnowledgeRecord> = parse_records(&out);
assert_eq!(records.len(), 1);
assert_eq!(records[0].slug, "policy");
@@ -748,7 +783,7 @@ mod tests {
write_knowledge(dir.path(), "h1", "howto", "d2", "body\n");
let (_, tool) = knowledge_query_tool(layout, QueryConfig::default())();
let out = tool.execute("{}").await.unwrap();
let out = tool.execute("{}", Default::default()).await.unwrap();
let records: Vec<OwnedKnowledgeRecord> = parse_records(&out);
let mut slugs: Vec<&str> = records.iter().map(|r| r.slug.as_str()).collect();
slugs.sort();
@@ -764,7 +799,10 @@ mod tests {
let (_, tool) = knowledge_query_tool(layout, QueryConfig::default())();
let inp = serde_json::json!({ "query": "needle", "kind": "howto" });
let out = tool.execute(&inp.to_string()).await.unwrap();
let out = tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
let records: Vec<OwnedKnowledgeRecord> = parse_records(&out);
assert_eq!(records.len(), 1);
assert_eq!(records[0].slug, "h1");
@@ -778,7 +816,10 @@ mod tests {
let (_, tool) = knowledge_query_tool(layout, QueryConfig::default())();
let inp = serde_json::json!({ "kind": "howto" });
let out = tool.execute(&inp.to_string()).await.unwrap();
let out = tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
let records: Vec<OwnedKnowledgeRecord> = parse_records(&out);
assert_eq!(records.len(), 1);
assert_eq!(records[0].slug, "h1");
@@ -792,7 +833,10 @@ mod tests {
let (_, tool) = knowledge_query_tool(layout, QueryConfig::default())();
let inp = serde_json::json!({ "query": "xyzzy" });
let out = tool.execute(&inp.to_string()).await.unwrap();
let out = tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
let records: Vec<OwnedKnowledgeRecord> = parse_records(&out);
assert_eq!(records.len(), 1);
assert_eq!(records[0].slug, "p");
@@ -804,7 +848,10 @@ mod tests {
write_knowledge(dir.path(), "p", "policy", "d", "no match\n");
let (_, tool) = knowledge_query_tool(layout, QueryConfig::default())();
let inp = serde_json::json!({ "query": "absent" });
let out = tool.execute(&inp.to_string()).await.unwrap();
let out = tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
let records: Vec<OwnedKnowledgeRecord> = parse_records(&out);
assert!(records.is_empty());
}
+36 -9
View File
@@ -45,7 +45,11 @@ 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 MemoryRead input: {e}")))?;
@@ -225,7 +229,10 @@ mod tests {
let (_meta, tool) = read_tool(layout)();
let inp = serde_json::json!({ "kind": "decision", "slug": "foo" });
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(" 1\talpha"));
assert!(body.contains(" 2\tbeta"));
@@ -240,7 +247,10 @@ mod tests {
let (_, tool) = read_tool(layout)();
let inp = serde_json::json!({ "kind": "summary" });
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("summary body"));
}
@@ -249,7 +259,10 @@ mod tests {
let (_dir, layout) = setup();
let (_, tool) = read_tool(layout)();
let inp = serde_json::json!({ "kind": "summary", "slug": "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(_)));
}
@@ -258,7 +271,10 @@ mod tests {
let (_dir, layout) = setup();
let (_, tool) = read_tool(layout)();
let inp = serde_json::json!({ "kind": "decision" });
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(_)));
}
@@ -267,7 +283,10 @@ mod tests {
let (_dir, layout) = setup();
let (_, tool) = read_tool(layout)();
let inp = serde_json::json!({ "kind": "decision", "slug": "Bad-Slug" });
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(_)));
}
@@ -280,7 +299,10 @@ mod tests {
let (_, tool) = read_tool(layout)();
let inp = serde_json::json!({ "kind": "knowledge", "slug": "policy" });
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("k"));
}
@@ -293,7 +315,9 @@ mod tests {
let (_, tool) = read_tool_with_usage(layout.clone(), "session-1")();
let inp = serde_json::json!({ "kind": "decision", "slug": "foo" });
tool.execute(&inp.to_string()).await.unwrap();
tool.execute(&inp.to_string(), Default::default())
.await
.unwrap();
let report = usage::build_usage_report(&layout).unwrap();
assert_eq!(report.records.len(), 1);
@@ -310,7 +334,10 @@ mod tests {
let (_dir, layout) = setup();
let (_, tool) = read_tool(layout)();
let inp = serde_json::json!({ "kind": "decision", "slug": "missing" });
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::ExecutionFailed(_)));
}
}
+30 -7
View File
@@ -42,7 +42,11 @@ 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 MemoryWrite input: {e}")))?;
@@ -229,7 +233,10 @@ mod tests {
"kind": "summary",
"content": content,
});
let out = tool.execute(&inp.to_string()).await.unwrap();
let out = tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
assert!(out.summary.contains("Created"));
assert!(path.exists());
}
@@ -249,7 +256,10 @@ mod tests {
"slug": "foo",
"content": content,
});
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("status") || msg.contains("missing"), "{msg}");
}
@@ -271,7 +281,10 @@ mod tests {
"slug": "foo",
"content": initial,
});
let out = tool.execute(&inp.to_string()).await.unwrap();
let out = tool
.execute(&inp.to_string(), Default::default())
.await
.unwrap();
assert!(out.summary.contains("Overwrote"));
}
@@ -283,7 +296,10 @@ mod tests {
"kind": "decision",
"content": "ignored",
});
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(_)));
}
@@ -298,7 +314,11 @@ mod tests {
"slug": "foo",
"content": bad,
});
assert!(tool.execute(&inp.to_string()).await.is_err());
assert!(
tool.execute(&inp.to_string(), Default::default())
.await
.is_err()
);
assert!(!path.exists());
}
@@ -312,7 +332,10 @@ mod tests {
"slug": "wf",
"content": "---\n---\n",
});
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(_)));
}
}