Subagents

Subagent-as-tool. Parent spawns scoped children via the `task` tool.

drover’s subagent model is “subagent-as-tool.” The parent agent gets a task tool the harness auto-injects when spec.subagents is declared. Calling it spawns a child run inline. The child returns its validated output as the tool result; the parent reads + composes.

Declare

ts
const planner = defineAgent({
  id: "planner",
  /* ... */
  subagents: {
    allowed: ["researcher", "summariser"],
    depth: 2,    // child max depth
    fanOut: 3,   // max concurrent children
  },
});

const researcher = defineAgent({
  id: "researcher",
  systemPrompt: "Research the topic and return 3 talking points.",
  /* ... */
});

import { staticRegistry } from "@drover/facade";

runAgent(planner, input, {
  agentRegistry: staticRegistry({ researcher, summariser }),
});

task tool shape

ts
task({
  agent_type: "researcher",
  prompt: "Research the convergence of agent harnesses",
  input?: { /* full inputSchema match */ },
  model?: "haiku",
  max_turns?: 5,
})

If input is omitted, the child receives { prompt } — works for child specs with inputSchema: { prompt: string }. For richer inputs, pass input explicitly.

Enforcement

  • allowed: child’s agent_type must be on the list. Otherwise SubagentLimitError with reason: "not_allowed".
  • depth: parent.depth + 1 > maxDepthSubagentLimitError, reason: "depth". Default 2.
  • fanOut: inFlight >= fanOutSubagentLimitError, reason: "fan_out". Default 3.

Limit errors surface as tool errors to the parent (not run crashes) — the parent’s model can adapt.

Child lifecycle

parent  ┐
        ├─ tool_call_start (task)
        ├─ subagent_start { childRunId, agentId }
        │  (child runs internally; events not mirrored)
        ├─ subagent_end { childRunId, status }
        └─ tool_call_end (task)  ← child's validated output as content

subagent_start and subagent_end fire on the parent stream — visible to stepTracerPlugin and the eval viewer.

Internal events from the child are NOT mirrored to the parent (too noisy). If you want the full child timeline, attach an observer to spec.plugins on the child spec directly — those events still flow through the harness, just not to the parent’s emit callback.

Child run ids

parent:1, parent:2, etc. — suffix counter per parent. Grandchildren get parent:2:1, parent:2:2, etc. Trace ancestry walks the suffix chain.

Sharing context

Child inherits parent’s:

  • cwd and env
  • signal (abort propagates parent → children)
  • meta (free-form tags)

Child gets fresh:

  • runId (suffixed)
  • depth (parent + 1)
  • parentRunId

The sandbox is the parent’s sandbox by default. If you want children to have a different sandbox, override at the harness level (advanced — usually you want the same boundary).

Type to search…

↑↓ navigate open esc close