Overview
Stoneforge agents communicate through channels - shared spaces for sending and receiving messages. Each agent has a dedicated channel for receiving task assignments and notifications, plus access to shared channels for team communication.
Channel Types
Agent Channels Dedicated 1:1 channel for each agent. Used for task dispatch, notifications, and direct messages.
Team Channels Shared channels for team-wide communication. Examples: #security, #blockers, #general.
Agent Channels
Every agent gets a dedicated channel on registration, automatically created with the naming pattern:
Examples:
agent-e-worker-1 - Ephemeral worker 1’s channel
agent-director - Director’s channel
agent-m-steward-1 - Merge steward 1’s channel
Automatic Channel Creation
import { createOrchestratorAPI } from '@stoneforge/smithy' ;
const api = createOrchestratorAPI ( storage );
// Register a worker
const worker = await api . registerWorker ({
name: 'e-worker-1' ,
workerMode: 'ephemeral' ,
createdBy: directorId ,
});
// Channel is automatically created
const channelId = await api . getAgentChannel ( worker . id );
console . log ( 'Agent channel:' , channelId );
// Output: 'el-channel-abc123' (references 'agent-e-worker-1')
Retrieving Agent Channels
import { createAgentRegistry } from '@stoneforge/smithy' ;
const registry = createAgentRegistry ( api );
// Get full channel object
const channel = await registry . getAgentChannel ( agentId );
if ( channel ) {
console . log ( 'Channel name:' , channel . name ); // 'agent-e-worker-1'
console . log ( 'Channel ID:' , channel . id ); // 'el-channel-abc123'
console . log ( 'Members:' , channel . memberIds ); // [agentId, directorId, ...]
}
// Get just the channel ID
const channelId = await registry . getAgentChannelId ( agentId );
Sending Messages
Direct Messages (Agent-to-Agent)
Agents send direct messages using the QuarryAPI:
import { createDocument , ContentType } from '@stoneforge/core' ;
// 1. Create a document for the message content
const contentDoc = await createDocument ({
contentType: ContentType . TEXT ,
content: 'Task task-123: Question about authentication flow. Should we use OAuth 2.0 or JWT tokens?' ,
createdBy: workerEntityId ,
tags: [ 'question' , 'task-123' ],
});
const savedDoc = await api . create ( contentDoc );
// 2. Send message to recipient
await api . sendDirectMessage ( workerEntityId , {
recipient: directorEntityId ,
contentRef: savedDoc . id ,
});
Messages require a Document for content (via contentRef). You cannot send raw text strings directly.
CLI Direct Messaging
Workers and stewards use the CLI for direct messaging:
# Worker asks Director for clarification
sf message send --from < Worker I D > --to < Director I D > \
--content "Task ID: task-123 | Question: Should the login form include 'Remember Me' checkbox?"
# Director responds
sf message send --from < Director I D > --to < Worker I D > \
--content "Re: task-123 - Yes, add 'Remember Me' with 30-day session expiry."
Always include the Task ID in clarification messages so the Director knows which task you’re asking about.
Channel Messages (Team Communication)
Send messages to shared channels for team-wide visibility:
# Check existing channels first
sf channel list
# Send to existing channel
sf message send --from < Worker I D > --channel < channel-i d > \
--content "Found security vulnerability in auth endpoint. See task-456 for details."
# Create channel if needed (include description)
sf channel create --name "security-issues" \
--description "Security vulnerabilities and fixes"
# Send to new channel
sf message send --from < Worker I D > --channel < channel-i d > \
--content "Reporting SQL injection risk in search query builder."
Prefer using existing channels over creating new ones. Always check sf channel list first.
Task Dispatch Messages
When the dispatch daemon assigns a task to an agent, it sends a notification message to the agent’s channel:
import { createDispatchService } from '@stoneforge/smithy' ;
const dispatchService = createDispatchService ( api , taskAssignment , agentRegistry );
const result = await dispatchService . dispatch ( taskId , workerId , {
priority: 3 ,
restart: false ,
markAsStarted: true ,
notificationMessage: 'New task assigned: Implement OAuth login' ,
notificationMetadata: {
taskId ,
branch: 'agent/e-worker-1/task-123-oauth' ,
worktree: '.stoneforge/.worktrees/e-worker-1-oauth' ,
},
});
console . log ( 'Notification sent:' , result . notification . id );
console . log ( 'Channel:' , result . channel . name );
Dispatch notifications include structured metadata:
interface DispatchNotificationMetadata {
type : 'task-assignment' | 'task-reassignment' | 'restart-signal' ;
taskId ?: ElementId ;
priority ?: number ;
restart ?: boolean ;
branch ?: string ;
worktree ?: string ;
sessionId ?: string ;
dispatchedAt : Timestamp ;
dispatchedBy ?: EntityId ;
suppressInbox ?: boolean ; // Prevents inbox items for channel members
}
The suppressInbox: true flag prevents dispatch notifications from cluttering the operator/director’s inbox. Only the assigned agent receives the notification.
Inbox System
Agents check their inbox to receive messages:
Checking Inbox (CLI)
# List inbox messages
sf inbox < Agent I D >
# Show full message content
sf inbox < Agent I D > --full
# View specific inbox item
sf show inbox-abc123
Inbox Service (Programmatic)
import { createInboxService } from '@stoneforge/quarry' ;
const inboxService = createInboxService ( storage );
// Get all inbox messages for an agent
const messages = inboxService . getInbox ( agentEntityId );
for ( const msg of messages ) {
console . log ( 'From:' , msg . sender );
console . log ( 'Content ref:' , msg . contentRef );
console . log ( 'Received:' , msg . createdAt );
// Fetch message content
const content = await api . get ( msg . contentRef );
console . log ( 'Message:' , content . content );
}
Polling for New Messages
The dispatch daemon handles inbox polling automatically. Agents don’t need to poll manually - they’re spawned when new tasks are assigned.
For persistent workers that poll manually:
// Poll inbox every 30 seconds
setInterval ( async () => {
const messages = inboxService . getInbox ( agentEntityId );
for ( const msg of messages ) {
// Process new messages
await handleMessage ( msg );
}
}, 30_000 );
Message Routing
The dispatch daemon routes incoming messages by agent role:
┌─────────────────────────────────────────────┐
│ Dispatch Daemon │
└──────────────┬──────────────────────────────┘
│
┌─────────┴─────────┬──────────────┐
▼ ▼ ▼
Director Channel Worker Channel Steward Channel
Message Types by Role
Recipient Role Message Types Director Questions from workers, status updates from stewards, blocker reports Worker Task assignments, clarification responses, nudges from stewards Steward Task review assignments, system alerts, recovery triggers
Communication Patterns
Worker → Director (Clarification)
# Worker encounters unclear requirement
sf message send --from el-entity-worker1 --to el-entity-director \
--content "Task ID: task-789 | Question: Acceptance criterion says 'validate email format' - should we use regex or external validation service?"
# Worker waits for response before continuing
# Session ends to avoid wasting context
After sending a clarification message, workers should end their session with sf task handoff to avoid wasting context while waiting for a response. They’ll be re-spawned when the Director responds.
Worker → Team Channel (Observation)
# Worker discovers security issue
sf message send --from el-entity-worker1 --channel el-channel-security \
--content "While implementing task-456, found hardcoded API key in src/config.ts line 23. Recommend immediate rotation."
# Worker continues current task (doesn't block on observation)
Steward → Director (Issue Report)
# Steward finds pre-existing bug during review
sf message send --from el-entity-steward1 --to el-entity-director \
--content "Found pre-existing issue during review of task-123: Test 'auth.login' fails on main (not caused by PR). Please create a task to fix this."
Director → Worker (Task Assignment)
Handled automatically by dispatch daemon:
// Daemon dispatches task
await dispatchService . dispatch ( taskId , workerId , {
priority: 2 ,
notificationMessage: 'Task assigned: Fix authentication bug' ,
});
// Worker receives notification in their channel
// Worker session spawns automatically
Channel Management
Creating Channels
# Always check existing channels first
sf channel list
# Create with description (required)
sf channel create --name "frontend-discussion" \
--description "Frontend architecture and component design discussions"
Listing Channels
# List all channels
sf channel list
# Output:
ID Name Members Created
el-channel-abc agent-director 2 2026-03-01
el-channel-def agent-e-worker-1 1 2026-03-01
el-channel-ghi security-issues 5 2026-03-02
el-channel-jkl frontend-discussion 3 2026-03-02
Adding Members to Channels
import { createChannel } from '@stoneforge/core' ;
// Create a team channel
const channel = await createChannel ({
name: 'blockers' ,
memberIds: [ directorId , worker1Id , worker2Id ],
createdBy: directorId ,
tags: [ 'team' , 'blockers' ],
});
const savedChannel = await api . create ( channel );
// Add more members later
await api . update ( savedChannel . id , {
memberIds: [ ... savedChannel . memberIds , worker3Id ],
});
Best Practices
Always Include Task ID When asking about a task, include the task ID in the message so the Director knows the context.
Use Existing Channels Check sf channel list before creating new channels. Reuse existing channels for similar topics.
End Session After Asking After sending a clarification question, end your session with sf task handoff to avoid wasting context while waiting.
Descriptive Channel Names Use kebab-case and descriptive names: security-issues, frontend-discussion, not misc or stuff.
Programmatic Channel Operations
Generate Agent Channel Name
import { generateAgentChannelName , parseAgentChannelName } from '@stoneforge/smithy' ;
// Generate channel name for an agent
const channelName = generateAgentChannelName ( 'e-worker-1' );
console . log ( channelName ); // 'agent-e-worker-1'
// Parse agent name from channel name
const agentName = parseAgentChannelName ( 'agent-e-worker-1' );
console . log ( agentName ); // 'e-worker-1'
const notAgentChannel = parseAgentChannelName ( 'security-issues' );
console . log ( notAgentChannel ); // null (not an agent channel)
Send Notification Without Task Assignment
// Send standalone notification (e.g., restart signal)
await dispatchService . notifyAgent (
agentId ,
'restart-signal' ,
'System restart requested. Please terminate and restart your session.' ,
{
reason: 'config-update' ,
urgency: 'high' ,
}
);
Batch Dispatch
Dispatch multiple tasks to the same agent efficiently:
const results = await dispatchService . dispatchBatch (
[ task1Id , task2Id , task3Id ],
workerId ,
{ priority: 2 }
);
console . log ( `Dispatched ${ results . length } tasks` );
for ( const result of results ) {
console . log ( `Task ${ result . task . id } -> ${ result . agent . name } ` );
}
Troubleshooting
Message requires contentRef error
Error : sendDirectMessage() requires a contentRefCause : Trying to send raw text instead of a Document referenceSolution : Create a Document first:const doc = await createDocument ({
contentType: ContentType . TEXT ,
content: 'Your message here' ,
createdBy: senderId ,
});
const savedDoc = await api . create ( doc );
await api . sendDirectMessage ( senderId , {
recipient: recipientId ,
contentRef: savedDoc . id ,
});
Channel not found for agent
Error : Agent channel not found for agent: el-entity-xxxCause : Agent was not properly registered or channel creation failedSolution : Re-register the agent:// Delete agent if it exists
await api . delete ( agentId );
// Re-register with channel auto-creation
const agent = await api . registerWorker ({ ... });
const channelId = await api . getAgentChannel ( agent . id );
Inbox messages not appearing
Symptom : Messages sent but sf inbox shows nothingCause : Messages with suppressInbox: true don’t create inbox itemsSolution : Check channel messages directly:const channel = await registry . getAgentChannel ( agentId );
const messages = await api . getChannelMessages ( channel . id );
Message vs Task Creation
Send a Message When...
Create a Task When...
Asking a question
Reporting an observation (security issue, bug, performance problem)
Providing status updates
Sharing FYI information
Example :sf message send --from < Worker I D > --channel < security-channe l > \
--content "Found potential XSS vulnerability in comment rendering. Recommend sanitization."
Work needs to be tracked and assigned
Multiple steps required to complete
Acceptance criteria need to be defined
Work should block/depend on other tasks
Example :sf task create --title "Fix XSS vulnerability in comment rendering" \
--priority 1 --type bug --tags security
Messages are for communication. Tasks are for work. When in doubt, create a task so the work is tracked.
Custom Agents Create agents with specialized communication behaviors
Playbooks Define communication workflows in playbooks
Agent Roles Understand Director, Worker, and Steward communication patterns