Delegation
Call a specialist, receive its result, and keep ownership of the turn.
Delegation borrows execution, not ownership. Every child returns to its caller; the root agent settles the user's turn.
Treat a sub-agent as a function
The caller supplies validated input and a budget. The child runs its own turn, then returns a structured result or an explicit failure. The caller resumes with that result before it can finish.
User → Agent A → Agent B → Agent C
← result ← result
← final responseA child turn crosses the same four gates as any other, minus the root-only admit checks — the ambient context was validated when the root turn opened and is inherited as-is, the thread claim belongs to the root, and only a root turn carries a request envelope. A child can delegate further, but cannot transfer ownership of the root conversation or directly finish its caller's turn. Delegation is not a handoff.
Declare the connection
asTool(researcher, {
budget: { steps: 6, timeMs: 15_000 },
deliverable: sourcesPart,
onError: "feedback",
});Only budget is required — asTool throws without one. Everything else
defaults on the caller's behalf:
| Option | Default | Meaning |
|---|---|---|
budget | required | the steps (and optionally time and output tokens) the child may spend |
retries | 0 | how many times a non-completed child is re-run — each retry is a fresh child turn |
onError | "feedback" | a child failure returns as the call's result; "fail" settles the caller failed with the child's cause |
forward | off | opt-in visibility: progress and an allowlist of parts |
deliverable | none | a part the child must submit; the plain part form grants 1 nudge round, { part, nudges } sets your own |
id | run-<agent.id> | the tool name the caller's model sees |
description | Delegate to the <agent.id> agent | the tool description in the caller's window |
input | z.object({ brief: z.string() }) | the call's schema, validated before any child opens |
access | "public" | who may open the connection — the one deliberate asymmetry with defineTool, which requires a rule |
deliverable requires the child to submit schema-valid data for a declared
content part. Without a deliverable contract, successful delegation returns
the child's text and structured parts. Use a deliverable when downstream
work requires a specific result shape.
Receive the result
The caller's model receives a JSON tool result matching DelegationResult.
With the reference app's sources deliverable, the shapes are:
// Success under a deliverable contract: the validated part data itself
{ ok: true, deliverable: { query: "decree 42", items: [{ title: "Result A", ref: "ws://doc-1" }] } }
// Success without a deliverable contract
{ ok: true, result: { text: "…", parts: [/* the child's custom parts */] } }
// Failure, including the underlying child's cause
{ ok: false, cause: { code: "delegation-failed", message: "…", detail: { /* cause, agent */ } } }result.parts carries custom parts only — the child's text is already
in result.text, and progress and outcome frames never cross the edge.
Invalid arguments return ok: false with tool-input-invalid before any
child runs. Other child failures return to the caller as feedback by default.
With onError: "fail", the caller settles failed instead of continuing.
Retries are explicit and bounded; a user stop is never retried.
A completed child does not prove the caller completed its own work. Keel requires another caller model step after delegation, even when the model that requested the child also reported a stop. Reserve enough caller steps to process the result.
Keep child output private by default
Child text, custom parts, and outcome frames stay on the child's own event stream. They do not automatically appear on the caller's user-facing wire. Studio can still inspect every child turn, including its tools and failures.
The caller can summarize a returned result or emit its own selected parts. This keeps the final response with the agent that accepted the request.
Forward visibility explicitly
When the UI needs progress or selected live parts, opt in on the connection:
asTool(researcher, {
budget: { steps: 6 },
deliverable: sourcesPart,
forward: {
progress: true,
parts: [sourcesPart],
},
});progress exposes compact status and character-count summaries, not raw
child text — one progress part per delegation call, updating in place as
the child advances (replace-by-id merge, one stable id). parts is an
allowlist of typed parts; each must be declared in both the child's and the
caller's emits, matched by name. Forwarded part ids are scoped to the
delegation call and attempt, so retries never collide with each other or
with the caller's own parts.
Forwarding is visibility, not substance. Forwarded parts land on the
caller's wire like anything else — they appear in the caller's
payload.parts and persist into thread history for clients to render. The
exclusion is precise: at the close gate they do not count toward the
caller's substance check, and they never satisfy the caller's own
deliverable contract. Forwarded events are also provisional — a child can
still fail after emitting them, and retries may expose multiple attempts.
Child outcome frames are never forwarded as caller outcome frames.
In nested delegation, each caller controls its own outward visibility. An inner forwarding choice does not automatically expose output at the root.
Cancel through the tree
Pass a signal on the root turn:
const controller = new AbortController();
const outcome = await runtime.runTurn({
agent: assistant,
ambient,
envelope,
budget: { steps: 8, timeMs: 30_000 },
hooks: { signal: controller.signal },
});
// The UI's stop handler can call controller.abort() while this runs.The root envelope is one of the admit checks children skip: it is
schema-validated, and its threadId must equal ambient.threadId — a
mismatch is a client/turn parity bug, refused at the door as
envelope-invalid.
The signal is inherited by descendants. Stop propagates back through callers without retrying or resuming their model loops. Each child also receives a time ceiling no larger than its caller's remaining time; delegation cannot reset the parent's deadline.
Cancellation is cooperative at execution boundaries. It does not forcibly terminate an already-running provider request or tool function. Once that work returns, the runtime observes cancellation before continuing.
Next: Tools.
