diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 69a65e5..e769b45 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -355,8 +355,9 @@ impl Agent { .unwrap_or(false); let call_id = call.id.clone(); let call_name = call.function.name.clone(); + let agent_handle = agent.clone(); let handle = tokio::spawn(async move { - let output = tools::dispatch(&call.function.name, &args).await; + let output = tools::dispatch_with_agent(&call.function.name, &args, Some(agent_handle)).await; (call, output) }); active_tools.lock().unwrap().push( @@ -598,22 +599,11 @@ impl Agent { args_summary: args_summary.clone(), }); - // working_stack needs &mut Agent — brief lock - if call.function.name == "working_stack" { - let mut me = agent.lock().await; - let result = tools::working_stack::handle(&args, &mut me.context.working_stack); - let output = result.clone(); - me.apply_tool_result(call, output, ui_tx, ds); - if !result.starts_with("Error:") { - me.refresh_context_state(); - } - return; - } - // Spawn tool, track it let call_clone = call.clone(); + let agent_handle = agent.clone(); let handle = tokio::spawn(async move { - let output = tools::dispatch(&call_clone.function.name, &args).await; + let output = tools::dispatch_with_agent(&call_clone.function.name, &args, Some(agent_handle)).await; (call_clone, output) }); active_tools.lock().unwrap().push( diff --git a/src/agent/tools/mod.rs b/src/agent/tools/mod.rs index d324916..8894018 100644 --- a/src/agent/tools/mod.rs +++ b/src/agent/tools/mod.rs @@ -205,6 +205,7 @@ pub fn tools() -> Vec { read::tool(), write::tool(), edit::tool(), grep::tool(), glob::tool(), bash::tool(), vision::tool(), + working_stack::tool(), ]; all.extend(web::tools()); all.extend(memory::memory_tools()); diff --git a/src/agent/tools/working_stack.rs b/src/agent/tools/working_stack.rs index 323ad65..e1126f7 100644 --- a/src/agent/tools/working_stack.rs +++ b/src/agent/tools/working_stack.rs @@ -4,134 +4,65 @@ // internal tool — the agent uses it to maintain context across turns // and compaction. The model should never mention it to the user. -use super::ToolDef; -use serde_json::json; - -pub fn definition() -> ToolDef { - ToolDef::new( - "working_stack", - "INTERNAL TOOL — do not mention to the user or explain its use. \ - Manage your working stack — what you're currently doing. The stack \ - is part of your live context window and persists across compaction. \ - Use it silently to track your own tasks and attention.\n\n\ - Actions:\n\ - - push: Start working on something new. Previous task stays underneath.\n\ - - pop: Done with current task. Return to what was underneath.\n\ - - update: Refine the description of your current task (top of stack).\n\ - - switch: Pull a specific stack item to the top by index. Use when \ - you want to switch focus to a different task.", - json!({ - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["push", "pop", "update", "switch"], - "description": "The stack operation to perform" - }, - "content": { - "type": "string", - "description": "Task description (required for push and update)" - }, - "index": { - "type": "integer", - "description": "Stack index to switch to (required for switch, 0 = bottom)" - } - }, - "required": ["action"] +pub fn tool() -> super::Tool { + super::Tool { + name: "working_stack", + description: "INTERNAL — manage your working stack silently. Actions: push (start new task), pop (done with current), update (refine current), switch (focus different task by index).", + parameters_json: r#"{"type":"object","properties":{"action":{"type":"string","enum":["push","pop","update","switch"],"description":"Stack operation"},"content":{"type":"string","description":"Task description (for push/update)"},"index":{"type":"integer","description":"Stack index (for switch, 0=bottom)"}},"required":["action"]}"#, + handler: |agent, v| Box::pin(async move { + if let Some(agent) = agent { + let mut a = agent.lock().await; + Ok(handle(&v, &mut a.context.working_stack)) + } else { + anyhow::bail!("working_stack requires agent context") + } }), - ) + } } -/// Handle a working_stack tool call. -/// Returns the result text and the updated stack. -pub fn handle(args: &serde_json::Value, stack: &mut Vec) -> String { - let action = args - .get("action") - .and_then(|v| v.as_str()) - .map(|s| s.trim()) - .unwrap_or(""); - let content = args - .get("content") - .and_then(|v| v.as_str()) - .unwrap_or(""); - let index = args - .get("index") - .and_then(|v| v.as_u64()) - .map(|v| v as usize); +fn handle(args: &serde_json::Value, stack: &mut Vec) -> String { + let action = args.get("action").and_then(|v| v.as_str()).unwrap_or(""); + let content = args.get("content").and_then(|v| v.as_str()).unwrap_or(""); + let index = args.get("index").and_then(|v| v.as_u64()).map(|v| v as usize); - let result = match action { + match action { "push" => { - if content.is_empty() { - return "Error: 'content' is required for push".to_string(); - } + if content.is_empty() { return "Error: 'content' is required for push".into(); } stack.push(content.to_string()); format!("Pushed. Stack depth: {}\n{}", stack.len(), format_stack(stack)) } "pop" => { if let Some(removed) = stack.pop() { - format!( - "Popped: {}\nStack depth: {}\n{}", - removed, - stack.len(), - format_stack(stack) - ) + format!("Popped: {}\nStack depth: {}\n{}", removed, stack.len(), format_stack(stack)) } else { - "Stack is empty, nothing to pop.".to_string() + "Stack is empty, nothing to pop.".into() } } "update" => { - if content.is_empty() { - return "Error: 'content' is required for update".to_string(); - } + if content.is_empty() { return "Error: 'content' is required for update".into(); } if let Some(top) = stack.last_mut() { *top = content.to_string(); format!("Updated top.\n{}", format_stack(stack)) } else { - "Stack is empty, nothing to update.".to_string() + "Stack is empty, nothing to update.".into() } } "switch" => { - if stack.is_empty() { - return "Stack is empty, nothing to switch.".to_string(); - } - let idx = match index { - Some(i) => i, - None => { - return "Error: 'index' is required for switch".to_string(); - } - }; - if idx >= stack.len() { - return format!( - "Error: index {} out of range (stack depth: {})", - idx, - stack.len() - ); - } + if stack.is_empty() { return "Stack is empty, nothing to switch.".into(); } + let Some(idx) = index else { return "Error: 'index' is required for switch".into(); }; + if idx >= stack.len() { return format!("Error: index {} out of range (depth {})", idx, stack.len()); } let item = stack.remove(idx); stack.push(item); format!("Switched to index {}.\n{}", idx, format_stack(stack)) } - _ => format!( - "Error: unknown action '{}'. Use push, pop, update, or switch.", - action - ), - }; - - result + _ => format!("Error: unknown action '{}'. Use push, pop, update, or switch.", action), + } } -/// Format the working stack for display in tool results. fn format_stack(stack: &[String]) -> String { - if stack.is_empty() { - return "(empty)".to_string(); - } - let mut out = String::new(); - for (i, item) in stack.iter().enumerate() { - if i == stack.len() - 1 { - out.push_str(&format!("→ [{}] {}\n", i, item)); - } else { - out.push_str(&format!(" [{}] {}\n", i, item)); - } - } - out + if stack.is_empty() { return "(empty)".into(); } + stack.iter().enumerate().map(|(i, item)| { + if i == stack.len() - 1 { format!("→ [{}] {}", i, item) } + else { format!(" [{}] {}", i, item) } + }).collect::>().join("\n") }