//! The `task` tool: delegate work to a specialist subagent, foreground or background. //! //! This tool is deliberately thin — it validates input, gates on a `task/` permission, //! and hands off to the engine's `SubagentSpawner` (owned by the composition root), which //! resolves the agent, enforces the depth limit, applies permission intersection, and runs //! the child session. See `docs/04-multiagent.md`. use async_trait::async_trait; use serde::Deserialize; use serde_json::json; use harness_core::tool::{ invalid_input, SpawnError, SpawnRequest, Tool, ToolCtx, ToolError, ToolOutput, }; #[derive(Debug, Deserialize)] struct TaskInput { /// Short human-facing label for the subtask (shown on the job board). description: String, /// The full instruction handed to the subagent. prompt: String, /// Which specialist to run (must be a subagent-capable agent). subagent_type: String, /// Alias or task id of a completed job to continue instead of starting fresh. #[serde(default)] task_id: Option, /// Run in the background and return immediately (tracked on the job board). #[serde(default)] background: bool, } pub struct TaskTool; #[async_trait] impl Tool for TaskTool { fn name(&self) -> &str { "task" } fn description(&self) -> &str { "Delegate a self-contained unit of work to a specialist subagent. Set `background: \ true` to launch it without blocking (track progress on the job board); reuse a \ completed subagent by passing its `task_id`/alias to continue the same session." } fn parameters(&self) -> serde_json::Value { json!({ "type": "object", "properties": { "description": { "type": "string", "description": "Short label for the subtask." }, "prompt": { "type": "string", "description": "Full instruction for the subagent." }, "subagent_type": { "type": "string", "description": "Specialist to run." }, "task_id": { "type": "string", "description": "Alias/id of a completed job to reuse." }, "background": { "type": "boolean", "description": "Launch without blocking." } }, "required": ["description", "prompt", "subagent_type"] }) } async fn execute( &self, input: serde_json::Value, ctx: ToolCtx, ) -> Result { let args: TaskInput = serde_json::from_value(input).map_err(|e| invalid_input(self, e))?; let Some(spawner) = ctx.spawner.clone() else { return Err(ToolError::Other( "subagent delegation is not available in this session".into(), )); }; // Gate on task/. `Always` grants blanket delegation to this specialist. ctx.ask .ask( "task", &args.subagent_type, &args.subagent_type, json!({ "agent": args.subagent_type, "description": args.description, "background": args.background, }), ) .await?; let req = SpawnRequest { parent_session_id: ctx.session_id.clone(), parent_message_id: ctx.message_id.clone(), agent: args.subagent_type.clone(), description: args.description.clone(), prompt: args.prompt, reuse_task_id: args.task_id, background: args.background, cancel: ctx.cancel.clone(), }; let outcome = spawner.spawn(req).await.map_err(map_spawn_error)?; if outcome.background { let alias = outcome.alias.unwrap_or_default(); Ok(ToolOutput { title: format!("launched {} ({alias})", args.subagent_type), output: format!( "Launched background task {alias} ({}). Check the job board; do not poll — \ wait for completion.", outcome.child_session_id ), metadata: json!({ "child_session": outcome.child_session_id, "agent": args.subagent_type, "alias": alias, "background": true, }), }) } else { let text = outcome.final_text.unwrap_or_default(); Ok(ToolOutput { title: format!("{} — {}", args.subagent_type, args.description), output: text, metadata: json!({ "child_session": outcome.child_session_id, "agent": args.subagent_type, "background": false, }), }) } } } fn map_spawn_error(err: SpawnError) -> ToolError { match err { // Depth/agent problems are the model's to fix — surface as tool errors it can read // and act on, not hard failures. SpawnError::DepthExceeded => ToolError::Other(err.to_string()), SpawnError::InvalidAgent(_) => ToolError::Invalid(err.to_string()), SpawnError::ReuseNotFound(_) => ToolError::Invalid(err.to_string()), SpawnError::Other(msg) => ToolError::Other(msg), } }