Skip to main content

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:
agent-{agent-name}
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 ID> --to <Director ID> \
  --content "Task ID: task-123 | Question: Should the login form include 'Remember Me' checkbox?"

# Director responds
sf message send --from <Director ID> --to <Worker ID> \
  --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 ID> --channel <channel-id> \
  --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 ID> --channel <channel-id> \
  --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);

Notification Metadata

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 ID>

# Show full message content
sf inbox <Agent ID> --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 RoleMessage Types
DirectorQuestions from workers, status updates from stewards, blocker reports
WorkerTask assignments, clarification responses, nudges from stewards
StewardTask 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

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,
});
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);
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

  • Asking a question
  • Reporting an observation (security issue, bug, performance problem)
  • Providing status updates
  • Sharing FYI information
Example:
sf message send --from <Worker ID> --channel <security-channel> \
  --content "Found potential XSS vulnerability in comment rendering. Recommend sanitization."
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

Build docs developers (and LLMs) love