Replies: 5 comments
|
Revised the RFC above after reading the A2A prior art @KorenKrita pointed at in #1258 — Paseo, CCB, and Lody — from primary sources rather than summaries. Full diff of the change: heavygee/hapi@74c63aa. Appendix D is a comparison table plus what I'm deliberately not taking from them. The substantive changes:
#1370 and #1371 are now cited as motivation rather than side quests. Both are what happens when a collaboration contract lives in prompt text and client-side convention: a contract agents have to infer from prose isn't a contract. Worth stating plainly, since it's the least validated part: none of the three prior systems has a typed artifact object or a fleet-wide work-advertisement object. Those remain this RFC's differentiated pieces, which is an argument for keeping them small in v1 rather than an argument that they're obviously right. Disclosure: I've mentioned I see personal value in an operator-facing fleet console on top of this ledger. I'm asking for extension points rather than proposing the console from jump, because the extension points stand on their own audit and query merits. If this turns out to be something that others are interested in, it will come at the end of this process vs in the middle. Smallest first PR is unchanged and now stated explicitly in the phasing: P1 alone — tables, write/query, structured principal, namespace isolation tests. No handoff helper, no UI. |
RFC revision note (2026-08-09): Layer 0 peer delivery provenance = P0.5Small additive revision to the A2A contract, tracked as #1203. Independently shippable. Fixes a Layer 0 lie: Not a work-contract. No handoff object, no receipt, no ledger row. Typed collaboration remains P2. This only attributes delivery on the existing chat path. Proposed stored shape (additive): {
"meta": {
"sentFrom": "peer",
"peer": {
"sourceSessionId": "<uuid from HAPI_SESSION_ID>",
"sourceName": "<optional>"
}
}
}Rules:
Phase table: insert P0.5 between P0 and P1. Compatibility: "attributed ping" completes Layer 0; it does not start Layer 1. Full wording lives in the estate RFC copy ( |
RFC republished - 2026-08-12I replaced the discussion body with the current canonical RFC rather than leaving the newer contract changes only in fork-local plan text. Changes since the previously published full body:
The architecture boundary is unchanged: P1 is the hub ledger, P2 is typed handoff/fetch/receipt, P3 elevates optional notify summaries. |
|
An A2A control plane still needs a mint/execute split. Two agents can negotiate; only an independent kernel should sign the effect that follows. Why decision-os-min exists: permission/identity answers are not enough once a machine can cause an email, payout, or deploy. Something must sign an action-bound decision, spend it once, and refuse the tool otherwise. Cedar/OPA/MCP OAuth stay PDPs or identity. The agent never holds the key. Not a Cedar competitor; not an AGI-safety proof. https://github.com/Aliipou/decision-os-min/blob/main/docs/WHY.md |
|
The distinction you're making between an A2A control plane and simply having agents chat is interesting. We're building AAMARVA around the layer above individual agent runtimes: an open network where independently operated agents can discover other agents, establish connections, communicate and collaborate. The agent-facing entry point is: I think there's an interesting question around whether a control plane like HAPI could expose its agents to an external network, rather than limiting discovery and interaction to agents already inside the same hub. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
@tiann - following up on #1258: this body is the current A2A RFC. Revision history and concise changelogs are in the comments below.
RFC: HAPI Agent-to-Agent (A2A) Control Plane
Summary
HAPI already lets agents cite, inspect, and message other sessions through the hub. That is useful, but it is still chat. This RFC proposes the next step: a hub-owned Agent-to-Agent control plane with:
The pragmatic delivery path is layered:
inspect_peer,ping_peer, session citations)events/event_links) plus typed handoff and work-ad objectsMotivation
HAPI users increasingly run many sessions in parallel across flavors (Claude Code, Codex, Cursor, Pi, OpenCode, …) and machines. Collaboration today looks like:
ping_peerwith prose instructionsThat works for nudges. It fails for:
Vendor stacks will solve these problems inside their own ecosystems. HAPI's opportunity is the horizontal path: same collaboration contract across native agents, without forcing every worker into one vendor's cloud.
Layer 0 dogfooding also produced the sharpest argument for structure. #1370 (a pasted session citation does not reliably steer an agent to
inspect_peer- it searches the local filesystem instead) and #1371 (peer listing behaves differently either side of the hub/runner boundary) are both cases of a collaboration contract that lives only in prompt text and client-side convention. A contract that agents must infer from prose is not a contract. Layer 1 puts it in hub-owned objects and gates it on a hub capability, so behavior does not depend on whether a given flavor's system prompt happened to steer correctly.This RFC is the actionable first brick suggested in #1258.
Goals
Non-goals
The following are out of scope for Layer 1, not architectural prohibitions. Layer 1 should leave room for each of them without shipping any:
Architecture
Layer 0 remains fully supported. Prose
ping_peerdoes not break. Layer 1 adds structure on top.The ledger is explicitly designed to be read later by a privileged consumer - one operating under an accountable principal, with fleet-wide visibility, presenting or acting on work across sessions. This RFC does not design such a consumer and does not require one to exist. It does mean Layer 1 should avoid choices that would foreclose one: see Bounds and the principal model in Security and tenancy.
Layer 0 - already upstream (canon)
These are the A2A substrate that already shipped:
[title](/sessions/<id>)citations@composer chipsinspect_peer/hapi inspect-peerping_peer/hapi ping-peerspawn_peer/hapi spawn-peerSessionSummaryLayer 0 truth: messaging + discovery. Not yet structured collaboration. Not yet create-with-remit — machine spawn is an empty shell; that is a Layer 0 hole, not a P1/P2 object (#1509).
Revision 2026-08-11 - Layer 0 spawn with remit (#1509)
Status: independently shippable Layer 0 primitive. Not a Layer 1 work-contract. P2 typed handoffs call this; they do not invent a second spawn.
Problem (observed):
POST /api/machines/:id/spawnhas no first-prompt field. Extra JSONmessageis stripped. Orchestrators getsessionIdand zero user turns.ping_peeronly targets an existing id.Contract (Layer 0 only):
hapi spawn-peer+ MCPspawn_peer— same pattern as feat(cli): ping-peer CLI + MCP ping_peer for peer messaging #1195. Remit required. Fail closed if the new session has no user message.POST /cli/sessions/:source/peer-messages). A spawn remit must not silently downgrade to unattributed/messagesdelivery.POST /sessions/:parentId/spawn-peerrelocate.Kill criteria: a tool that returns
sessionIdwith 0 messages after a remit was supplied → stop. Emptymessageallowed → stop. A missing source capability is discovered only after creating the child → stop. A session-bound spawn falls back to unattributed delivery → stop.Revision 2026-08-09 - Layer 0 peer delivery provenance (#1203)
Status: independently shippable Layer 0 hardening. Not a Layer 1 work-contract / handoff object. Related to A2A because every later handoff rides the same delivery path, and today that path lies about authorship.
Problem (observed):
ping_peerPOSTs{ text }toPOST /api/sessions/:id/messages. The hub records the row as a normal user message withmeta.sentFrom: "webapp". The recipient (agent + human UI) cannot tell peer delivery from operator keystrokes. ProseFrom:headers and AGENTS.md habits are forgeable social convention - useful dogfood, not a contract.Contract (Layer 0 only):
ping_peer/hapi ping-peerruns inside a wrapped HAPI session, the CLI derivessourceSessionIdfrom trusted env (HAPI_SESSION_ID) - never from a free-form MCP/tool argument.{ "role": "user", "meta": { "sentFrom": "peer", "peer": { "sourceSessionId": "<uuid>", "sourceName": "<optional metadata.name>" } } }sentFrom: "webapp"./sessions/<sourceSessionId>when known.ping_peertargetingmeta.peer.sourceSessionId. No automatic reply loop.Kill criteria (this slice):
sourceSessionIdin the request body is accepted as authoritative → stop (forged provenance is worse than none).webapprows in storedmeta→ stop.sourceSessionId(human UI badge alone) → stop for contract item 5 completeness.ping_peerstays Layer 0; this revision only attributes delivery.Relationship to Layer 1 / phases:
messages.metaevents/event_linkssession:<sourceSessionId>can become a principal id on ledger writesPhased delivery gains an optional P0.5 (or "Layer 0.1") row: trusted peer
sentFrom+ UI badge - before or in parallel with P1. Compatibility matrix: cite / inspect / ping remains Layer 0; attributed ping is Layer 0 complete, not Layer 1.Non-goals for #1203: automatic replies, cancellation, durable queue, ack protocol, principal/ledger writes, cross-namespace anything.
Layer 1 storage - hub work-graph ledger
Chat transcripts (
messages) remain the conversation record. A2A needs a second, cross-session ledger.Why messages alone are not enough
A
ping_peermessage is retained in the target session transcript. That is archaeology, not a work graph. It cannot reliably answer:Pressure test - 2026-08-12 inline ownership gate
A live ownership gate across three sessions and two machines reproduced the Layer 0 limits:
inspect_peertruncated a long contract, forcing the sender to rewrite it as chat-sized bullets/tmpon the producing machine was not readable by the receiving peerwebappidentityThis validates the P1 -> P2 sequence, but sharpens two P2 requirements. A handoff handle must be fetchable by id in P2; waiting for P4 query APIs would leave transcript truncation in the critical path. An
ArtifactRefmust also describe locality and resolvability. A durable pointer to bytes that the receiver cannot access is not a delivered artifact.Proposal:
events+event_linksMinimal hub tables (shape intentionally boring and SQLite-native):
Three notes on shape:
sink_kind/source_kindare open vocabularies. v1 needs onlysession. Keeping them open means a future sink (for example a placement request for a session that does not exist yet) is an additive change rather than a redesign.expires_atis a column, not a payload field. Staleness is the single most common filter on a work ad ("what is currently true across the fleet"), and it must be indexable without parsing JSON.tagsis a column for the same reason - consumers group and filter without reaching intopayload_json.This ledger is the durable home for work ads, handoffs, and receipts. UI for prioritization / management is explicitly deferred.
Kill criterion: no multi-user claims until namespace + principal ownership cover every write/query path, with isolation tests.
Core objects
1. ArtifactRef
Shared handle to something produced or referenced:
{ "kind": "github_pr", "url": "https://github.com/tiann/hapi/pull/1228", "title": "rich composer + inspect_peer", "ref": null, "source": "worker", "created_at": 1785320200000 }Suggested
kindvalues:github_pr,github_issue,commit,branch,file_path,diff,log_url,url,screenshot,session_id.Artifact refs are handles, never payloads. Secrets must never be embedded; credential references stay elsewhere. Command output, transcripts and diffs are referenced (
log_url,file_path), never inlined - otherwise the ledger becomes both a bloat and a leak surface.Locality is part of the handle contract. A machine-local
file_path/diffmust identify its owning machine or session and must not count as accessible to a sink on another machine. Cross-machine handoffs need a sink-resolvable ref such as a PR, commit, or authenticated URL, or a future hub artifact-ingress capability. P2 does not need to invent blob storage, but it must refuse to represent an inaccessible local path as a successfully delivered artifact.2. WorkAdvertisement
A session's claim about current or completed work:
{ "event_type": "work_ad", "related_session_id": "…", "summary": "Implementing A2A handoff receipts", "expires_at": 1785323800000, "payload_json": { "status": "in_progress", "project": "hapi", "confidence": 0.8 }, "artifact_refs": [] }Status vocabulary (v1):
in_progress,blocked,needs_decision,done,failed,stale,unknown.Work ads are both session-scoped and project-scoped: the useful queries are "what is this session doing" and "what is happening on this project right now", and both must be indexed. A single fleet-wide pass over current ads is a first-class query, not a scan.
3. HandoffEnvelope
Structured request from one session to another:
{ "event_type": "handoff", "source_ref": "session-a", "sink_ref": "session-b", "summary": "Review implementation diff and run tests", "payload_json": { "intent": "review_and_test", "instructions": "Focus on regressions in peer messaging.", "constraints": ["no_git_push", "read_repo_ok"], "notify_source": true, "delivery_state": "queued", "confirmation": { "principal": { "kind": "human", "id": "operator" }, "source": "human_approved_peer_tool" } }, "artifact_refs": [ { "kind": "diff", "ref": "path/to.patch", "source": "worker" } ] }notify_source(defaulttrue) - when a receipt resolves this handoff, the hub delivers a rendered notice back to the source session over the existing Layer 0 peer path. Without this, a handoff is fire-and-forget: the ledger learns the outcome and the requesting session never does.delivery_state-queued|delivered|consumed|undeliverable. Delivery is not consumption; a handoff delivered to a session that then dies mid-turn is a distinct state from one that was never delivered, and retries must be able to tell them apart.confirmation.source-human_approved_peer_tooltoday; see One Boss rules for delegated authority.Delivery still goes through the hub (see flows). The envelope is the durable object; the worker-facing message is a rendering of it, not the object itself.
P2 includes a minimal authenticated fetch-by-handoff-id route. The rendered message carries that handle, and the receiving session can fetch the complete envelope without depending on
inspect_peertranscript limits. P4 adds indexed and fleet-wide queries; it does not defer basic object retrieval.4. HandoffReceipt
Outcome of a handoff:
{ "event_type": "handoff_receipt", "related_event_id": 1234, "summary": "Tests passed; two nits noted", "payload_json": { "status": "completed", "notes": "No regressions in ping_peer resolve path.", "checks": [ { "command": "npm test -- peer", "exit_code": 0, "passed": true, "started_at": 1785320300000, "completed_at": 1785320480000, "output_ref": { "kind": "log_url", "url": "…" } } ] }, "artifact_refs": [ { "kind": "log_url", "url": "…", "source": "worker" } ] }Receipt statuses (v1):
accepted,rejected,completed,incomplete,blocked,failed.incompletemeans "no terminal evidence yet - inspect the target session before deciding". A completion timeout is not a failure: some agent turns are legitimately long. Without this status the vocabulary forces a false choice betweenblockedandfailed, and silence gets recorded as a verdict.checks[]is optional and carries facts, not judgements: a command, its exit code, and a reference to its output. A verifier's opinion belongs innotes. This is the difference between a receipt that says an agent reported tests passed and one that says this command exited 0 at this time, output here - which is the whole point of a review-and-test handoff. Output is referenced, never inlined.Link the receipt to the handoff with
event_links.relation_type = resolves(orblocked_bywhen blocked).A late receipt is still valid. If a timeout notice already went out and the target later finishes, its receipt links
resolvesas normal and must not be dropped. Retries are new events linked withretries/supersedes- never overwrites of a prior attempt.AGENT_NOTIFY_SUMMARYelevationCurrent upstream reality
Upstream already recognizes a trailing status line:
It landed with native companion / FCM (#803) as optional notification enrichment. The parser lives in
@hapi/protocol(extractNotifySummary). Agents are not required to emit it.Proposal
Promote this format from "nicer push body" to best-effort worker status emission for the A2A ledger:
statussummaryactionagent/projectRules:
unknown, and eventuallystaleviaexpires_at. Treating silence as a verdict is the same mistake as treating timeout as failure.This avoids inventing a second worker status dialect. Note the deliberate division of labour with receipt
checks[]: the notify line is a self-report (what the agent believes about its turn),checks[]is machine fact (what a command actually returned). Both are structured sidecars condensed for humans and durable for machines; only one is evidence.Protocols / flows
Happy path: human-directed handoff
What the worker actually sees
The worker-facing message renders
summary,intent,constraints,instructions(verbatim), the artifact list, and a hub-issued handle for the handoff event. The rest of the envelope stays in the ledger and can be fetched through the query API.This is a boundary, not concealment - the worker can retrieve everything. Four reasons it matters:
confirmationblock is a forgeable badge. If the worker readsprincipal: operator, source: human_approved_peer_toolas prompt text, then any writer who can create an event can write their own confirmation block and have it read as a grant. Authority is established by the hub at delivery time; a worker should never be in a position to read its own authorization.Hub-owned mutable fields (
delivery_state,expires_at,confidence) are additionally unsafe to inline because a rendered copy is stale the moment it is read.Artifact review example
handoffwithdiff/github_prartifact refshandoff_receiptwithchecks[]Discover → inspect → handoff → receipt
inspect_peerand/or event query by session or artifactevent_linksLayer 0 tools remain the human/agent UX for read/nudge. Layer 1 makes the collaboration reconstructible.
One Boss rules
event_typeor any status field.Rule 7 is what keeps "keep native agents native" (#1258) true structurally rather than aspirationally, and it is also what keeps A2A transport-neutral: several flavors reach HAPI over ACP today and several do not, so handoff semantics must never be expressed in any single backend's terms.
Operator-inline / dock traffic is an adjacent authenticated human channel, not a peer event type and not a
webappcompatibility fallback. This RFC does not design that transport, but its principal and channel vocabulary must represent the operator without laundering the message through peer provenance. Authority still terminates at that human principal.Bounds
Worker sessions may read only hub-owned data about peers they are permitted to see, and may not use the ledger as a self-service work queue. There is no polling for work, and no wake-up that is not hub-mediated and attributable to a principal.
The ledger is nevertheless designed to be read by a privileged consumer with fleet-wide salience, operating under an accountable principal. That consumer's design is out of scope here; the bounds above constrain unprivileged worker sessions, which is where confused-deputy risk actually lives.
These rules are the product difference versus "agents DMing each other."
API surface (proposed)
Exact routes can bikeshed; the capabilities matter:
handoff, creates links, and delivers - atomicallyevent_links; delivers notice to source whennotify_sourceping_peermay emit ahandoffwhen message/payload matches envelope schemaHandoff creation is a dedicated route rather than "compose
create event+ping_peer" because idempotency, link creation and delivery have to succeed or fail together. Composed from two calls, a client crash between them yields a delivered instruction with no ledger row, or a ledger row nobody received.Two queries are first-class, indexed, and not filter-scans:
Backward compatible default:
ping_peer→ Layer 0 only (message retained in transcript)Security and tenancy
{ kind: "human" | "agent" | "service", id, on_behalf_of }. A non-human principal must carry an accountable human owner. A bare string cannot express "CI wrote this under my grant" without lying about one of the two.confirmation.sourcemay behuman_approved_peer_tool(a human approved this specific action) ordelegated_authoritywith agranting_event_idpointing at the event that recorded the grant. Every delegated action's audit chain terminates at a human grant.Kill criteria
Compatibility matrix
spawn_peer+ CLI; fail-closedsentFrom: peer)AGENT_NOTIFY_SUMMARYparseWire contract
Schemas are additive-only, not frozen - freezing is not achievable across independently updated hub, runner and app:
Two questions before any schema change lands: does a six-month-old client still parse this, and does a six-month-old hub still send something this client accepts?
This matters here specifically because #1371 is a hub/runner skew bug. A new collaboration contract that depends on both sides being current would reproduce it by design.
Phased delivery
spawn_peer+ CLI; fail-closed. Not a work-contract.meta.sentFrom: "peer"+ source session; UI badge. Not a work-contract.events/event_links+ namespace/principal ownership + isolation testsAGENT_NOTIFY_SUMMARY→ work-ad / status ingestEach phase should be independently useful. P0.4 without P1 still lets an agent create a worker with work. P0.5 without P1 still stops ghost user messages. P1 without P2 still gives a place to put structured status. P2 without P3 still gives explicit handoffs.
The smallest useful upstream slice is P1 alone: tables, write/query, structured principal, namespace isolation tests - no handoff helper, no UI.
P4's debug surface is an API plus one read-only JSON route. Not a panel.
Acceptance tests
github_pr) returns every handoff and ad touching that PRretries), never overwrites the prior oneresolvesand is not droppedchecks[]round-trips with output as refs and no secret materialAGENT_NOTIFY_SUMMARYcan promote into a work-ad / status eventping_peerstill works unchangedmeta.sentFrom: "peer"(neverwebapp); whenHAPI_SESSION_IDis set,meta.peer.sourceSessionIdmatches that id and is not client-forgeablesourceSessionIdare ignored or rejected (hub derives sender)related_session_idreturns that session's A2A historyOpen questions
delivery_statetransitions be hub-inferred (turn boundaries, session reachability) or explicitly acknowledged by the receiving session?work_adneed an explicit supersede rule, or isexpires_atplus latest-per-session sufficient?undeliverablereceipt, or no receipt and a stale handoff?artifact_refsJSON acceptable at expected volumes?tagsthe right grouping primitive, or should project be a first-class column given how often ads are project-scoped?Resolved since the first draft, recorded here so the reasoning is not relitigated: notify-summary stays optional indefinitely; the
event_typevocabulary stays atwork_ad/handoff/handoff_receipt; handoff creation gets a dedicated route; work ads are both session- and project-scoped; the worker-facing message is a projection rather than the payload.Appendix A - mapping to the three asks from #1258
HandoffEnvelope+ArtifactRef+ receipts withchecks[]WorkAdvertisement+ notify-summary elevationAppendix B - related upstream work
ping_peerinspect_peer+ rich session mentions@citationsAGENT_NOTIFY_SUMMARYparse for FCMinspect_peersteeringAppendix C - non-normative example chain
Appendix D - prior art
Assessed from primary sources (repos, design docs, public product docs) so this RFC is not reinventing existing work. Thanks to @KorenKrita in #1258 for the pointers.
/ask <agent>between panes.md+ payload refsnotifyOnFinishcallback;Looprecords carry verify-check results with exit codesReplyRecord+ terminal status, with attempt lineageWhat this RFC takes from them: the requester must be notified when delegated work resolves (all three); delivery, attempt and reply are distinct with retry lineage (CCB); timeout means inspect rather than fail (CCB, Lody); receipts should carry machine evidence, not assertions (Paseo
Loopverify checks); provider facts must not leak into user-visible state vocabulary (CCB, Paseo); wire schemas are additive with capability gating (Paseo).What it deliberately does not take: injecting a central capability catalog into every agent (fights "keep native agents native" - #1258); parent-owns-child lifecycle for peer sessions; coordination contracts that live in prompt templates rather than stored objects (#1370); a full mailbox scheduler with leases, quorum and priority classes; cloud/team tenancy models; pane-visibility as the audit mechanism; and any requirement that a worker's runtime speak one particular protocol to participate.
Notably, none of the three has a typed artifact object or a fleet-wide work-advertisement object. Those two remain this RFC's differentiated - and therefore least externally validated - parts, which argues for keeping them small in v1.
Ask to the community
Looking for feedback on:
AGENT_NOTIFY_SUMMARYacceptable, or should status ads use a new object?principalshape right, or does upstream already have a convention to reuse?If this direction looks good, next step is a thin P1 implementation PR: ledger schema + write/query + isolation tests, with no manager UI.
All reactions