RFC: the fak Multi-Agent Coordination Protocol
Status: Draft (rungs 1–3 shipped in-process; durable cross-process backing is the named next rung). Issue: #241 · Slug: D-007 · Epic: #304 (Track D — Agent Framework Parity). Sibling epic: #639 (MPI-shaped message-passing primitives). House rule: every primitive named here is on disk with a package test; the honest-scope section says plainly what is in-process today versus durable across a process boundary. No throughput or latency number is asserted here.
This is the authoritative spec the issue’s “RFC/spec document” acceptance names. The three other acceptance items — message passing, shared KV/cache space, coordination primitives — already ship as test-witnessed kernel packages; until now they were described only in scattered design docs. This RFC pulls them into one protocol and states the invariant that makes it fak-native: every coordination act is an adjudicated tool call — fail-closed, scope- and taint-bounded, refusable with a closed reason vocabulary. Coordination in fak is not a side library with its own security surface; it rides the registries the kernel already walks for every tool call.
1. Why a protocol (the gap it closes)
Most “agent-to-agent” work is a transport: a way for two agents to find each other and
move bytes over HTTP. fak already has that story at the fleet edge
(a2a-value-opportunities.md, the out-of-kernel Agent Link
in agent-machine-link-protocol.md). What was missing
is the in-kernel substrate the transport projects onto: a way for one agent to hand a
value to another, share mutable state, and synchronize a wave — under the same
default-deny floor that gates a tool call, so a poisoned result or a private payload
cannot cross an agent boundary just because it travelled through a “coordination” call
instead of a “tool” call.
fak had every half but no whole: abi.Ref carries the Taint+ShareScope a
cross-agent message needs but never routes anywhere; the async Submit/Reap seam is
1:1 with no recipient identity; session/recall/the vDSO fan-out move drive state,
read-only memory images, or cache-invalidation broadcasts — none delivers an addressed
value from agent A to a different agent B. This protocol is the missing whole, assembled
from the existing currency (Ref provenance) and the existing registries (the
adjudicator + result-admitter chains).
The protocol has three layers. §3 and §5 ship as tested packages today; §4’s
normative contract and worked fixtures ship as versioned data, with the in-memory
runtime fold (internal/sharedtask) the named next rung — not yet wired into the
live tree (see shared-task-record-contract.md):
| Layer | What it carries | Ships today | Try it |
|---|---|---|---|
| §3 Message passing | one addressed value, now, A→B | internal/a2achan |
go run ./cmd/a2ademo |
| §4 Shared state | a named record / KV space many agents co-edit | shared-task-record-contract.md + tools/schemas/shared-*.json + examples/shared-task-record/ |
inspect the versioned envelopes + fixtures (self-validating as data; runtime fold not yet wired) |
| §5 Coordination primitives | broadcast / scatter / gather / barrier over a wave | internal/comm, internal/agenttopo |
go test ./internal/comm |
2. The adjudication invariant (the spine)
Everything below obeys one rule, and the rule is the contribution:
A coordination act is a synthetic tool call. A
Send, aRecv, aPublish, aBroadcast, aScatter, aBarrier— each folds the same registered adjudicator
- ingress admitter the kernel walks for a real
tool_call. There is no collective and no message that is exempt from refusal.
Three consequences are normative for any conforming implementation or adapter:
- Fail-closed by default. The default
abi.Refis(Tainted, ScopeAgent)— private and quarantine-eligible. Such a body is undeliverable across an agent boundary by construction. To share, the sender must explicitly widen the body’sScope(ScopeFleet/ScopeTenant), an auditable act — never an implicit side effect of “sending.” - Provenance rides the value, unchanged. A coordination op copies an
abi.Refthrough; it never re-marshals or re-labels the body. Sharing a result shares its taint — an admitted message/broadcast keeps itsTaint, so a receiver cannot re-share it past itsScope. Quarantined bytes are held out of the receiver’s context on ingress, never admitted. - Refusal is a value from a closed vocabulary. A denied coordination act returns an
abi.Verdictciting the core reason set (DEFAULT_DENYfor an un-negotiated capability;TRUST_VIOLATIONfor a scope/taint breach). No new reason is minted — the 12-reason core set is unchanged, so a coordination refusal is auditable by exactly the machinery that audits a tool-call refusal.
This is why the protocol is fak-native rather than a generic message bus: the security floor is the same object on the coordination path and the tool-call path.
3. Message passing — the message format (a2achan)
The live-message rung: deliver one addressed value from agent A to a different agent B.
3.1 Addressing and the message
A mailbox is named by a ChannelKey; a delivered unit is a Message:
type ChannelKey struct {
Locale Locale // InKernel | Session | Window
ID string // rendezvous name | peer TraceID | window continuation id
}
type Message struct {
From string // the sending principal
To ChannelKey // the destination mailbox
Body abi.Ref // the payload — its Taint + Scope ride unchanged
Seq uint64 // per-bus monotonic; fixes a deterministic delivery order
}
Two keys are equal iff both fields match: a Session channel and an InKernel
channel that happen to share an ID are distinct mailboxes — the Locale is part of the
identity. The Body is an abi.Ref (inline or CAS-backed); its (Taint, Scope) are the
share bound and are never widened by transit.
3.2 One shape, three locales
The same Send/Recv serve all three communication locales; only the key differs.
Sessions and windows are the same mailbox addressed differently — not three
mechanisms.
| Locale | ID is… |
What it bridges | Status |
|---|---|---|---|
InKernel |
a rendezvous name in one process | two concurrent goroutine-agents | shipped, race-tested |
Session |
a peer’s ToolCall.TraceID |
a cross-session handoff | code-shared; durable backing = next rung |
Window |
a continuation id minted on compaction | an explicit handoff across a context window | code-shared; compaction trigger = next rung |
3.3 Two delivery shapes
Point-to-point and pub/sub are two delivery shapes over one floor, not two security surfaces:
- Point-to-point —
Send(ctx, from, to, body, caps…) → VerdictandRecv(ctx, to, caps…) → (Message, Verdict, error)(ctx-aware blocking;TryRecvis the non-blocking dual). One message, one receiver. - Pub/sub —
Subscribe(topic) → (inbox, cancel)andPublish(ctx, from, topic, body, caps…) → (Verdict, fanout). One adjudicated message fanned out as an independent copy to every current subscriber’s private inbox. APublishfolds the same send-time gate, so publishing a private or quarantined body is refused identically.
3.4 The capability floor on messages
Send/Recv/Publish fold a registered adjudicator (a2aGate, tools a2a.send /
a2a.recv) and ingress admitter (a2aIngress). The capabilities are
CapA2ASend = "a2a.send" and CapA2ARecv = "a2a.recv", negotiated like any other. The
verdict table is normative:
| Situation | Verdict | Reason |
|---|---|---|
Send without the negotiated CapA2ASend |
Deny |
DEFAULT_DENY (no send-right) |
TaintQuarantined body |
Deny |
TRUST_VIOLATION (poison never leaves) |
ScopeAgent (private) body to another agent’s channel |
Deny |
TRUST_VIOLATION (widen Scope to share) |
ScopeFleet/ScopeTenant body, not quarantined, cap held |
Allow |
— |
Recv without CapA2ARecv |
Deny |
DEFAULT_DENY (no receive-right) |
On ingress, a TaintQuarantined delivered message |
Quarantine |
held out of the receiver’s context |
Witness: internal/a2achan/a2achan_test.go (determinism, fail-closed default,
taint/scope enforcement, async rendezvous, ingress quarantine-hold; go test -race
./internal/a2achan). Reference design: a2a-in-kernel-channel.md.
4. Shared state — the shared KV/cache space API (sharedtask)
The shared-state rung. fak keeps five related-but-different senses of “shared state” separate (the shared-state ladder) so they are not collapsed into one over-claimed feature:
| Rung | Meaning | Shipped shape |
|---|---|---|
| Live addressed message | one value delivered now | §3 a2achan Send/Recv, Publish/Subscribe |
| Live shared object | a named mutable cell during one run | compose from refs + messages today; first-class region work is planned |
| Durable handoff | state that survives a process/session/window boundary | session-image + snapshot primitives; the sharedtask journal is a task-local materialized handoff |
| Disaggregated state | bytes live outside the record | shared refs carry digest, taint, scope, store, and a deletion certificate |
| User-level collaboration | a human + agents co-edit task state | the shared-task-record contract + in-memory fold |
4.1 The shared record / KV space
A coordinated wave’s shared KV space is a shared task record: a single addressable
record (task_id, monotonic rev, a scoped body Ref, and append-only notes /
artifacts / open_decisions) that many agents and humans co-edit by patch, not by
unstructured chat. The envelopes are versioned JSON (fak.shared-task.v1,
fak.shared-patch.v1, fak.shared-patch-result.v1, fak.shared-event.v1,
fak.shared-artifact-ref.v1, fak.shared-task-journal.v1); the normative contract +
worked fixtures are in shared-task-record-contract.md.
4.2 Merge semantics (normative)
| Operation | Auto-merge? | Rule |
|---|---|---|
| append note / artifact / open decision | yes | id must be new; body is a scoped ref |
replace /title or /state |
no | requires the current base rev; a stale writer gets a typed conflict |
replace /body_ref |
no | external refs need a deletion certificate |
| replace an open-decision state | no | stale or missing decisions conflict |
Append-only edits commute on new ids; scalar edits are current-base and return a typed
conflict (base, current, proposed) when stale — so concurrent agents converge
deterministically instead of last-writer-wins clobbering. Scoped views (View,
EventsView, SubscribeScopedView) redact a snapshot and its event history by the
reader’s scope, so a tenant-scoped reader never sees a fleet-scoped body.
4.3 The KV/cache connection
“Shared KV/cache space” has two faces in fak, both honored here:
- Shared state KV — the record above: a scoped, taint-tracked, patch-merged KV that
agents read and write under the §2 invariant. Disaggregated bytes (
l3-kvstore) are admitted only with a digest and a deletion witness. - Shared cache KV — the cross-agent prefix-cache reuse fak already ships: do the shared prefill work once, later agents read it for free (the addressable, bit-exact KV cache and the radix prefix pool). That reuse is the performance dual of this protocol; this RFC governs the coordination and provenance of shared bytes, not the cache arithmetic (see the cache docs for the measured reuse).
Witness (today): the versioned envelope schemas (tools/schemas/shared-*.json) and
the worked fixtures under examples/shared-task-record/ and
examples/shared-task-record-verdicts/, each declaring its schema and self-validating
as data (per-schema counts pinned in each example’s EXAMPLE-OUTPUT.md). The executable
runtime validator (internal/sharedtask/contract.go,
go test ./internal/sharedtask -run TestContract over examples/shared-task-record) is
the intended witness once the fold is wired; it is not runnable in the live tree yet.
5. Coordination primitives — the wave collectives (comm, agenttopo)
The synchronize-a-wave rung. A comm.Group is an ordered
set of member agents: Rank is a member’s position in the sorted member set, so the
same members always get the same ranks regardless of arrival order — rank is a
deterministic function of the member identities, never of arrival order or a member’s
output.
5.1 The collectives
Each collective routes its admitting tool call through abi.Kernel.Submit (the
adjudication chokepoint) — there is no collective exempt from refusal. The I* variants
return StatusPending handles completed via Kernel.Reap; no ABI edit is needed.
| Primitive | Shape | Floor behavior |
|---|---|---|
Broadcast(payload) |
one Ref to every member |
refuses to broadcast a ScopeAgent/private Ref to a multi-member group |
Scatter(goals) |
one per-rank goal Ref |
per-rank Submit; each adjudicated |
Gather(outputs, reduce) |
fold member outputs in rank order | layout is deterministic even though member text is not |
Barrier() |
one adjudicated read-back descriptor per rank | a dos-witness-claim-shaped arrival fold, not a scheduler lock |
Split(color) / SplitLane() |
partition the group by color → lane | each color binds a dos.toml lane; overlapping lanes serialize by refusal at the arbiter |
Spawn() |
mint rank-stamped Membership for a wave |
— |
5.2 Topology: declare vs search
- Declare —
internal/agenttopodeclares a named, validated DAG over acomm.Group: who may hand a result to whom, every edge checked against the group, cycles refused, declaration order preserved. (TheMPI_Graph_createanalogue.) - Search —
cmd/topobench+turnbench.TopologyGenomeoptimize an anonymous shape, ranking topologies by measured prefix-reuse savings capped at the corpus divergence frontier — never an extrapolated number.
The full MPI-communicator analogy (the lane lease as MPI_Comm_split, ShareScope as the
communicator isolation scope) is documented in
comm-as-mpi-split.md, with the honest line that no bytes move
at the lane-lease layer — a lease coordinates who may write which files, it does not
transport a message.
Witness: internal/comm/comm_test.go, internal/agenttopo/agenttopo_test.go.
6. Conformance — issue #241 acceptance mapping
A conforming claim for D-007 is evidence-backed: each acceptance item maps to a shipped
artifact and a witness, not to prose. Three of the four rungs have a runnable Go witness
today; the shared-state rung ships its contract, versioned schemas, and self-validating
fixtures as data, with the internal/sharedtask runtime fold — and its go test witness —
the named next rung, not yet wired into the live tree.
| Acceptance item | Shipped artifact | Witness |
|---|---|---|
| Message passing between agents | internal/a2achan — Send/Recv/TryRecv, Publish/Subscribe, three locales, the capability floor |
go test -race ./internal/a2achan; go run ./cmd/a2ademo |
| Shared KV/cache space | shared-task record contract (shared-task-record-contract.md) + versioned schemas (tools/schemas/shared-*.json) + worked fixtures (examples/shared-task-record/); the internal/sharedtask runtime fold is the named next rung, not yet wired. Cross-agent prefix-cache reuse ships separately |
schemas + fixtures self-validate as data (counts pinned in each EXAMPLE-OUTPUT.md); go test ./internal/sharedtask -run TestContractSequenceFixtureValidates over examples/shared-task-record is the intended runtime witness, not yet runnable |
| Coordination primitives | internal/comm Group collectives (broadcast/scatter/gather/barrier/split/spawn) + internal/agenttopo declared topology DAG |
go test ./internal/comm ./internal/agenttopo |
| RFC/spec document | this document | renders + link-clean; binds the three pillars under the §2 invariant |
7. Honest scope, non-goals, and the roadmap
Shipped (in-process): the InKernel message locale, the message capability floor,
pub/sub fan-out, and the comm adjudicated collectives + agenttopo declared topology —
all race/contract tested. The shared-state rung (§4) ships as its normative contract,
versioned schemas, and self-validating fixtures; its in-memory sharedtask patch fold +
scoped views + materialized journal is the named next rung — not yet wired into the live
tree (see shared-task-record-contract.md).
Not claimed here:
- This is not MPI.
comm’s collectives borrow collective names and rank-order structure; they inherit no interconnect, message-rate, progress, or collective-latency property. They are explicitly notinternal/model’sDistComm(the real cross-process tensor collective whose ranks are GPU shards of one model). Acomm.Group’s ranks index detached OS processes that communicate only through git and leases. - Durable cross-process delivery is the named next rung. The
Session/Windowlocales share theInKernelcode path and work in-process today; a session-image-backed mailbox (so aSession/Windowmessage survives a process boundary) and the compaction trigger that mints aWindowcontinuation id are not shipped. - No networked task-store daemon, no external L3 transport, no browser/editor UI for the shared record — it is an in-memory reference fold plus a data-only journal.
- No new ABI surface. The protocol registers no
abiengine and makes zero ABI edits; routingSend/Recvas true kernel syscalls (wiring the registered-but-dormantabiOp table) is a named future rung, not a current claim.
Where the fleet edge fits: the out-of-kernel A2A HTTP edge
(a2a-value-opportunities.md,
agent-machine-link-protocol.md) is the projection of
this substrate — discovery, task lifecycle, multi-tenant routing — and should map its
SendMessage/GetTask onto §3/§4, not reinvent the floor. MCP stays the
model/tool/context boundary; it is not the peer-agent channel.
8. References
- Message passing:
a2a-in-kernel-channel.md·internal/a2achan/doc.go - Shared state:
shared-state-ladder.md·shared-task-record-contract.md·tools/schemas/shared-*.json·examples/shared-task-record/ - Coordination primitives:
comm-as-mpi-split.md·internal/comm/doc.go·internal/agenttopo/doc.go - The fleet edge (out-of-kernel projection):
a2a-value-opportunities.md·agent-machine-link-protocol.md - Track-D status + the sibling epic:
notes/track-d-agent-framework-parity-tracking-304.md· epic #639