//! Tools that interact with the harness itself: todo list, questions to the //! user and subagents. use super::{Tool, ToolCtx, ToolOutput, arg_str}; use crate::types::Question; use anyhow::{Result, bail}; use async_trait::async_trait; use serde_json::{Value, json}; pub struct TodoWrite; #[async_trait] impl Tool for TodoWrite { fn name(&self) -> &str { "todo_write" } fn description(&self) -> String { "Maintain a task list for the current work, shown live to the user. Send the complete \ list each time. Use it for multi-step tasks: exactly one item in_progress at a time, \ mark items completed as soon as they are done." .into() } fn schema(&self) -> Value { json!({ "type": "object", "properties": { "todos": { "type": "array", "items": { "type": "object", "properties": { "content": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]} }, "required": ["content", "status"] } } }, "required": ["todos"] }) } fn read_only(&self) -> bool { true } async fn run(&self, input: Value, ctx: &ToolCtx) -> Result { let todos = input.get("todos").cloned().unwrap_or(json!([])); if !todos.is_array() { bail!("todos must be an array"); } ctx.app.set_todos(&ctx.session_id, todos.clone())?; let open = todos.as_array().unwrap().iter().filter(|t| t["status"] != "completed").count(); Ok(ToolOutput::text(format!("Todo list updated ({open} open).")).with_meta(json!({"todos": todos}))) } } // --------------------------------------------------------------------------- pub struct AskUser; #[async_trait] impl Tool for AskUser { fn name(&self) -> &str { "ask_user" } fn description(&self) -> String { "Ask the user one or more questions and wait for the answers. Use this only when you are \ blocked on a decision that is genuinely the user's (requirements, preferences, choices \ between valid approaches) – not for things you can find out yourself. Offer 2-4 concrete \ options per question; the user can always answer with free text instead." .into() } fn schema(&self) -> Value { json!({ "type": "object", "properties": { "questions": { "type": "array", "minItems": 1, "maxItems": 4, "items": { "type": "object", "properties": { "question": {"type": "string"}, "header": {"type": "string", "description": "Very short label (max 12 chars)"}, "multi_select": {"type": "boolean"}, "options": { "type": "array", "items": { "type": "object", "properties": { "label": {"type": "string"}, "description": {"type": "string"} }, "required": ["label"] } } }, "required": ["question"] } } }, "required": ["questions"] }) } fn read_only(&self) -> bool { true } async fn run(&self, input: Value, ctx: &ToolCtx) -> Result { let questions: Vec = serde_json::from_value(input["questions"].clone())?; if questions.is_empty() { bail!("at least one question is required"); } let answers = ctx .app .ask_user(&ctx.session_id, "agent", questions.clone(), ctx.cancel.clone()) .await?; let Some(answers) = answers else { return Ok(ToolOutput::error("The user dismissed the questions without answering.")); }; let mut out = String::from("The user answered:\n"); for q in &questions { let a = answers.get(&q.question).map(|v| match v { Value::String(s) => s.clone(), Value::Array(a) => a.iter().filter_map(|x| x.as_str()).collect::>().join(", "), other => other.to_string(), }); out.push_str(&format!("- {}\n → {}\n", q.question, a.unwrap_or_else(|| "(no answer)".into()))); } Ok(ToolOutput::text(out).with_meta(json!({"answers": answers}))) } } // --------------------------------------------------------------------------- pub struct Task; #[async_trait] impl Tool for Task { fn name(&self) -> &str { "task" } fn description(&self) -> String { "Launch a subagent to handle an independent, multi-step task in its own context (e.g. a \ broad codebase search, a focused review). The subagent only knows what you put in the \ prompt – write a complete, self-contained brief. Its final message is returned to you. \ Several task calls in one response run in parallel (subject to the configured limits). \ With background=true you continue working and receive the result later." .into() } fn schema(&self) -> Value { json!({ "type": "object", "properties": { "description": {"type": "string", "description": "Short (3-5 words) task description"}, "prompt": {"type": "string", "description": "The full brief for the subagent"}, "subagent_type": {"type": "string", "description": "Subagent to use (default: general)"}, "background": {"type": "boolean", "description": "Run in the background: returns immediately, the result arrives later as a message. Use for work you don't need right away (e.g. writing docs, summaries, translations)."} }, "required": ["description", "prompt"] }) } fn read_only(&self) -> bool { // Read-only agents (plan, explore) can spawn read-only subagents; the runner enforces it. true } fn title(&self, input: &Value) -> String { format!("Subagent: {}", input["description"].as_str().unwrap_or("task")) } async fn run(&self, input: Value, ctx: &ToolCtx) -> Result { let prompt = arg_str(&input, "prompt")?.to_string(); let description = input["description"].as_str().unwrap_or("subagent").to_string(); let agent = input["subagent_type"].as_str().unwrap_or("general").to_string(); let (child_id, result) = crate::runner::run_subagent(ctx.app.clone(), ctx, &agent, &description, prompt).await?; Ok(ToolOutput::text(result).with_meta(json!({"session_id": child_id, "agent": agent}))) } }