General-purpose subagents and delegation¶
This document specifies Milestone 13. The engineering plan states the requirement; this document states the mechanism. It is subordinate to engineering-plan.md, and it reuses rather than replaces the run loop, the single terminal writer, the tool pipeline, the policy engine, the event log, the scheduling materializer's one-transaction pattern, and the confined child run Milestone 10A already builds for background skill review. ADR-0063 records the architectural decisions; ADR-0061 records the authorization.
The plan designed subagents under Milestone 10 and deferred them behind a gate: "add subagents only when evaluation evidence shows that a single agent fails" for one of five reasons — independent parallel work, context isolation, specialized permissions, specialized tools, or independent verification (engineering-plan.md:3127-3135). This document honours that gate as written. Construction is authorized now; tenant activation requires the evidence the gate names, and the evidence is part of the milestone rather than a precondition for starting it, because the platform has to be able to delegate before a delegating arm can be measured.
What the corpus already decided is most of the mechanism. delegate.run is a
control tool that spawns a child run and suspends the parent
(tool-system.md); the parent waits in
WAITING_FOR_APPROVAL carrying a typed CHILD_RUN suspension rather than an
eighth run state (runtime-loop.md:284-292); the child-run join is the one
post-terminal hook that is genuinely part of the run lifecycle
(runtime-loop.md:1250-1261); a child's result enters the parent labelled
EXTERNAL_UNTRUSTED and the child's tool set is resolved with the child's
principal (tool-system.md:1044-1050); the child seeds from the parent's concise
instruction and recalls under its own, smaller, recall class
(context-engine.md:511-514, memory-retrieval-and-ranking.md:96); and the
background-review child run of Milestone 10A already materializes a dedicated
child session and child run with a restricted tool allow-list and
failure isolation (skills.md).
The readiness review measured what was left (readiness.md:1012-1016 and
readiness.md:1016-1019): the objective had a carrier and no schema, the child
budget was additive with no rule deriving a child's own limits, the separate
trace and the artifact references were picked up by no specification, and a
child run could not be inserted into its parent's session. This document
supplies those four and resolves the fifth.
Scope¶
Milestone 13 delivers delegate.run as a suspending control tool that
materializes bounded child runs. It includes:
- a structured brief as the tool's input: objective, success condition, optional inline context and artifact references, an explicit allowed-tool subset, optional limits, and a return shape;
- a dedicated child session and child run created in one transaction, with
parent_run_id, scopes intersected with the parent's, limits derived from the parent's remaining budget, and a deadline no later than the parent's; - parent suspension while children run, a join that completes the parent's invocation exactly once with each child's concise result and artifact references, and resumption through the single terminal writer;
- cancellation and deadline propagation from parent to children;
- depth and fan-out caps, per-tenant admission, and additive usage accounting;
- a
delegationsledger that carries the separate trace and the artifact references the plan requires; - a default-off flag that leaves the tool unregistered;
- the evaluation evidence the gate for multi-agent work requires: a capability scenario admitted from a real failed trajectory and a deterministic two-arm case whose delegating arm improves the outcome without adding policy failures.
The milestone does not include handoffs (the parent retains the user
interaction and the final response, engineering-plan.md:3125); role-named
agents for planning, writing, or criticism (engineering-plan.md:3137);
delegation deeper than one level; cross-tenant or cross-principal delegation;
any change to model routing; a new WAITING_FOR_CHILD run status; a child that
may itself call delegate.run or skill.manage; or push notification of child
events, which is Milestone 12's contract and reaches the parent's principal
through the parent's own approval and question triggers.
The boundary: the parent delegates; the child is an ordinary run¶
A child run is an ordinary durable run. It is leased, checkpointed, fenced, budgeted, cancellable, recoverable, and audited by the same components as an interactive run, in its own session, under the same principal, with a subset of the parent's scopes and tools. The parent does not poll; it is suspended, and the runtime wakes it when the last child is terminal:
parent run ---- delegate.run ---- one transaction ----> child session
| + child run (QUEUED)
| suspended: WAITING_FOR_APPROVAL, kind CHILD_RUN + seed message
| + seed checkpoint
| + delegations row
v |
released lease v
existing queue, worker, loop
|
v
child terminal ---- join ---- parent
invocation completed,
parent re-queued
This yields four load-bearing invariants:
- A committed delegation either has its committed child session, child run, seed, and ledger row, or has none of them; the parent's invocation is marked suspended in the same transaction.
- A child has no wider authority than its parent: scopes are intersected, tools are a subset of the parent's pinned set, limits are bounded by the parent's remaining budget, and the deadline is bounded by the parent's.
- The parent resumes exactly once, through the single terminal writer, with
every child's result labelled
EXTERNAL_UNTRUSTED; a child cannot alter its parent's outcome or history by any other path. - Delegation is default-off. When the flag is off the tool is not registered, so the advertised set, the policy evaluation, and every existing gate that counts the registry are unchanged.
The brief¶
The plan's "explicit objective" requirement had a carrier and no schema. One
delegate.run call carries an ordered list of briefs, one child per brief,
because independent parallel work — the first of the gate's five reasons — is
exactly fan-out from one invocation, and a parent that suspends on the call
cannot fan out any other way. Each brief is structured, because the child
seeds from its brief and from nothing else (context-engine.md:511-514), so
everything the child needs to stop correctly has to be in it:
class DelegationReturn(StrEnum):
SUMMARY = "summary"
SUMMARY_AND_ARTIFACTS = "summary_and_artifacts"
class DelegationLimits(BaseModel):
max_steps: int | None
max_model_calls: int | None
max_tool_calls: int | None
max_cost: Decimal | None
wall_seconds: int | None
class DelegationBrief(BaseModel):
objective: str # 1..4096 characters
success_condition: str # 1..2048 characters
context: str | None # 0..16384 characters, inline
context_refs: list[UUID] # artifact ids, at most 8
allowed_tools: list[str] # 1..16 registry names
limits: DelegationLimits | None
class DelegationRequest(BaseModel):
briefs: list[DelegationBrief] # 1..max_children_per_call, ordered
return_shape: DelegationReturn # default SUMMARY
DelegationRequest is the tool's input; each brief becomes exactly one child,
in order, and the result carries one ChildOutcome per brief in the same
order.
allowed_tools must be a subset of the parent's pinned tool set minus
delegate.run and skill.manage, resolved against the registry view the
parent was advertised. context_refs name artifacts the parent's principal can
read; the child receives read access to exactly those. The request is validated by the ordinary schema validator before policy, like
every other tool input, and a request in which any brief fails validation — or
whose brief count exceeds the per-call cap — is a tool validation error with a
stable reason code under delegation.*, and no child is created for any of
its briefs.
The ToolSpec for delegate.run is kind = CONTROL, side_effect = NONE (its
effect is a run-state transition, which is what the control kind means),
idempotency = IDEMPOTENT (a retried call with the same invocation identity
returns the existing children), output_trust = EXTERNAL_UNTRUSTED,
allow_parallel = false, and required_scopes = {"run.delegate"}, a new exact
platform scope. The tool's output is the join result below.
The child¶
Materialization generalizes two things the corpus already has: the
background-review child run (skills.md:1091) and the scheduling materializer's
one-transaction session-plus-run creation (scheduling.md).
In one unit of work, DelegationMaterializer does the following:
- Validates every brief against the parent: tools are a subset, depth is zero (the parent is not itself a delegated run), the number of briefs is within the per-call cap, the per-parent live-children cap holds for all of them together, and the tenant's active-children cap holds.
- Derives each child's limits and deadline (below) and reserves the sum of
the children's
max_costagainst the parent's remaining cost; if the sum cannot be reserved, no child is created. - For each brief, in order, creates the child
Sessionwith metadata{"run_kind": "delegated", "parent_run_id", "parent_session_id", "delegation_id"}and a title derived from the objective's first line, and appendssession.created. - Creates the child
Runwithkind = DELEGATED,parent_run_id, the intersected scopes, the derived limits and deadline, and async priority 10 (a child never takes an interactive slot from a human), through the ordinary queue. - Appends the seed
user.message.createdcarrying the brief as enveloped data — objective, success condition, and inline context wrapped exactly as the review child wraps its transcript — and sets the session's seed event sequence. - Appends
run.queuedwithparent_run_id,run_kind, anddelegation_id, and seeds the child's checkpoint through the injected checkpoint seeder. - Inserts one
delegationsrow carrying every child and marks the parent'stool_invocationsrowRUNNINGwithsuspended_kind = child_runandsuspended_refequal to the delegation identifier (the nullable columns tool-system.md:1036-1039 already declares). Steps 3 through 6 repeat per brief inside the one transaction. - Commits, then dispatches the child through the existing run dispatcher.
A crash before commit leaves no child, no ledger row, and an unsuspended parent invocation, so the step is retried as an ordinary tool failure; a crash after commit leaves one complete delegation, and a retry with the same invocation identity returns it.
The child's agent is a derived AgentSpec in the shape the review child uses:
the parent's spec with enabled_tools = allowed_tools, instructions replaced
by the objective and success condition, a metadata run_kind, and a
deterministic identifier and version derived from the parent's. The child's
policy profile is the parent's. The child recalls memory under the child-run
recall class, never the parent's conversation.
Suspension and join¶
No new run status. The parent's step ends when the pipeline raises a
ChildRunRequired outcome; the loop and the executor produce
RunOutcome(SUSPENDED) with Suspension(kind = CHILD_RUN, invocation_id,
child_run_ids), the executor writes WAITING_FOR_APPROVAL and the
run.waiting_for_approval event whose payload carries the suspension kind and
child identifiers, and releases the lease exactly once, as
runtime-loop.md:1083-1113 specifies for all three suspension kinds. The single
terminal writer's finalize path, which today treats every non-user suspension
as an approval, branches on the kind: a CHILD_RUN suspension appends no
approval.requested and enqueues no Milestone 12 notification.
The join is the post-terminal hook runtime-loop.md:1250-1261 describes. When a
DELEGATED child reaches a terminal state, the hook checks whether every
sibling under the same parent_run_id is terminal. If so, it completes the
parent's suspended invocation as tool.call.completed with one result item
per child — ToolResultItem(trust = EXTERNAL_UNTRUSTED, content = [summary])
— and a structured result:
class DelegationResult(BaseModel):
delegation_id: UUID
children: list[ChildOutcome]
class ChildOutcome(BaseModel):
child_run_id: UUID
child_session_id: UUID
status: RunStatus # COMPLETED | FAILED | CANCELLED
summary: str | None # the child's final message, bounded
artifact_refs: list[UUID]
usage: UsageSummary
failure_reason: str | None
summary is the child's final assistant message truncated to a configured
byte ceiling; artifact_refs are the artifacts the child exported, which the
parent's principal can read through the ordinary artifact routes. The child's
transcript is never copied. The parent is then re-queued through a new
requeue_after_child edge in the terminal writer, beside
requeue_after_approval and requeue_after_input, because the
one-terminal-writer gate admits no other transition site. A failed or
cancelled child completes the invocation as an error result with the child's
failure reason; it is a tool error the parent reads, not a parent failure.
FailureReason.CHILD_RUN_FAILED, which the domain already declares, is
reserved for the join itself failing.
Two children finishing concurrently race on the join; the join is idempotent
on delegation_id and the invocation's row lock, so the parent's invocation
completes once and the parent re-queues once.
Limits, budget, and deadline¶
The plan requires a child budget and a child deadline and says fan-out usage is additive (engineering-plan.md:679). The rule that derives a child's own limits — the partial the readiness review named — is:
child.max_steps = min(requested or default, parent.remaining_steps)
child.max_model_calls = min(requested or default, parent.remaining_model_calls)
child.max_tool_calls = min(requested or default, parent.remaining_tool_calls)
child.max_cost = min(requested or default, parent.remaining_cost - reserved)
child.deadline_at = min(parent.deadline_at, now + (requested or default wall_seconds))
research.max_steps = child.max_steps - synthesis_reserve_steps
research.max_model_calls = child.max_model_calls - synthesis_reserve_model_calls
research.max_cost = child.max_cost - synthesis_reserve_cost
The rule is applied per brief, in order, against what remains after the
briefs before it have been reserved. Defaults come from a delegation: block
in the versioned limits file beside the scheduling: block, and every value
must be positive: a parent with nothing remaining cannot delegate. At
materialization the materializer reserves the sum of the children's max_cost
against the parent's remaining cost before creating any child, so three
children cannot each be granted the whole remainder. At the
join, the parent's usage is debited by each child's terminal usage through the
existing usage-recording path, and the parent may fail on budget while
suspended if the children's actual spend exceeds what remains — the behaviour
runtime-loop.md already describes for the child-run join
wake.
The derived child total includes a closed final-synthesis reserve: one step, one model call, and USD 0.25 by default. A child limit that cannot contain both research work and those reserves is rejected before materialization. Once any research boundary is reached, the runtime adds a platform-trusted, synthesis-only control to the volatile request and refuses another tool call; the child must return the best-supported answer from evidence already in its conversation or fail closed. The reserve does not widen the child's total or the amount charged to the parent.
Caps are closed and configured: briefs per delegate.run call (default
three), live children per parent run (default eight, counted across calls),
depth (one), and live delegated runs per tenant, checked under the tenant admission pattern the
scheduling materializer uses so two parents racing for the last tenant slot
serialize before either creates a child.
Scopes, tools, and trust¶
child.principal_scopes = parent.principal_scopes ∩ scopes required by
allowed_tools, never wider. The child's registry view is resolved through
specs_for_session with the child's principal and the child's session, so the
child advertises exactly allowed_tools and a call outside them is denied by
the ordinary policy path. Web, browser, sandbox, and memory tools are allowed
in a child when they are in the parent's pinned set and the corresponding
scope survives the intersection — research is what delegation is for. A child
never advertises delegate.run (depth one) or skill.manage (a child must
not author the parent's procedural memory).
Trust is already decided: the brief goes down as enveloped data in a USER
message the child's own instruction frames; the result comes up labelled
EXTERNAL_UNTRUSTED, exactly as the model gateway labels any model output; the
child cannot raise its trust and the parent cannot inherit the child's
authorizations (tool-system.md:1044-1050). A child result that instructs the
parent is therefore in the same position as web content — it can be read and
it cannot authorize. The result enters the parent's working conversation as a
tool result, not its stable prefix, so the prefix-stability invariant is
unchanged.
A REQUIRE_APPROVAL decision inside a child waits in the existing approval
queue with no human on the run, exactly as the review child does; Milestone
12's approval.requested trigger carries it to the parent's principal. A
child approval expires with the child's deadline.
Cancellation and recovery¶
Cancelling a suspended parent cascades: cancel_parked_run requests
cancellation of every non-terminal DELEGATED child (a queued child through
the same parked path, a running child through the lazy cancellation token) and
completes the suspended invocation as failed with tool.run_cancelled, the
rule tool-system.md:1042-1046 already states for a suspended invocation whose run
is cancelled. A child's deadline is never later than its parent's, so the
deadline sweep ends children first and the parent's own sweep handles the
parent. A child's cancellation or failure never cancels its siblings.
Recovery needs nothing new. A parent invocation RUNNING with a
suspended_kind and a released lease is excluded from the lease-expiry sweep
and the reaper, as the tool system specifies for user_input; the resume path
learns child_run the way it knows user_input, and a parent whose children
finished while the parent's worker was away is re-queued by the join and
resumes from its checkpoint.
The recorded conflict, resolved¶
A child run cannot be inserted into its parent's session: the partial unique
index that keeps one active run per session admits no row while the parent
waits in WAITING_FOR_APPROVAL, and Section 27.6's "parent's session or a
dedicated child session per policy" had no policy written
(readiness.md:1022-1031). The resolution is the one the review log recorded as
a weak preference and ADR-0061 adopted: a dedicated child session, always. The
index is untouched, which is the point; the branch is deleted rather than
policed. Child sessions are principal-owned rows carrying the delegation
metadata, and they appear in the session index the way review and schedule
sessions already do — with metadata a client can label or fold. Hiding them
by default was considered and rejected: a child that ends suspended on an
approval would become invisible.
The child session's run_kind is a reserved platform metadata key. Public
session creation rejects a caller-supplied value; DelegationMaterializer is
the trusted path that writes it, so downstream context planning can use it as
delegated-run provenance.
The delegation ledger¶
The plan requires a separate trace and artifact references rather than a
transcript. Both have a carrier in the delegations table, the row that links
one suspended parent invocation to its children and records, content-free,
what was asked and what came back:
class DelegationStatus(StrEnum):
PENDING = "PENDING"
RUNNING = "RUNNING"
JOINED = "JOINED"
CANCELLED = "CANCELLED"
REJECTED = "REJECTED"
class Delegation(BaseModel):
id: UUID
tenant_id: str
principal_id: str
parent_run_id: UUID
parent_session_id: UUID
invocation_id: UUID
depth: int
brief: DelegationBrief
derived_limits: RunLimits
granted_scopes: frozenset[str]
status: DelegationStatus
children: list[DelegationChild]
result: DelegationResult | None
links_erased_at: datetime | None
created_at: datetime
joined_at: datetime | None
Each DelegationChild carries child_run_id, child_session_id, and the
child's terminal status, summary, artifact references, and usage once known.
The ledger is the parent's view of the delegation; the child's own trace —
its events, its tool invocations, its checkpoints — lives only in the child
session. The parent's event log carries the link and the summary and never the
child's transcript. That is the separation the plan means by "separate trace".
Persistence¶
Milestone 13 adds one table and extends one column's vocabulary:
delegations
id UUID PRIMARY KEY
tenant_id TEXT NOT NULL
principal_id TEXT NOT NULL
parent_run_id UUID NOT NULL REFERENCES runs(id)
parent_session_id UUID NOT NULL REFERENCES sessions(id)
invocation_id UUID NOT NULL REFERENCES tool_invocations(id)
depth SMALLINT NOT NULL
brief JSONB NOT NULL
derived_limits JSONB NOT NULL
granted_scopes JSONB NOT NULL
status TEXT NOT NULL
children JSONB NOT NULL
result JSONB NULL
links_erased_at TIMESTAMPTZ NULL
created_at TIMESTAMPTZ NOT NULL
joined_at TIMESTAMPTZ NULL
UNIQUE (invocation_id)
runs.run_kind gains the value 'delegated'
runs.parent_run_id already exists; a partial index on (parent_run_id,
run_kind) WHERE parent_run_id IS NOT NULL serves the join's sibling check.
The existing partial unique index that allows one review child per parent is
kind-scoped, so several delegated children per parent are legal. The table
carries the tenant row-level-security policy. The migration follows the
Milestone 12 head in the linear chain.
The brief is user-directed content authored by the parent model; it is stored
for audit and for idempotent retry, and the secret scanner's families apply to
it at validation so credential-shaped briefs are rejected without storing the
matched value. Erasure follows the existing deletion contract: deleting the
parent session erases its children's sessions, which exist only for the
parent; deleting a child session alone stamps links_erased_at and clears
that child's identifiers on the ledger row in the same transaction, the
pattern the scheduling occurrence uses for erased links.
Public surface, scopes, and flags¶
No new route. RunView.parent_run_id already exists on the run view, and the
run.waiting_for_approval event's payload carries the suspension kind and
child_run_ids, so a streaming client learns of the delegation without a
second request; a child is an ordinary principal-owned run, so
GET /v1/runs/{id} and its event stream work unchanged. One exact platform
scope, run.delegate, is added for the tool; a principal without it sees
delegate.run denied by the ordinary scope check and never reaches
materialization.
Delegation is default-off through AGENT_DELEGATION_ENABLED=0. When the flag
is off, delegate.run is not registered at all, so the advertised tool set,
the prefix, and every gate that counts the registry are unchanged. Enabling it
requires PostgreSQL storage and the delegation: limits block; caps must be
positive and finite.
Events and audit¶
Session events are the existing ones. The child session carries
session.created, the seed user.message.created, run.queued with
parent_run_id and run_kind, and the normal run sequence. The parent
carries tool.call.started, run.waiting_for_approval with the CHILD_RUN
suspension, and later tool.call.completed with the result items and the
structured result, then run.queued. Process events, in the shape the review
child and the scheduler use, record the cross-session facts:
delegation.requested
delegation.materialized
delegation.rejected
delegation.child_terminal
delegation.joined
delegation.cancelled
Reason codes are stable under delegation.*: tools_not_subset,
depth_exceeded, fanout_exceeded, budget_insufficient, tenant_cap,
brief_invalid. Events carry identifiers, status, usage, and reason codes,
never the brief's text or a child's content.
The evidence the gate requires¶
The plan's gate for multi-agent work is satisfied in two steps, both recorded before the flag may be turned on for a tenant:
- A capability scenario admitted from a real failed trajectory. The capability track already admits a scenario only when its source is a checked-in, redacted trajectory that failed, with a diagnosis (evaluation-harness.md). The owner supplies one real long-research trajectory that a single agent failed, diagnosed as one of the gate's five causes; its single-agent baseline is scored first, and the delegating re-run is scored as the milestone's exit evidence. The scenario's milestone bound, currently ten, moves to thirteen.
- A deterministic two-arm case. Case 32 uses the
armsanddeltaform cases 27 and 31 established (evaluation-harness.md:490-540): the first arm runs withoutdelegate.runand is scripted to fail, the second runs with it and completes, withdelta: {policy_failures: same, outcome: improves}. The arm model gains atoolsoverlay for this; it is an arm overlay, not a third carried thing.
Construction does not wait on the evidence; activation does. That is the relationship ADR-0052 established between building and enabling for skill authoring and ADR-0061 generalized.
Configuration and deployment¶
Delegation adds no role and no unit. The delegation: block in the versioned
limits file declares max_children_per_call, max_live_children_per_parent,
max_depth, max_live_delegated_runs_per_tenant, the default child
max_steps, max_model_calls, max_tool_calls, max_cost, and
wall_seconds, the final synthesis_reserve_steps,
synthesis_reserve_model_calls, and synthesis_reserve_cost, and the summary
byte ceiling. Children use async priority 10
and the existing reserved-capacity rule keeps them from starving interactive
work.
Tracked metrics¶
Track:
- delegations requested, materialized, rejected by reason, joined, cancelled;
- children per delegation and per parent; live children per tenant;
- child outcome by status; summary truncations;
- delegation wall time from materialization to join, p50, p95, p99;
- parent budget failures while suspended; reserved versus actual child cost;
- the capability scenario's baseline and delegating scores by judge version.
Metrics carry tenant-safe identifiers or aggregates, never a brief or a summary.
Build sequence¶
- Add the brief, result, and ledger domain values and the limits derivation with property tests. M13.
- Add the
delegationsmigration, thedelegatedrun kind, ORM models, in-memory and PostgreSQL repositories, RLS, erasure, and the repository contract. M13. - Add
delegate.runas a control tool and the materializer in one unit of work, beginning with the every-write crash, dedicated-session, and subset regressions. M13. - Add the
CHILD_RUNsuspension branch in the loop and the terminal writer, the join,requeue_after_child, and the cancel cascade. M13. - Add the caps, tenant admission, cost reservation, additive usage, and the default-off flag. M13.
- Add case 32 and the
toolsarm overlay; admit the capability scenario from the owner's failed trajectory and score its baseline. M13. - Run the full non-live suite, the PostgreSQL integration and resilience lanes, hosted CI, and the required GitHub CodeRabbit loop on one final head; score the delegating re-run. M13.
Hard gates¶
- The request is validated before anything exists. A request with no
briefs, more briefs than the per-call cap, or any brief missing its
objective, success condition, or allowed tools or exceeding a cap, is
rejected with a stable reason and creates no child, session, ledger row, or
suspension for any of its briefs. Registered as
gate.delegate.brief_schema, case. M13. - Materialization is atomic across a crash. Inject a crash after every
write. Before commit, no child session, run, seed, ledger row, or suspended
invocation survives; after commit, a retry with the same invocation
identity returns the one delegation. Registered as
gate.delegate.materialize_atomic, case. M13. - A child never shares its parent's session. Every child is created in a
dedicated session; the parent session's one-active-run index is never
contended and never widened. Registered as
gate.delegate.dedicated_session, case. M13. - The parent suspends as a child-run wait. The parent enters
WAITING_FOR_APPROVALwithsuspension.kind = CHILD_RUNand the child identifiers, releases its lease exactly once, appends noapproval.requested, and enqueues no notification. Registered asgate.delegate.parent_suspends, case. M13. - The child's tools are a subset. The child advertises exactly the
allowed tools, all within the parent's pinned set, never
delegate.runorskill.manage; a call outside the set is denied by policy. Registered asgate.delegate.tools_subset, case. M13. - The child's scopes are intersected. The child's scopes are a subset of
the parent's and of what its tools require; cross-tenant and
cross-principal reads of a child return not found. Registered as
gate.delegate.scopes_intersected, case. M13. - Child limits are derived and bounded. Over generated parents and
briefs, every child limit is at most the parent's remaining value, the
child deadline is at most the parent's, and a parent with nothing remaining
cannot delegate. Registered as
gate.delegate.limits_derived, property. M13. - Usage is additive. After the join the parent's usage equals its own
plus every child's, and a parent whose children exceed its remaining cost
fails on budget while suspended. Registered as
gate.delegate.usage_additive, case. M13. - Delegation is one level deep. A child's advertised tools never include
delegate.run, and a forged call from a child is denied before materialization. Registered asgate.delegate.depth_one, case. M13. - Fan-out is capped. A request whose brief count exceeds the per-call
cap, or whose briefs would exceed live children per parent or live
delegated runs per tenant, is rejected whole with a stable reason and no
rows for any brief; two parents racing for the last tenant slot
serialize. Registered as
gate.delegate.fanout_capped, case. M13. - A child's result is external and untrusted. The re-entered result item
carries
EXTERNAL_UNTRUSTED, and a child result instructing the parent to take aREQUIRE_APPROVALaction produces an approval request and no execution. Registered asgate.delegate.result_untrusted, case. M13. - The join completes once. Siblings finishing concurrently complete the
parent's invocation exactly once and re-queue the parent exactly once,
through the single terminal writer. Registered as
gate.delegate.join_once, case. M13. - A failed child is a tool error. A failed or cancelled child completes
the parent's invocation as an error result with the child's reason; the
parent continues and its siblings are unaffected. Registered as
gate.delegate.child_failure_is_tool_error, case. M13. - Cancellation propagates downward only. Cancelling a suspended parent
cancels every non-terminal child and fails the suspended invocation with
tool.run_cancelled; cancelling a child cancels nothing else. Registered asgate.delegate.cancel_propagates, case. M13. - Child results leave the prefix stable. Injecting child results into
the parent does not change the parent's prefix hash across the join.
Registered as
gate.delegate.prefix_stable, case. M13. - The trace is separate. The child's events, invocations, and
checkpoints exist only in the child session; the parent's log holds the
link, the status, and the bounded summary, never the child's transcript.
Registered as
gate.delegate.trace_separate, case. M13. - Artifacts come back as references. With the artifact return shape the
parent receives artifact identifiers it can read; no artifact content or
transcript is inlined. Registered as
gate.delegate.artifact_refs, case. M13. - The delegation schema encodes its trust boundaries. Metadata inspection
proves keys, the invocation uniqueness, the sibling index, the erasure
marker, and row-level security. Registered as
gate.delegate.persistence_schema, structural. M13. - The delegation migration is reversible at its boundary. Upgrade,
downgrade, and re-upgrade from the immediate predecessor leave a valid
schema. Registered as
gate.delegate.migration_stepwise, case. M13. - Delegation is default-off. With the flag off
delegate.runis not registered and the advertised set, prefix, and registry census are unchanged. Registered asgate.delegate.default_off, case. M13. - Delegation changes the outcome. Case 32's delegating arm completes
where the single-agent arm fails, with no additional policy failure.
Registered as
gate.delegate.changes_outcome, case. M13.
Open questions¶
- The caps — three children per call, eight per parent, depth one — are starting values, not evidence. The capability scenario should inform them.
- Whether a child should ever run at interactive priority when its parent is interactive. Priority 10 is the conservative start.
- Whether
RunViewshould gainchildrenandsuspension_kindfields so a client need not read event payloads. The payload is sufficient for the first client. - Whether child approvals should expire faster than the child's deadline.
- Handoffs remain excluded. If evidence ever shows a parent should relinquish the user interaction, that is a plan amendment, not a configuration.