Skip to the content.

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, a Recv, a Publish, a Broadcast, a Scatter, a Barrier — each folds the same registered adjudicator

Three consequences are normative for any conforming implementation or adapter:

  1. Fail-closed by default. The default abi.Ref is (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’s Scope (ScopeFleet / ScopeTenant), an auditable act — never an implicit side effect of “sending.”
  2. Provenance rides the value, unchanged. A coordination op copies an abi.Ref through; it never re-marshals or re-labels the body. Sharing a result shares its taint — an admitted message/broadcast keeps its Taint, so a receiver cannot re-share it past its Scope. Quarantined bytes are held out of the receiver’s context on ingress, never admitted.
  3. Refusal is a value from a closed vocabulary. A denied coordination act returns an abi.Verdict citing the core reason set (DEFAULT_DENY for an un-negotiated capability; TRUST_VIOLATION for 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:

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:

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

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/a2achanSend/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:

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