Agentic កម្រិតស្មុគស្មាញ: ខ្ពស់

Multi-Agent Communication in Go

Enable agents to exchange typed messages through a shared message bus so they can collaborate without direct references to each other's implementations.

The Problem

As agent systems grow, agents need to react to each other’s outputs: a planner publishes a task, an executor carries it out, a reviewer audits the result. If agents call each other directly, the graph of dependencies becomes a tangle — changing one agent’s interface breaks all its callers. Testing any single agent requires wiring up all its dependencies.

The Solution

A MessageBus decouples agents through typed Message values. Each agent subscribes under its own name and receives a dedicated buffered channel. To communicate, an agent calls bus.Publish(msg) — the bus routes the message to the addressed recipient (or broadcasts if To is empty). Agents never import each other; they only import the shared message type. In Go, channels are the natural implementation: the bus wraps a map[string]chan Message protected by a RWMutex.

Structure

Multi-Agent Communication Pattern
Step 1 of 4

Planner Publishes a Task

The PlannerAgent publishes a Message with Type=task.assigned and To=executor. The bus looks up the executor's channel and delivers the message — no direct reference to ExecutorAgent.

Implementation

package main

import "context"

// MessageType categorises a bus message so subscribers can filter.
type MessageType string

const (
	MsgTaskAssigned  MessageType = "task.assigned"
	MsgTaskCompleted MessageType = "task.completed"
	MsgReviewPassed  MessageType = "review.passed"
	MsgReviewFailed  MessageType = "review.failed"
)

// Message is the unit of communication between agents on the bus.
type Message struct {
	From    string
	To      string // empty means broadcast
	Type    MessageType
	Payload string
}

// Publisher sends messages to other agents.
type Publisher interface {
	Publish(msg Message)
}

// Agent is any actor that can send and receive messages via a bus.
type Agent interface {
	Name() string
	Run(ctx context.Context, pub Publisher, inbox <-chan Message) error
}

Real-World Analogy

An airport control tower: each aircraft (agent) communicates only with the tower (bus), never directly with other aircraft. The tower routes messages — “cleared for takeoff”, “hold position” — to the addressed party. Adding a new aircraft to the airspace means tuning to the tower frequency, not negotiating a direct radio link with every other plane.

Pros and Cons

ProsCons
Agents are fully decoupled — adding one never requires editing othersBus is a central point of failure; must be robust and non-blocking
Go channels make the implementation idiomatic and race-free with RWMutexBuffered channels drop messages silently on overflow — requires monitoring
Typed MessageType constants enable exhaustive switch handlingDebugging message flows requires tracing across multiple goroutines
Broadcast messages simplify fan-out notificationsMessage ordering is per-agent-inbox, not globally guaranteed

Best Practices

  • Use typed MessageType constants and document every message type — undocumented message formats become tribal knowledge.
  • Monitor channel buffer occupancy in production; a full inbox channel is the first sign of a stuck agent.
  • Give every agent a context.Context so it can exit cleanly when the bus closes — avoid goroutine leaks on shutdown.
  • Log every message publication with From, To, Type, and a correlation ID — multi-agent traces are impossible to debug without it.
  • Test agents in isolation by providing a mock Publisher and a pre-loaded inbox channel; never require a live bus in unit tests.

When to Use

  • Systems with three or more agents that react to each other’s outputs.
  • Event-driven workflows where the sequence of agent activations is not fully predetermined.
  • Any architecture where independent deployability of agent logic is required.

When NOT to Use

  • Two-agent systems — a direct function call or a shared channel is simpler.
  • Strictly sequential pipelines where the next step is always the same — use a pipeline instead of a bus.
  • Systems that require guaranteed message delivery — an in-process channel bus has no persistence.