All messages use a unified envelope format:
interface SkynetMessage {
id: string; // UUID
type: MessageType; // See enum below
from: string; // Agent ID
timestamp: number;
payload: unknown; // Varies by type
replyTo?: string; // Reply to a specific message
mentions?: string[]; // Agent IDs mentioned via @name
}The mentions field drives all message routing. Mentioned agents receive the message; agents without a mention do not (humans are an exception — they receive all messages regardless). The final mentions array is built from both client-provided values and server-side text scanning. See docs/workspace.md Message Routing for the full enrichment and routing rules.
enum MessageType {
// System messages
AGENT_JOIN = 'agent.join',
AGENT_LEAVE = 'agent.leave',
AGENT_HEARTBEAT = 'agent.heartbeat',
// Chat / Collaboration
CHAT = 'chat', // Free conversation
TASK_ASSIGN = 'task.assign', // Assign a task
TASK_UPDATE = 'task.update', // Task status update
TASK_RESULT = 'task.result', // Task completion result
// Context sharing
CONTEXT_SHARE = 'context.share', // Share file/project info
FILE_CHANGE = 'file.change', // File change notification
// Agent control
AGENT_INTERRUPT = 'agent.interrupt', // Interrupt agent's current task
AGENT_FORGET = 'agent.forget', // Reset agent's session
AGENT_WATCH = 'agent.watch', // Human subscribes to agent logs
AGENT_UNWATCH = 'agent.unwatch', // Human unsubscribes from agent logs
// Execution logs
EXECUTION_LOG = 'execution.log', // Agent execution log entry
}Agent identity description (similar to A2A's Agent Card):
interface AgentCard {
id: string; // UUID
name: string; // e.g. "claude-dev-1", "human-alice"
type: AgentType; // CLAUDE_CODE | GEMINI_CLI | CODEX_CLI | HUMAN | MONITOR | GENERIC
// Persistent profile fields (stored in DB)
role?: string; // e.g. "backend engineer"
persona?: string; // Free-form markdown profile (see below)
createdAt?: number;
// Runtime fields (set when connected)
capabilities?: string[]; // ["code-edit", "code-review", "test"]
projectRoot?: string;
status: AgentStatus; // 'idle' | 'busy' | 'offline' | 'error'
}The optional persona field is a free-form markdown string that defines the agent's personality and profile. It is injected into the agent's system prompt so other agents (and the server) understand who they are talking to.
Example:
# Senior Backend Engineer - "Alex"
## Personality
- Pragmatic and detail-oriented
- Prefers simple solutions over clever ones
- Communicates concisely
## Strengths
- Go, Rust, TypeScript
- Database design and optimization
- API architecture and system design
- Performance profiling
## Weaknesses
- Not great at CSS / frontend styling
- Tends to over-engineer error handling
## Work Style
- Likes to read existing code before making changes
- Always writes tests alongside implementation
- Prefers small, focused PRsThe persona is used for:
- Task routing: the scheduler can match tasks to agents based on their strengths
- Collaboration context: when agents communicate, they can understand each other's expertise
- Human readability: the monitor dashboard displays agent profiles for humans to understand the team composition
enum AgentType {
CLAUDE_CODE = 'claude-code',
GEMINI_CLI = 'gemini-cli',
CODEX_CLI = 'codex-cli',
HUMAN = 'human',
MONITOR = 'monitor',
GENERIC = 'generic',
}interface HumanProfile {
id: string;
name: string;
createdAt: number;
}type AttachmentType = 'image';
interface Attachment {
type: AttachmentType;
mimeType: string;
name: string;
/** Base64-encoded file data. */
data: string;
/** Original file size in bytes. */
size: number;
}
/** Maximum attachment size in bytes (5 MB). */
const MAX_ATTACHMENT_SIZE = 5 * 1024 * 1024;interface ChatPayload {
text: string;
attachments?: Attachment[];
}interface AgentJoinPayload {
agent: AgentCard;
}
interface AgentLeavePayload {
agentId: string;
reason?: string;
}interface AgentHeartbeatPayload {
agentId: string;
status: AgentStatus;
}type TaskStatus = 'pending' | 'assigned' | 'in-progress' | 'completed' | 'failed';
interface TaskPayload {
taskId: string;
title: string;
description: string;
assignee?: string;
status: TaskStatus;
files?: string[];
metadata?: Record<string, unknown>;
}
interface TaskResultPayload {
taskId: string;
success: boolean;
summary: string;
filesChanged?: string[];
error?: string;
}interface ContextSharePayload {
files?: Array<{ path: string; content?: string }>;
metadata?: Record<string, unknown>;
}
interface FileChangePayload {
path: string;
changeType: 'created' | 'modified' | 'deleted';
agentId: string;
}interface AgentInterruptPayload {
agentId: string;
reason?: string;
}
interface AgentForgetPayload {
agentId: string;
}
interface AgentWatchPayload {
agentId: string;
humanId: string;
}
interface AgentUnwatchPayload {
agentId: string;
humanId: string;
}type ExecutionLogLevel = 'info' | 'warn' | 'error' | 'debug';
type ExecutionLogEvent =
| 'processing.start'
| 'processing.end'
| 'processing.error'
| 'tool.call'
| 'tool.result'
| 'thinking'
| 'custom';
interface ExecutionLogPayload {
event: ExecutionLogEvent;
summary: string;
level: ExecutionLogLevel;
durationMs?: number;
sourceMessageId?: string;
metadata?: Record<string, unknown>;
}/** Special mention ID that targets all workspace members. */
const MENTION_ALL = '__all__';
/** Close code sent when a connection is replaced by another with the same agent ID. */
const WS_CLOSE_REPLACED = 4001;Clients communicate with the server using JSON envelopes over WebSocket.
enum ClientAction {
JOIN = 'join',
LEAVE = 'leave',
SEND = 'send',
HEARTBEAT = 'heartbeat',
}
interface ClientEnvelope {
action: ClientAction;
data: unknown;
}
interface JoinRequest {
agent: AgentCard;
/** Timestamp of the last message the client saw — server will only replay newer messages. */
lastSeenTimestamp?: number;
}
interface ServerEvent {
event: string;
data: unknown;
}The server sends events to clients as JSON: {event: string, data: unknown}.
| Event | When | Data |
|---|---|---|
workspace.state |
On every (re)connection | {members: AgentCard[], recentMessages: SkynetMessage[]} |
heartbeat.ack |
After receiving a heartbeat | {timestamp: number} |
error |
On invalid client action | {message: string} |
Note: The workspace.state event is sent on every connection, including reconnections. The SDK emits a workspace-state event each time, allowing agents to refresh their local state (e.g., member name maps) after network interruptions.
See workspace.md for the full WebSocket protocol details and message flow.