-
Notifications
You must be signed in to change notification settings - Fork 0
ADR 0010 structured handoff field
Status: Accepted
The multi-agent pipeline pattern already exists in this repo's own starter
content — collections/base/software-engineer/agents/orchestrator.md
routes work through builder → verifier → security-auditor
(conditionally) → code-reviewer → docs-writer, and every one of those
subagents documents its own next step in a prose "## Handoff" section.
That routing is real and already followed by hand-written agents, but it
exists only as free text — nothing in the Canonical IR, the compiler, or
the frontend can answer "which agents does this one hand off to" without a
human reading the prose. That blocks two things this phase wants to build:
an Orchestration Gallery that visualizes an existing pipeline as a diagram,
and a wizard that composes a new pipeline and needs to generate frontmatter
that means the same thing the hand-written prose does.
The field only makes sense on agent artifacts — rules, skills, workflows,
and model configs don't hand off to anything — and only some agents (an
orchestrator's mode: primary agents, mainly) will ever populate it. Most
existing agent rows, and probably most future ones, will never set it.
Add handoff_to: list[str] | None to the Canonical IR as a new optional
field on agent artifacts, stored the same way tags/target_compatibility
already are: a nullable Text column on artifacts
(backend/alembic/versions/d1e2f3a4b5c6_add_handoff_to_artifacts.py),
JSON-encoded, decoded through the existing _artifact_to_read()/
_db_to_canonical() conversion choke points (AGENTS.md rule 11) rather
than a new mechanism. Unlike tags/target_compatibility (which default
to "[]"), the column defaults to NULL — None on the wire means "not
declared" and is kept distinct from [] ("declared, but terminal — never
hands off"), since most rows will never touch this field at all and a
default empty list would blur that.
The value is a flat list of agent names (strings), not artifact IDs and
not foreign keys. _parse_agent_file (both the backend and CLI scanners,
kept in sync per AGENTS.md rule 8) reads it from an optional handoff_to:
frontmatter key, mirroring how mode/model are already read. The prose
"## Handoff" section stays in every hand-written agent body as the
human-readable version of the same fact — the frontmatter is the
machine-readable version, and the two are expected to stay consistent by
convention, not by any enforced coupling.
compile_profile() gets one new whole-profile validation pass, added
after the existing per-collection dedup loop rather than inside it (a
handoff_to target may live in any collection composed into the profile,
so it can only be checked once the final deduplicated artifact set is
known): every referenced name is checked against that set, and an
unresolved reference produces a dangling_handoff ValidationIssue —
reusing the exact warnings plumbing Phase 1 built for name_collision
(AGENTS.md rule 32) rather than inventing a second mechanism.
-
A new
agent_handoffsjoin table (from_artifact_id,to_artifact_id, both FKs intoartifacts) — rejected. This is the "correct" relational shape and would get real referential integrity for free, but it breaks the moment ahandoff_totarget lives in a different collection that hasn't been created yet, or is composed into a profile later — the whole point of the field is to describe routing before a specific profile's composition is known, the same wayProfile.additional_collection_idsis already a JSON list of IDs rather than a join table (see data-model.md) for exactly this reason. A join table would also need a migration path for every future scanner/import source that produces agent artifacts, where a plain string list round-trips through frontmatter,ArtifactCreate, and GitHub export with no special handling. -
Reference by artifact ID instead of name — rejected. IDs aren't
known at authoring time (a hand-written
orchestrator.mdincollections/has no artifact row yet — one is only created when a collection is seeded/scanned/imported), and names are already the effective identity of an artifact throughout this codebase: dedup incompile_profile()is by name (rule 29), not ID. Using name keepshandoff_toconsistent with how the rest of the system already treats artifact identity. -
Enforce referential integrity at write time (reject a
handoff_toentry that doesn't resolve, at scan/import/create time) — rejected. At the point an agent is scanned or created, the target of itshandoff_tomay legitimately not exist yet in that same request (it could be in a collection not yet imported, or a sibling agent file processed in a different order), and profile composition — the only point at which the full available agent set is actually known — happens later and separately. Validating at compile time (viadangling_handoff, same timing and shape asname_collision) is the only point where "does this resolve" is actually answerable, so that's where the check lives.
-
handoff_tois advisory metadata, not enforced routing. Nothing stops an agent'shandoff_tofrom drifting out of sync with its own prose "## Handoff" section — Epic 3.2's starter-pack update keeps them consistent by hand, and there's no automated check that they stay that way. A future enhancement could lint for this (e.g. flag when an agent'shandoff_tolist and the agent names mentioned in its own body diverge) but that's not implemented here. - No adapter's
translate()readshandoff_to— it doesn't change a single compiled file's content for any existing target framework. It's consumed entirely by new frontend surfaces (the Orchestration Gallery's diagram, the pipeline wizard's generated frontmatter) added in this same phase. - A
handoff_toreference is only ever checked against a specific profile's fully-composed artifact set, never in isolation. The same agent, with the samehandoff_tolist, can be "clean" in one profile and produce adangling_handoffwarning in another, depending on which collections that profile happens to include — this is inherent to referencing by name across collection boundaries (see the rejected join-table alternative above) rather than a bug in the check itself. - Like
name_collision,dangling_handoffnever blocks compilation — it's alevel: "warning"ValidationIssue, surfaced the same way (CLIpulloutput, the zip's_myace_warnings.txt,TargetExporter.tsx's warnings panel) with no code changes needed at any of those call sites, since they already render whatever's in thewarningslist generically rather than special-casingname_collision.
Generated from docs/ by scripts/sync_wiki.py. Back to repo
- Home
- Architecture
- Data Model
- Invariants
- Extending MyACE
- Debugging
-
ADR Index
- ADR-0001-canonical-ir-as-markdown-with-frontmatter
- ADR-0002-session-cookie-auth
- ADR-0003-ownership-based-authorization
- ADR-0004-github-export-via-rest-api
- ADR-0005-email-password-baseline-auth
- ADR-0006-encrypted-admin-editable-secrets
- ADR-0007-additive-user-role-column
- ADR-0008-collection-moderation-state-machine
- ADR-0009-manifest-based-drift-detection
- ADR-0010-structured-handoff-field
- ADR-0011-public-demo-sandbox
- ADR-0012-manual-collection-freshness-verification
- ADR-0013-post-hoc-unpublish
- Adapter Research