Agent Communication — Agent IPC Primitives and the Syscall Tool Layer
Note: This article is grounded in the actual code (
internal/agentipc—bus.go/primitives.go/policy.go/deadletter.go/collaboration_observer.go— andinternal/agentsyscall—syscall.go/plan.go), the dedicated Agent-communication article in the docs series. Agents are peer cognitive processes (A ≡ B ≡ C); communication runs over a peer-mesh message bus, no Leader relay required.
1. Why Agent Communication Matters
In a multi-agent system agents must exchange messages — "I found X" / "help me verify Y" / "your conclusion conflicts with mine" (the archetypal collaboration phrases listed in internal/agentipc/doc.go). This is ARES's Kernel IPC pillar (P4), and the third of the three context layers (Task Shared / Agent Private / IPC Messages) — the first two live in internal/fabric/agent; this layer is carried by this package.
Let's first separate duties: task dispatch is the Scheduler's (Task Fabric) job; agent communication is IPC's job. They are not the same thing:
| Dimension | Task Dispatch (taskfabric / Scheduler) | Agent Communication (agentipc) |
|---|---|---|
| View | Who executes what task | Who needs to know what message |
| Path | Created via Task Fabric → scheduled → executed | Direct peer-mesh delivery |
| Semantics | Task execution, recoverable, replayable | Collaboration, request/reply, broadcast/subscribe, handoff |
| State | Durable Task | In-memory registry + immediate delivery |
Core principle: agents express intent (Send / Request / Delegate / Handoff / Subscribe); the Kernel guarantees delivery. Communication is a "collaboration channel", not a "task execution channel".
2. The Native Message Unit: Message
Every IPC message uses Message from internal/agentipc/bus.go (design Section 13: Context layer 3 — IPC Messages):
type Message struct {
ID string // unique message ID generated by the bus
From string // sender agent ID
To string // target agent ID ("" = broadcast to subscribers)
Topic string // message subject (e.g. "verify-conclusion" / "handoff-task")
CorrelationID string // correlation id pairing a reply with its request
Payload any // message body
At time.Time // send timestamp
}
The paired delivery function and sentinel errors (Handler, ErrAgentNotRegistered, ErrNoHandler, ErrTimeout, ErrInvalidMessage) are defined in the same package.
3. The Six Primitives: Bus
The core object is Bus (internal/agentipc/bus.go): a peer message bus mapping agent IDs to handlers. It maintains handlers, subscribers (topic → subscriber list), and pending / pendingErr (per-correlation reply channels / stashed errors). The full primitive set:
| Primitive | Semantics | Notes |
|---|---|---|
Send | fire-and-forget direct send | Invokes the target handler synchronously; waits for no reply |
Request | synchronous request/reply | Allocates a correlation ID, waits for a reply, with a timeout |
Reply | asynchronous response | The target handler answers later by correlation ID |
Delegate | forward on behalf of | Forwards to the final target, preserving the end-to-end correlation ID |
Handoff | task hand-off | Structured task payload + receiver acknowledgement; does NOT go through the Scheduler |
Subscribe / Unsubscribe | subscribe / unsubscribe | Topic subscription, paired with Broadcast fan-out |
Note:
doc.goframes the set as Send / Request / Delegate / Handoff / Subscribe (+ Reply); the implementation addsBroadcast/Unsubscribeas supplementary entry points, listed all together here.
graph TD
A[Initiator Agent] -->|Send / Broadcast| B[Bus]
A -->|Request / Delegate / Handoff| B
B -->|Send: sync handler call, no wait| C1[Target handler]
B -->|Request: allocate corrID + reply channel| C2[Target handler]
B -->|Broadcast: fan out by topic| C3[Subscriber handlers]
C2 -->|Reply same corrID| A
C1 -->|result vs failure| D[DeadLetterStore / CollaborationObserver]
C3 -->|delivery count| ASend vs Request: Two Collaboration Stances
Send(ctx, from, to, topic, payload) error— fire-and-forget. The target handler is invoked synchronously in the caller's goroutine; a handler error is surfaced but no reply channel is set up — "fire and done", no paired reply.Request(ctx, from, to, topic, payload, timeout) (*Message, error)— synchronous request/reply. The bus allocates a correlation ID and registers a pending reply channel; the target handler must callReplywith the same correlation ID to complete the request. Timeout has a sane default: atimeout <= 0falls back todefaultRequestTimeout = 30 * time.Second(primitives.go, B16 — no indefinite blocking). The request runs in a managed goroutine with a recover boundary — if the handler panics, it is contained as oneErrHandlerPanic, failing exactly that single request rather than the whole process.
4. Delegate and Handoff
Delegate(ctx, delegator, to, topic, payload, timeout)— forwards a request to another agent on the caller's behalf. The original requester's correlation ID is preserved end-to-end so the reply chains back. Semantics: "I can't handle this — let me ask someone who can".Handoff(ctx, from, to, taskID, contextSnapshot, timeout)— transfers a task's ownership from a yielding agent to an accepting agent. Payload is{task_id, context, artifacts}on the constant topichandoff-task; the receiver returns an acceptance reply. Key point: this is a peer-to-peer task-transfer primitive — it does NOT go through the Scheduler.
5. Publish/Subscribe: Subscribe / Broadcast / Unsubscribe
func (b *Bus) Subscribe(agentID, topic string) error // idempotent: duplicates de-duplicated
func (b *Bus) Unsubscribe(agentID, topic string)
func (b *Bus) Broadcast(ctx context.Context, from, topic string, payload any) int
Broadcast fans one message out to every subscriber's handler for a topic; a handler error does not stop the rest, and the successful-delivery count is returned. Subscribe de-duplicates the same agent on the same topic (B16).
6. Observability: DeadLetterStore and CollaborationObserver
IPC is not just communication — it is one input to the evolution loop (N-11 / GAP-3 closure):
- DeadLetterStore (
deadletter.go): a bounded FIFO recording failed deliveries (ErrAgentNotRegistered/ErrTimeout/ handler errors). Default capacity 1024 (capacity <= 0falls back).Record(oldest evicted),Snapshot,Countserve diagnosis and redelivery. Both Send and Request record failures. - CollaborationObserver (
collaboration_observer.go): each collaboration attempt with an observable receipt emits onefeedback.CollaborationOutcome.SendandRequest(and the Delegate / Handoff built on them) are observed;Broadcastis deliberately NOT observed (no single target to attribute the outcome to). The observer is invoked on the initiator's path, so it MUST NOT block — and it is called outside the bus lock.
graph LR
S[Initiator] -->|Send / Request failure| DL[DeadLetterStore bounded FIFO]
S -->|Send / Request receipt| OBS[CollaborationObserver]
OBS --> FB[feedback source: collaboration]
DL -->|Snapshot / redelivery| OPS[diagnosis / ops tooling]7. The Syscall Layer: Kernel and Tools
internal/agentsyscall wraps IPC/collaboration capability into LLM-callable tools (W2 design: the real LLM executor decides whether to split work; the Kernel validates and enforces — "Agent decides. Kernel enforces."). BindTools(binder, kernel) registers four tools:
| Tool constant | Kernel syscall | Meaning |
|---|---|---|
SpawnAgentTool (spawn_agent) | Kernel.SpawnAgent | Spawn a peer Agent with a declared capability; validate quota and register it as a schedulable executor |
CreateTaskTool (create_task) | Kernel.CreateTask | Create a sub-task in the Task Fabric (→ READY), leaves it to the Scheduler |
AskAgentTool (ask_agent) | Kernel.AskAgent | Ask a target Agent a question on a topic (use when you know WHICH agent to ask) |
CreatePlanTool (create_plan) | Kernel.CreatePlan | Compile the whole dependency DAG into one batch of tasks (optional bounded round-loop) |
ToolSchemas() returns the LLM-facing schemas for these tools and injects them into resources.Registry, alongside the built-in tools in the Chat API.
The Kernel's AskAgentFn is the key injection point (syscall.go):
type AskAgentFn func(ctx context.Context, from, to, topic string, payload any) error
// WithAskAgent injects the collaboration primitive; SetAskAgent can replace it during
// serve assembly (after setupPeerRegistry). Production wire-up:
// aresrecovery.EvolutionAwareIPC.Send → agentipc Bus.
ask_agent is the ACT half of the collaboration channel (Step Y.2-ACT): it turns "which agent to ask" into an agent-visible, changeable decision. Kernel.AskAgent requires a non-empty target and a wired primitive (fail-loud if absent — never a silent no-op). Note its semantics are a fire-and-forget send: AskAgentResult.Accepted means "the request was handed to the collaboration primitive", not "an answer is in hand".
graph LR
LLM[LLM Agent] -->|ask_agent tool call| Bind[BindTools binding]
Bind --> K[Kernel.AskAgent]
K -->|AskAgentFn: ipc.Send| BUS[agentipc Bus.Send]
BUS --> H[Target agent handler]
H -->|receipt| OBS[CollaborationObserver]
OBS --> FB[collaboration feedback source]8. Single-Track Dispatch: the dual-track machinery is retired
ExecutionPolicy / PolicyFlag / shadow mode / Mismatches() and the whole facade in policy.go (Dispatcher / KernelDispatcher / NewPath / SetNewPath) are gone — internal/agentipc no longer carries any "how a task is dispatched" abstraction. HTTP submits tasks to the Task Fabric directly (submitPeerTask / POST /api/graphs) and the kernelScheduler is the sole executor (Schedule→Acquire→RunQuantum). The kernel-bridge dispatch layer that only tests ever invoked (wireKernelDispatcher / enableKernelExecution / submitFabricTask) was removed along with it.
9. Summary
| Primitive / Component | Package | Responsibility |
|---|---|---|
Message + Handler | internal/agentipc | IPC message unit and delivery callback |
Bus (Send/Request/Reply/Delegate/Handoff/Subscribe/Broadcast/Unsubscribe) | internal/agentipc | Peer message bus and the full collaboration primitive set |
DeadLetterStore | internal/agentipc | Bounded FIFO record of failed deliveries (default 1024) |
CollaborationObserver | internal/agentipc | Collaboration receipts → feedback source |
Kernel (SpawnAgent/CreateTask/AskAgent/CreatePlan) | internal/agentsyscall | LLM-callable syscall tool kernel |
Design line: agents express intent, the Kernel guarantees delivery, and the evolution loop stays observable. The communication primitives are the peer collaboration layer; task dispatch is the scheduling execution layer — orthogonal. And ask_agent turns a "collaboration intent" into a real, injectable, observable, evolvable tool, wiring the IPC from a library primitive into the agent's cognition loop.