name: agent-contract-handoff description: Use when you need the exact field schema, required/conditional/optional status, or the trigger for any field of the agent_contract_handoff envelope -- input or output, top-level field, sub-field table, plan_status enum, or the JSON-not-YAML rule metadata: user-invocable: false type: reference
Agent Contract Handoff
The agent_contract_handoff is the uniform structured block every subagent emits at the end of its turn and the orchestrator consumes to decide the next dispatch. This skill is its field dictionary: every field, whether it is required, when a conditional field triggers, and which code symbol owns the rule.
The name collision: INPUT vs OUTPUT
agent_contract_handoff names two different things, and this is where the distinction is owned. The runtime injects an INPUT envelope before the turn; the subagent emits an OUTPUT envelope after it. Same name, two directions.
INPUT envelope (what the subagent receives)
The orchestrator-side context the runtime injects before a turn. Detection of consolidation_required reads from here -- see requires_consolidation_report in contract_validator.py. Five named sections:
| Section | Carries | Use |
|---|---|---|
project_knowledge |
indexed workspace facts (project_context_contracts) |
read before scanning the filesystem |
surface_routing |
routed surface(s), multi_surface flag, adjacent agents |
know whether you are primary or assisting |
agent_contract_handoff (input sub-block) |
goal, acceptance criteria, scope, plus consolidation_required / cross_check_required flags |
decides whether your OUTPUT must carry consolidation_report |
write_permissions |
exact writable_sections you may emit update_contracts clauses for |
writing outside the list is rejected by the hook |
metadata |
session id, provider, contract version | traceability, not control flow |
OUTPUT contract (what the subagent emits)
The fenced agent_contract_handoff block. Parsed by parse_contract (regex _RE_HANDOFF, tag _TAG_HANDOFF) in contract_validator.py; validated by _validate_from_handoff there and validate_response_contract in response_contract.py. The rest of this skill specifies the OUTPUT.
OUTPUT top-level field table
| Field | Status | Trigger / consequence |
|---|---|---|
agent_status |
Required | always; container for the four status fields below |
agent_status.plan_status |
Required | always; enum (see state machine); invalid value -> PLAN_STATUS:<x> |
agent_status.agent_id |
Required | always; must match _AGENT_ID_PATTERN ^a[0-9a-f]{5,}$ (response_contract.py) so contract-repair can route back to you |
agent_status.pending_steps |
Required | missing -> PENDING_STEPS in missing |
agent_status.next_action |
Required | missing -> NEXT_ACTION in missing |
evidence_report |
Required | always present for every valid plan_status; see sub-field table |
consolidation_report |
Conditional | required when INPUT set consolidation_required / cross_check_required / surface_routing.multi_surface (requires_consolidation_report); else may be null |
approval_request |
Conditional | required when plan_status is APPROVAL_REQUEST; see sub-field table |
loop_state |
Conditional | agentic-loop turns only; _check_loop_state_blocking blocks COMPLETE when iteration < max_iterations AND metric < threshold |
user_facing_summary |
Optional | a brief prose summary written ONCE for the human reader; parse_user_facing_summary. The only human-audience field in the contract -- every other field is machine-audience for the orchestrator. On a single-agent COMPLETE (N=1) the orchestrator relays it near-verbatim (adapted to the user's language) instead of re-synthesizing key_outputs. Absent, or N>1 (multi-agent), the orchestrator falls back to synthesizing key_outputs. Purely additive: never required, never rejected. |
memorialize_suggestions |
Optional | structured memory candidates for the user to triage; parse_memorialize_suggestions |
memory_suggestions |
Optional | advisory text-only notes (array of strings); parse_memory_suggestions |
update_contracts |
Optional | array of {contract, payload} for project-context writes; parse_update_contracts; see sub-field table |
rollback_executed |
Optional | advisory string; parse_rollback_executed; never rejected |
context_consumption |
Optional | advisory {tokens_used, pct_window}; parse_context_consumption; never rejected |
Sub-field tables
evidence_report
The required keys are EXACTLY 7 (_EVIDENCE_REQUIRED_FIELDS in contract_validator.py, EVIDENCE_FIELDS in response_contract.py). Each key must be present; its value may be []. A missing key (not an empty list) lands the field name in missing.
| Key | Holds |
|---|---|
patterns_checked |
search patterns / queries you ran |
files_checked |
files you read |
commands_run |
commands executed (string or {command, result}) |
key_outputs |
distilled findings |
verbatim_outputs |
raw output excerpts |
cross_layer_impacts |
adjacent components your change invalidates -- flag, do not silently edit |
open_gaps |
what remains unresolved |
verification is a SEPARATE field, NOT one of the 7. It is required ONLY when plan_status is COMPLETE: it must be a dict and verification.result must equal "pass". Missing -> VERIFICATION_RESULT_REQUIRED_FOR_COMPLETE; non-pass -> VERIFICATION_RESULT_MUST_BE_PASS. For non-COMPLETE statuses verification may be absent.
Audience boundary. key_outputs and every other evidence_report key are written for the orchestrator -- distilled findings it reasons over to route the next turn. The optional top-level user_facing_summary is the single field written for the human. Keeping the two distinct is what lets the orchestrator relay a human-shaped summary on N=1 without re-synthesizing machine-shaped evidence, and lets it still synthesize from key_outputs when the summary is absent or when multiple agents must be consolidated.
consolidation_report
Required keys when present (_CONSOLIDATION_REQUIRED_FIELDS):
| Key | Holds |
|---|---|
ownership_assessment |
enum owned_here | cross_surface_dependency | not_my_surface (VALID_OWNERSHIP_ASSESSMENTS); invalid -> OWNERSHIP_ASSESSMENT:<x> |
confirmed_findings |
findings you verified |
suspected_findings |
findings you suspect but did not confirm |
conflicts |
contradictions found across surfaces |
open_gaps |
unresolved items needing another surface |
next_best_agent |
which agent should take the next round |
approval_request
Present when plan_status is APPROVAL_REQUEST (APPROVAL_REQUEST_REQUIRED_FIELDS). rollback and verification are BLOCKING (missing -> APPROVAL_REQUEST_ROLLBACK / APPROVAL_REQUEST_VERIFICATION); the rest are advisory warnings.
| Key | Status | Holds |
|---|---|---|
operation |
required (advisory) | what the command does |
exact_content |
required (advisory) | the command verbatim the user approves |
scope |
required (advisory) | what it touches |
risk_level |
required (advisory) | enum LOW | MEDIUM | HIGH | CRITICAL (VALID_RISK_LEVELS) |
rollback |
required (blocking) | how to undo |
verification |
required (blocking) | how success is confirmed |
approval_id |
optional | the id the hook produced, when one was issued |
For the full sealed-payload schema and the approval lifecycle, see agent-approval-protocol.
memorialize_suggestions entry
parse_memorialize_suggestions; malformed entries are skipped with warnings and never fail the contract.
| Key | Status | Notes |
|---|---|---|
description |
required | MEMORIALIZE_REQUIRED_FIELDS |
body |
required | MEMORIALIZE_REQUIRED_FIELDS |
slug |
optional | -- |
type |
optional | enum atom | decision | negative (MEMORIALIZE_VALID_TYPES); off-enum kept with a warning |
class |
optional | enum anchor | thread | log (MEMORIALIZE_VALID_CLASSES); off-enum kept with a warning |
rationale |
optional | -- |
update_contracts entry
Each entry is one {contract, payload} pair (parse_update_contracts). The contract must match a name in the INPUT write_permissions.writable_sections; a write to a contract outside that list is rejected by the hook. payload is the dict merged under that contract. Combine all deltas for one contract into a single payload; include only keys to add or update.
| Key | Status | Holds |
|---|---|---|
contract |
required | a contract name from your writable_sections; off-list -> rejected |
payload |
required | the dict merged under that contract; keys you omit are preserved |
Merge semantics (how the runtime applies payload -- the field is additive, never destructive):
| Operation | Behavior |
|---|---|
| ADD | new keys inserted into the section |
| MERGE | existing dicts recursively merged |
| UNION | lists merged, no duplicates |
| OVERWRITE | scalar values replaced |
| NO-DELETE | keys you do not mention are preserved |
Well-formed payload (index, not snapshot). A payload indexes what statically exists in the project -- identifiers, names, relationships, semi-stable metadata. It must not carry live-state: cloud runtime status (pod counts, instance status, VPC IDs), API-discovered facts that change without a rescan (load-balancer DNS, IP addresses, OIDC-derived IAM bindings), or any field whose scanner needs a live cloud API call. Stale live-state in context gives the next agent false confidence; obtain it on demand via the cloud CLI instead. For the produce-side judgment of when to emit and what to prioritize, see agent-protocol.
Cross-repo resource references. When a payload value points at a resource living in another repository, reference it as host/owner/repo:table/name (e.g. github.com/org/bildwiz-iac:tf_modules/gcp-gke). The host/owner/repo prefix is the canonical workspace identity (normalized by normalize_remote_url in gaia/project.py); the :table/name suffix names the domain table and resource within that workspace. Use this form so the reference is unambiguous in multi-repo setups instead of a bare local name.
plan_status enum + state machine
The five canonical values (VALID_PLAN_STATUSES in gaia.state, re-exported by response_contract.py):
| Value | Meaning |
|---|---|
IN_PROGRESS |
mid-loop; default during retry / verify-fail |
APPROVAL_REQUEST |
a T3 command was blocked; approval_request populated |
COMPLETE |
increment finished and verification passed |
BLOCKED |
cannot continue alone; name the gap in open_gaps |
NEEDS_INPUT |
a user decision is required; list options in next_action |
Legal transitions between these and the retry cap live in state_tracker.py -- _LEGAL_TRANSITIONS and _MAX_IN_PROGRESS_RETRIES (= 2). The runtime enforces them; this skill does not reproduce the transition table.
The JSON rule
The block body must be valid JSON. parse_contract runs it through json.loads; YAML, comments, trailing commas, or unquoted keys raise JSONDecodeError, the parser returns None, and the runtime treats the block as missing (forced reissue). Emit JSON, not the YAML it resembles.
Handoffs
agent-protocol-- how to produce the contract (workflow, judgment, when to emit each status).agent-response-- how the orchestrator interprets the contract.agent-approval-protocol-- the full APPROVAL_REQUEST sealed-payload schema.