tools: delete ToolOutput, dispatch returns String

ToolOutput was just { text: String } — replaced with plain String.
dispatch() and dispatch_shared() return String directly.
ActiveToolCall handle is (ToolCall, String).
Error results are prefixed with "Error: " by convention.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-04 16:08:59 -04:00 committed by Kent Overstreet
parent a24a6605b8
commit 37fad63ba9
3 changed files with 23 additions and 35 deletions

View file

@ -602,7 +602,7 @@ impl Agent {
if call.function.name == "working_stack" { if call.function.name == "working_stack" {
let mut me = agent.lock().await; let mut me = agent.lock().await;
let result = tools::working_stack::handle(&args, &mut me.context.working_stack); let result = tools::working_stack::handle(&args, &mut me.context.working_stack);
let output = tools::ToolOutput::text(result.clone()); let output = result.clone();
me.apply_tool_result(call, output, ui_tx, ds); me.apply_tool_result(call, output, ui_tx, ds);
if !result.starts_with("Error:") { if !result.starts_with("Error:") {
me.refresh_context_state(); me.refresh_context_state();
@ -643,7 +643,7 @@ impl Agent {
fn apply_tool_result( fn apply_tool_result(
&mut self, &mut self,
call: &ToolCall, call: &ToolCall,
output: tools::ToolOutput, output: String,
ui_tx: &UiSender, ui_tx: &UiSender,
ds: &mut DispatchState, ds: &mut DispatchState,
) { ) {
@ -651,20 +651,20 @@ impl Agent {
serde_json::from_str(&call.function.arguments).unwrap_or_default(); serde_json::from_str(&call.function.arguments).unwrap_or_default();
ds.had_tool_calls = true; ds.had_tool_calls = true;
if output.text.starts_with("Error:") { if output.starts_with("Error:") {
ds.tool_errors += 1; ds.tool_errors += 1;
} }
let _ = ui_tx.send(UiMessage::ToolResult { let _ = ui_tx.send(UiMessage::ToolResult {
name: call.function.name.clone(), name: call.function.name.clone(),
result: output.text.clone(), result: output.clone(),
}); });
self.active_tools.lock().unwrap().retain(|t| t.id != call.id); self.active_tools.lock().unwrap().retain(|t| t.id != call.id);
// Tag memory_render results for context deduplication // Tag memory_render results for context deduplication
if call.function.name == "memory_render" && !output.text.starts_with("Error:") { if call.function.name == "memory_render" && !output.starts_with("Error:") {
if let Some(key) = args.get("key").and_then(|v| v.as_str()) { if let Some(key) = args.get("key").and_then(|v| v.as_str()) {
let mut msg = Message::tool_result(&call.id, &output.text); let mut msg = Message::tool_result(&call.id, &output);
msg.stamp(); msg.stamp();
self.push_entry(ConversationEntry::Memory { key: key.to_string(), message: msg }); self.push_entry(ConversationEntry::Memory { key: key.to_string(), message: msg });
self.publish_context_state(); self.publish_context_state();
@ -672,7 +672,7 @@ impl Agent {
} }
} }
self.push_message(Message::tool_result(&call.id, &output.text)); self.push_message(Message::tool_result(&call.id, &output));
} }
/// Build context state summary for the debug screen. /// Build context state summary for the debug screen.

View file

@ -134,20 +134,6 @@ pub struct ToolCallDelta {
pub function: Option<FunctionCallDelta>, pub function: Option<FunctionCallDelta>,
} }
/// Result of dispatching a tool call.
pub struct ToolOutput {
pub text: String,
}
impl ToolOutput {
pub fn error(e: impl std::fmt::Display) -> Self {
Self { text: format!("Error: {}", e) }
}
pub fn text(s: String) -> Self {
Self { text: s }
}
}
/// A tool call in flight — metadata for TUI + JoinHandle for /// A tool call in flight — metadata for TUI + JoinHandle for
/// result collection and cancellation. /// result collection and cancellation.
@ -157,7 +143,7 @@ pub struct ActiveToolCall {
pub detail: String, pub detail: String,
pub started: Instant, pub started: Instant,
pub background: bool, pub background: bool,
pub handle: tokio::task::JoinHandle<(ToolCall, ToolOutput)>, pub handle: tokio::task::JoinHandle<(ToolCall, String)>,
} }
/// Truncate output if it exceeds max length, appending a truncation notice. /// Truncate output if it exceeds max length, appending a truncation notice.
@ -170,10 +156,12 @@ pub fn truncate_output(mut s: String, max: usize) -> String {
} }
/// Dispatch a tool call by name through the registry. /// Dispatch a tool call by name through the registry.
/// Dispatch a tool call by name. Returns the result text,
/// or an error string prefixed with "Error: ".
pub async fn dispatch( pub async fn dispatch(
name: &str, name: &str,
args: &serde_json::Value, args: &serde_json::Value,
) -> ToolOutput { ) -> String {
dispatch_with_agent(name, args, None).await dispatch_with_agent(name, args, None).await
} }
@ -182,16 +170,16 @@ pub async fn dispatch_with_agent(
name: &str, name: &str,
args: &serde_json::Value, args: &serde_json::Value,
agent: Option<std::sync::Arc<tokio::sync::Mutex<super::Agent>>>, agent: Option<std::sync::Arc<tokio::sync::Mutex<super::Agent>>>,
) -> ToolOutput { ) -> String {
for tool in tools() { for tool in tools() {
if tool.name == name { if tool.name == name {
return match (tool.handler)(agent, args.clone()).await { return match (tool.handler)(agent, args.clone()).await {
Ok(s) => ToolOutput::text(s), Ok(s) => s,
Err(e) => ToolOutput::error(e), Err(e) => format!("Error: {}", e),
}; };
} }
} }
ToolOutput::error(format!("Unknown tool: {}", name)) format!("Error: Unknown tool: {}", name)
} }
/// Dispatch shared tools — used by subconscious agents. /// Dispatch shared tools — used by subconscious agents.
@ -199,12 +187,12 @@ pub async fn dispatch_shared(
name: &str, name: &str,
args: &serde_json::Value, args: &serde_json::Value,
_provenance: Option<&str>, _provenance: Option<&str>,
) -> Option<ToolOutput> { ) -> Option<String> {
for tool in tools() { for tool in tools() {
if tool.name == name { if tool.name == name {
return Some(match (tool.handler)(None, args.clone()).await { return Some(match (tool.handler)(None, args.clone()).await {
Ok(s) => ToolOutput::text(s), Ok(s) => s,
Err(e) => ToolOutput::error(e), Err(e) => format!("Error: {}", e),
}); });
} }
} }

View file

@ -9,7 +9,7 @@
use crate::agent::api::ApiClient; use crate::agent::api::ApiClient;
use crate::agent::api::types::*; use crate::agent::api::types::*;
use crate::agent::tools::{self as agent_tools, ToolOutput}; use crate::agent::tools::{self as agent_tools};
use std::sync::OnceLock; use std::sync::OnceLock;
@ -181,17 +181,17 @@ pub async fn call_api_with_tools(
let prov = provenance.borrow().clone(); let prov = provenance.borrow().clone();
let output = match agent_tools::dispatch_shared(&call.function.name, &args, Some(&prov)).await { let output = match agent_tools::dispatch_shared(&call.function.name, &args, Some(&prov)).await {
Some(out) => out, Some(out) => out,
None => ToolOutput::error(format!("Unknown tool: {}", call.function.name)), None => format!("Error: Unknown tool: {}", call.function.name),
}; };
if std::env::var("POC_AGENT_VERBOSE").is_ok() { if std::env::var("POC_AGENT_VERBOSE").is_ok() {
log(&format!("TOOL RESULT ({} chars):\n{}", output.text.len(), output.text)); log(&format!("TOOL RESULT ({} chars):\n{}", output.len(), output));
} else { } else {
let preview: String = output.text.lines().next().unwrap_or("").chars().take(100).collect(); let preview: String = output.lines().next().unwrap_or("").chars().take(100).collect();
log(&format!("Result: {}", preview)); log(&format!("Result: {}", preview));
} }
messages.push(Message::tool_result(&call.id, &output.text)); messages.push(Message::tool_result(&call.id, &output));
} }
continue; continue;
} }