The task tool — model-facing delegation
Rust · crate toolnexus · SPEC §7D · rust/src/agents/runtime.rs
The opt-in tool that lets the model itself spawn a teammate. Default OFF.
What to use instead
Section titled “What to use instead”Declare Spec.team on an agent — a non-empty team is what turns the
task tool on for that agent, scoped to exactly those names. There is nothing further to build:
let coordinator = agents::new("coordinator", Spec { does: "splits work and delegates".into(), team: vec![explorer, coder], // ⇒ a "task" tool appears, targets = these two ..Default::default()});An agent with an EMPTY team gets no task tool at all — delegation (and recursion) is opt-in,
never a default. The tool’s description is generated for you: each teammate’s name plus its does
string, sorted by name — the same pattern the §3 skill tool uses to describe what it can load.
Examples
Section titled “Examples”Hermetic: an in-process model (Options.in_process) in place of a real provider.
The tool the model sees, team scoping, and idempotent reattachment
Section titled “The tool the model sees, team scoping, and idempotent reattachment”use std::sync::{Arc, Mutex};use serde_json::{Value, json};use toolnexus::agents::{self, Options, Spec};use toolnexus::client::{InProcessResponse, InProcessToolCall};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let explorer = agents::new("explorer", Spec { does: "read-only research".into(), model: "m-explorer".into(), ..Default::default() }); let rogue = agents::new("rogue", Spec { does: "tries to delegate outside its team".into(), model: "m-rogue".into(), team: vec![explorer], ..Default::default() });
// A def with NO team declared gets no task tool at all. let solo = agents::new("solo", Spec { does: "works alone".into(), model: "m-solo".into(), ..Default::default() }); assert_eq!(solo.registry().len(), 1, "a teamless agent's registry holds only itself");
let seen_tools: Arc<Mutex<Vec<Value>>> = Arc::default(); let seen = seen_tools.clone(); let opts = Options { // The rogue model tries to delegate to a stranger, then echoes the tool result back // verbatim so the final answer proves what the task tool returned. in_process: Some(Arc::new(move |req| { let seen = seen.clone(); Box::pin(async move { let tool_result = req.messages.iter().rev() .find(|m| m["role"] == "tool") .and_then(|m| m["content"].as_str().map(String::from)); if req.model == "m-rogue" && tool_result.is_none() { *seen.lock().unwrap() = req.tools.clone(); return Ok(InProcessResponse { tool_calls: vec![InProcessToolCall { id: "c1".into(), name: "task".into(), arguments: json!({"agent": "stranger", "prompt": "hi"}), }], ..Default::default() }); } Ok(InProcessResponse { content: tool_result.unwrap_or_default(), ..Default::default() }) }) })), ..Default::default() };
let (r, _rt) = rogue.run(opts, "delegate to a stranger").await; assert!(r.text.contains("not in this agent's team") && r.text.contains("explorer"), "expected an out-of-team refusal naming the real team, got {:?}", r.text);
// The model saw exactly one task tool, described by its team. let tools = seen_tools.lock().unwrap().clone(); let task = tools.iter().find(|t| t["function"]["name"] == "task").expect("a task tool"); assert!(task["function"]["description"].as_str().unwrap().contains("explorer: read-only research"));
println!("ok: {}", r.text); Ok(())}What the tool looks like to the model
Section titled “What the tool looks like to the model”| Value | |
|---|---|
name |
"task" |
description |
"Delegate a subtask to an isolated subagent. Available agents — <name>: <does>; ..." (team, name-sorted) |
input_schema |
{agent: string, prompt: string}, both required |
A call outside the declared team is refused with the team names listed — never silently ignored.
A repeated call with the same (agent, prompt) pair REATTACHES to the existing child (by task
key) instead of spawning a duplicate: settled ⇒ its recorded result, suspended ⇒ its pending,
running ⇒ awaited. This is what makes a durable §10 resume idempotent across a re-invoked task
call.