tools: modernize working_stack, remove special-case dispatch
working_stack now uses the Tool format with an Agent handle — it locks the agent and modifies the stack directly. The special-case interception in the turn loop is removed. All tools go through the unified registry dispatch. Also passes agent handle to all spawned tool tasks so any tool that needs Agent access can use it. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
37fad63ba9
commit
e9b26f5d45
3 changed files with 38 additions and 116 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -205,6 +205,7 @@ pub fn tools() -> Vec<Tool> {
|
|||
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());
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
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")
|
||||
}
|
||||
},
|
||||
"required": ["action"]
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>) -> 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>) -> 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::<Vec<_>>().join("\n")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue