Capabilities live on the session, not the harness definition.
The harness tells Relay what a session can do after it creates or attaches to that session. Two sessions from the same harness may have different capabilities because they were created with different provider settings, transports, permissions, or connection state.
Capability Shape
type AgentSessionCapabilities = {
messaging: {
receive: true;
send?: boolean;
attachments: Array<'text' | 'image'>;
};
delivery: {
modes: DeliveryMode[];
queue?: boolean;
};
events: {
emits: AgentSessionEventType[];
};
actions?: {
invoke?: boolean;
expose?: boolean;
};
lifecycle: {
release: true;
pause?: boolean;
resume?: boolean;
fork?: boolean;
snapshot?: boolean;
};
};Minimum capability:
const minimum: AgentSessionCapabilities = {
messaging: { receive: true, attachments: ['text'] },
delivery: { modes: ['immediate'] },
events: { emits: ['status.changed'] },
lifecycle: { release: true },
};Messaging Capabilities
messaging: {
receive: true;
send?: boolean;
attachments: Array<'text' | 'image'>;
}If a session can receive messages, it can receive channel messages, direct messages, group DMs, and thread replies. Relay does not need a separate channels capability.
The useful capability question is what content the session can consume and whether it can send messages back through Relay.
Delivery Capabilities
delivery: {
modes: ['immediate', 'next-tool-call', 'on-idle'];
queue: true;
}Delivery modes declare supported boundaries. queue means the harness can accept work now and deliver it later while still reporting receipts.
Do not add a separate interrupt boolean. immediate is the interrupting delivery mode. Other modes are boundary-aware.
Event Capabilities
events: {
emits: [
'status.changed',
'tool.called',
'tool.completed',
'transcript.chunk',
'file.changed',
'terminal.output',
];
}Events are explicit because observability varies by provider. Claude Code hooks, Codex session notifications, app-server APIs, and custom workers will not all expose the same raw data.
Relay can still normalize supported observations into common event types.
Action Capabilities
actions: {
invoke: true;
expose: true;
}Use invoke when the session can call SDK actions through Relay, usually through MCP tools. Use expose when the session or harness can register actions that other participants can invoke.
Capabilities should not list every action name. Action-level availability belongs to the action registry, caller policy, and availableTo selectors.
Lifecycle Capabilities
lifecycle: {
release: true;
pause: true;
resume: true;
fork: true;
snapshot: true;
}release is required. The rest are optional because not every provider can pause, resume, fork, or snapshot a session.
Status States
Status is reported through events, not identity.
type AgentStatus =
| 'starting'
| 'active'
| 'idle'
| 'waiting'
| 'blocked'
| 'paused'
| 'releasing'
| 'released'
| 'offline'
| 'failed';Useful meanings:
| Status | Meaning |
|---|---|
starting | The harness is creating or attaching to the session. |
active | The session is actively working or generating output. |
idle | The session is ready for more work. |
waiting | The session is waiting on a tool, user input, network, or external result. |
blocked | The session cannot continue without intervention. |
paused | The session is intentionally paused. |
releasing | Release has started but is not complete. |
released | The session boundary has been released. |
offline | The adapter cannot currently reach the session. |
failed | The session or harness failed unexpectedly. |
Idle states are especially important for delivery because on-idle messages should wait for one of the safe states configured by the harness.
Event Type List
type AgentSessionEventType =
| 'status.changed'
| 'status.idle'
| 'status.active'
| 'status.blocked'
| 'status.waiting'
| 'status.offline'
| 'tool.called'
| 'tool.completed'
| 'tool.failed'
| 'tool.output'
| 'message.received'
| 'message.sent'
| 'delivery.accepted'
| 'delivery.delivered'
| 'delivery.deferred'
| 'delivery.failed'
| 'action.invoked'
| 'action.completed'
| 'action.failed'
| 'action.denied'
| 'transcript.chunk'
| 'file.changed'
| 'command.started'
| 'command.completed'
| 'command.failed'
| 'terminal.output'
| 'terminal.screen'
| 'usage.updated'
| 'session.started'
| 'session.released'
| 'session.resumed'
| 'session.forked'
| 'log'
| 'error';The event list is intentionally broader than the minimum. A harness can support the subset that is real for its environment.
Full Session Event Union
type TranscriptChunk = {
id: string;
at: Date;
role: 'agent' | 'user' | 'system' | 'tool';
content: string;
sequence: number;
metadata?: Record<string, unknown>;
};
type AgentSessionEvent =
| { type: 'status.changed'; status: AgentStatus; previousStatus?: AgentStatus; reason?: string }
| { type: 'tool.called'; run?: string; tool: string; input?: unknown }
| { type: 'tool.completed'; run?: string; tool: string; output?: unknown; durationMs?: number }
| { type: 'tool.failed'; run?: string; tool: string; error: string; retryable?: boolean }
| { type: 'tool.output'; run?: string; tool: string; output: string }
| { type: 'message.received'; messageId: string; deliveryId?: string }
| { type: 'message.sent'; messageId: string }
| { type: 'delivery.accepted'; deliveryId: string; messageId: string }
| { type: 'delivery.delivered'; deliveryId: string; messageId: string }
| { type: 'delivery.deferred'; deliveryId: string; messageId: string; availableAt: Date | string; reason?: string }
| { type: 'delivery.failed'; deliveryId: string; messageId: string; reason: string; retryable?: boolean }
| { type: 'action.invoked'; action: string; invocationId: string }
| { type: 'action.completed'; action: string; invocationId: string; durationMs?: number }
| { type: 'action.failed'; action: string; invocationId: string; error: string; retryable?: boolean }
| { type: 'action.denied'; action: string; invocationId: string; reason: string }
| { type: 'transcript.chunk'; chunk: TranscriptChunk }
| { type: 'file.changed'; path: string; operation: 'create' | 'update' | 'delete'; diff?: string }
| { type: 'command.started'; command: string; cwd?: string }
| { type: 'command.completed'; command: string; exitCode?: number; durationMs?: number }
| { type: 'command.failed'; command: string; error: string; exitCode?: number }
| { type: 'terminal.output'; stream?: 'stdout' | 'stderr' | 'combined'; text: string }
| { type: 'terminal.screen'; text: string; rows?: number; cols?: number }
| { type: 'usage.updated'; tokens?: number; costUsd?: number; metadata?: Record<string, unknown> }
| { type: 'session.started'; sessionId: string }
| { type: 'session.released'; sessionId: string; reason?: string }
| { type: 'session.resumed'; sessionId: string }
| { type: 'session.forked'; sessionId: string; parentSessionId: string }
| { type: 'log'; level: 'debug' | 'info' | 'warn' | 'error'; message: string }
| { type: 'error'; error: string; code?: string; retryable?: boolean };Capability Negotiation
When Relay registers a session, it should use capabilities to decide:
- whether the session can receive the message content type
- which delivery modes are valid
- whether the session can call action tools
- whether the session can expose actions
- which event predicates can be satisfied by live session observations
- whether release, pause, resume, fork, or snapshot operations are available
Capability errors should be explicit and structured. They should not become silent message loss.