Agent Communication
Blackboard shared state, MessageBus direct messaging, and EventEmitter coordination for inter-agent communication.
Overview
Agents in a swarm communicate through three complementary primitives:
| Primitive | Purpose | Pattern |
|---|---|---|
| Blackboard | Shared key-value state that any agent can read/write | Publish-subscribe on sections |
| MessageBus | Direct agent-to-agent or broadcast messaging | Point-to-point or broadcast |
| EventEmitter | Coordination signals emitted by the swarm and strategies | Observer pattern |
Every Swarm instance exposes all three via swarm.blackboard, swarm.messageBus, and swarm.events. The SwarmCoordinator creates them from your configuration — in-memory by default, Redis-backed in distributed mode. Strategies use the blackboard for their own state (sections such as consensus, auction, pipeline, debate), so you can watch a run from outside.
Blackboard
The blackboard is a shared state store organized into named sections. Agents read and write to sections, enabling asynchronous collaboration without direct messaging.
Configuration
import { swarm } from '@cogitator-ai/swarms';
const team = swarm('research-team')
.strategy('hierarchical')
.supervisor(leadAgent)
.workers([searcherAgent, analyzerAgent])
.blackboardConfig({
enabled: true,
sections: {
sources: [],
facts: [],
conclusions: [],
},
trackHistory: true,
})
.build(cogitator);API
const bb = team.blackboard;
bb.write('sources', ['https://example.com/paper.pdf'], 'searcher');
const sources = bb.read<string[]>('sources'); // throws if the section does not exist
bb.append('facts', { claim: 'X is true', source: 'paper.pdf' }, 'analyzer');
bb.has('conclusions');
bb.getSections();
const section = bb.getSection<string[]>('sources');
// { name, data, lastModified, modifiedBy, version }
bb.delete('conclusions');History Tracking
When trackHistory: true, every write is recorded with the author, timestamp, and version number:
const history = bb.getHistory('facts');
// [
// { value: [...], writtenBy: 'analyzer', timestamp: 1700000000, version: 1 },
// { value: [...], writtenBy: 'analyzer', timestamp: 1700000005, version: 2 },
// ]Subscriptions
React to changes in real-time:
const unsubscribe = bb.subscribe('conclusions', (data, agentName) => {
console.log(`${agentName} updated conclusions:`, data);
});
// later
unsubscribe();MessageBus
The message bus enables direct communication between agents. Messages are typed, routable, and support both point-to-point and broadcast patterns.
Configuration
const team = swarm('collab-team')
.strategy('consensus')
.agents([agentA, agentB, agentC])
.messaging({
enabled: true,
maxMessageLength: 2000,
maxMessagesPerTurn: 5,
maxTotalMessages: 100,
})
.build(cogitator);Sending Messages
const bus = team.messageBus;
await bus.send({
swarmId: team.id,
from: 'agent-a',
to: 'agent-b',
type: 'request',
content: 'Can you review my analysis?',
channel: 'reviews',
});
await bus.broadcast('agent-a', 'I found something important', 'announcements');send resolves to the stored message (with id and timestamp) and throws when messaging is disabled or a limit below is hit.
Querying Messages
const myMessages = bus.getMessages('agent-b', 20); // last 20 sent by, to or broadcast for agent-b
const unread = bus.getUnreadMessages('agent-b');
const thread = bus.getConversation('agent-a', 'agent-b');
const all = bus.getAllMessages();Read tracking is per agent. Inside a swarm, unread messages are injected into the recipient's next turn exactly once (emitting message:received), and the read_messages tool marks the messages it returns as read. swarm.messageBus is typed as ReadTrackingMessageBus, so it also has markAsRead(agentName, ids) and onMessage(listener) (every message sent on the bus; returns an unsubscribe function).
Subscribing to Incoming Messages
const unsubscribe = bus.subscribe('agent-b', (message) => {
console.log(`${message.from}: ${message.content}`);
});Rate Limiting
The InMemoryMessageBus enforces the limits you set in the configuration:
maxMessageLength— rejects messages that exceed the character limitmaxMessagesPerTurn— per-agent message cap per turn; the coordinator resets it at the start of every agent turn (bus.resetTurnCounts(agentName))maxTotalMessages— hard cap on total messages to prevent infinite loops
EventEmitter
The swarm event system tracks all coordination activity. Strategies emit events at key points, and you can subscribe from outside the swarm for logging, metrics, or UI updates.
Subscribing
team.on('agent:start', (event) => {
console.log(`[${event.type}] ${event.agentName} at ${event.timestamp}`);
});
team.on('swarm:complete', (event) => {
console.log('Swarm finished:', event.data);
});
// wildcard catches everything
team.on('*', (event) => {
metrics.record(event.type, event.data);
});
// one-time listener
team.once('consensus:reached', (event) => {
const { decision } = event.data as { decision: string };
console.log(`Decision: ${decision}`);
});Built-in Event Types
Events emitted by the swarm and its coordinator (event.data holds the payload):
| Event | Data |
|---|---|
swarm:start | { swarmId, strategy, input } |
swarm:complete | { swarmId, outputLength, agentCount } |
swarm:error | { swarmId, error } |
swarm:paused | { swarmId } |
swarm:resumed | { swarmId } |
swarm:aborted | { swarmId } |
swarm:reset | { swarmId } |
agent:start | { agentName, input } |
agent:complete | { agentName, result } |
agent:error | { agentName, error } |
message:sent | { id, from, to, type, channel, content } |
message:received | { agentName, count, messageIds } |
blackboard:write | { section, version, writtenBy, deleted } |
Strategy-specific events:
| Event | Strategy |
|---|---|
round-robin:assigned | Round-robin |
consensus:round, consensus:turn, consensus:vote, consensus:reached | Consensus |
auction:start, auction:bid, auction:winner, auction:complete | Auction |
pipeline:stage, pipeline:stage:complete, pipeline:gate:pass, pipeline:gate:fail | Pipeline |
debate:round, debate:turn | Debate |
negotiation:start, negotiation:round, negotiation:turn, negotiation:phase-change, negotiation:offer-made, negotiation:offer-accepted, negotiation:offer-rejected, negotiation:offer-countered, negotiation:convergence-update, negotiation:stagnation-detected, negotiation:mediation-suggested, negotiation:approval-required, negotiation:approval-received, negotiation:agreement-reached, negotiation:deadlock, negotiation:escalation, negotiation:arbitration, negotiation:terminated (and coalition events) | Negotiation |
assessor:complete | Assessor |
Querying Event History
The event emitter keeps a rolling buffer of recent events (default 1000):
const allEvents = team.events.getEvents();
const agentEvents = team.events.getEventsByAgent('frontend-dev');
const roundEvents = team.events.getEventsByType('consensus:round');
team.events.clearEvents();swarm.events is typed as QueryableSwarmEventEmitter, which adds getEventsByType, getEventsByAgent and clearEvents to SwarmEventEmitter. SwarmRunOptions.onEvent receives every event of one run.
Swarm Tools
Strategies attach the tools they depend on: the hierarchical supervisor gets the delegation tools, consensus voters the voting tools and negotiating agents the negotiation tools. The messaging and blackboard tools are opt-in with agentTools (or SwarmBuilder.agentTools()), which gives every agent both sets bound to the swarm's own bus and blackboard:
const team = swarm('collab-team')
.strategy('round-robin')
.agents([agentA, agentB])
.agentTools({ messaging: true, blackboard: true })
.build(cogitator);Tools an agent already defines with the same name are left untouched. All of this applies to in-process swarms only: distributed swarms attach no tools, and agentTools is rejected there (and when messaging.enabled or blackboard.enabled is false).
The factories build the same tools for a given blackboard, bus and event emitter:
| Factory | Tools |
|---|---|
createMessagingTools | send_message (optional waitForReply), read_messages (marks returned messages read), broadcast_message, reply_to_message |
createBlackboardTools | read_blackboard, write_blackboard (merge extends arrays / merges objects), append_blackboard, list_blackboard_sections, get_blackboard_history |
createDelegationTools | delegate_task, check_progress, request_revision, list_workers |
createVotingTools | cast_vote, get_votes, change_vote, get_consensus_status |
createNegotiationTools | make_offer, counter_offer, accept_offer, reject_offer, get_negotiation_status, get_current_offers, propose_coalition, join_coalition, declare_interests |
import { createBlackboardTools, createVotingTools } from '@cogitator-ai/swarms';
const { readBlackboard, writeBlackboard } = createBlackboardTools(team.blackboard, 'analyst');
const voting = createVotingTools(team.blackboard, team.events, 'analyst', 2);Each factory returns an object of tools bound to one agent name. Because the blackboard, bus and events belong to a built swarm while an Agent's tools are fixed when it is constructed, these factories are mainly for custom strategies and coordinators: createSwarmTools(context) returns every tool and createStrategyTools(strategy, context) the set a strategy needs, from a SwarmToolContext (coordinator, blackboard, messageBus, events, agentName, optional agentWeight and swarmId). Messaging tools created that way enforce the hierarchical communication policy (createHierarchyMessageAuthorizer).
Redis-Backed Variants
For distributed swarms, all three communication primitives have Redis-backed implementations that synchronize state across machines:
RedisMessageBus— messages persisted in Redis lists, delivered via pub/subRedisBlackboard— sections stored in Redis keys, changes broadcast via pub/subRedisSwarmEventEmitter— events stored in Redis lists, live-streamed via pub/sub
These are used automatically when you enable distributed mode. See Distributed Swarms for details.