Session Capabilities

Capabilities describe what a created session can receive, emit, invoke, expose, and release.

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:

StatusMeaning
startingThe harness is creating or attaching to the session.
activeThe session is actively working or generating output.
idleThe session is ready for more work.
waitingThe session is waiting on a tool, user input, network, or external result.
blockedThe session cannot continue without intervention.
pausedThe session is intentionally paused.
releasingRelease has started but is not complete.
releasedThe session boundary has been released.
offlineThe adapter cannot currently reach the session.
failedThe 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.