Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307/** * Message catalog — the single union of every protocol message (PROTOCOL §5 cross-ref). * * Ties together the command catalog (§9.2/§9.5/§10.1), response (§9.3), lifecycle events * (§9.1), the streaming/turn/permission/UI events from `contracts/event.ts` (§9.4/§9.5), * and the orchestrator events (§10.2). `Message` is the closed set a transport can carry. * * Also provides: * - runtime dispatch helpers (isCommand/isEvent/isResponse/isOrchestratorMessage) — the * full envelope-level discrimination that `envelope.ts` deliberately omits. * - a `MESSAGE_TYPE_RULES` table encoding the §5 correlation mode + agent_id requirement * per message type, so transports can validate without a separate hand-maintained map. */import { Type, type Static } from '@sinclair/typebox';import { Value } from '@sinclair/typebox/value';import { EventSchema } from '@/contracts/event';import { CommandSchema } from '@/protocol/commands';import { ResponseSchema } from '@/protocol/response';import { LifecycleEventSchema } from '@/protocol/lifecycle';import { OrchestratorEventSchema } from '@/protocol/orchestrator-events';import { SubagentRunChangedSchema } from '@/protocol/subagent-commands';import type { Envelope } from '@/protocol/envelope';import type { CorrelationMode } from '@/protocol/envelope';
/** * The full v0.1 message catalog. Order is conventional (commands, response, lifecycle, * streaming/permission events, orchestrator events) and does not affect matching — typebox * `Type.Union` validates against any member. */export const WireMessageSchema = Type.Union([ CommandSchema, ResponseSchema, LifecycleEventSchema, EventSchema, OrchestratorEventSchema, SubagentRunChangedSchema,]);
/** The full v0.1 wire message (any command, response, or event). */export type WireMessage = Static<typeof WireMessageSchema>;
// ─── Dispatch helpers (envelope-level) ───────────────────────────────────────────
/** All command `type` values (client→server). */const COMMAND_TYPES = new Set<string>([ 'prompt', 'steer', 'abort', 'set_mode', 'list_modes', 'list_models', 'set_model', 'list_reasoning_levels', 'set_reasoning_level', 'session_op', 'permission_response', 'ui_response', 'agent.list', 'agent.create', 'agent.attach', 'agent.detach', 'agent.terminate', 'agent.kill', 'agent.status', 'daemon.shutdown', 'web.approve', 'web.token.create', 'web.clients', 'web.revoke', 'web.token.rotate', 'session.create', 'session.status', 'session.terminate', 'session.kill', 'session.activate', 'session.list', 'session.replay', // Tool-usage stats aggregate (PROTOCOL §10 stats surface). 'stats.tool_usage', // Token-usage stats aggregate (PROTOCOL §10 stats surface; sibling to tool-usage). 'stats.token_usage', 'workspace.create', 'workspace.attach', 'workspace.list', 'workspace.status', 'workspace.retire', // Subagent job orchestrator commands (saga u4uo9m.1). 'subagent.create', 'subagent.wait', 'subagent.steer', 'subagent.relaunch', 'subagent.status', 'subagent.stop',]);
/** All response / lifecycle / streaming / orchestrator event `type` values (server→client). */const EVENT_TYPES = new Set<string>([ // response 'response', // lifecycle (§9.1) 'hello', 'sync', 'status', // streaming & turn (§9.4) 'turn_start', 'message_delta', 'message_complete', 'tool_call', 'tool_result', 'turn_end', 'usage', 'compaction', 'provider_retry', 'model_changed', 'mode_changed', 'reasoning_level_changed', 'error', // permission / ui (§9.5) 'permission_request', 'permission_resolved', 'ui_request', 'ui_resolved', // managed strict steer ack (saga u4uo9m.3) 'managed_steer_ack', // orchestrator (§10.2) 'agent_spawned', 'agent_exited', 'agent_status_changed', 'agent_activity', 'session_runtime_changed', 'workspace_created', 'workspace_attached', 'workspace_retired', 'workspace_conflict', // subagent job run-state changes (saga u4uo9m.1). 'subagent_run_changed',]);
/** Whether a message is a client→server command. */export function isCommand(msg: Envelope): boolean { return COMMAND_TYPES.has(msg.type);}
/** Whether a message is a server→client response (§9.3). */export function isResponse(msg: Envelope): boolean { return msg.type === 'response';}
/** Whether a message is a server→client event (lifecycle, streaming, permission/ui, orchestrator). */export function isEvent(msg: Envelope): boolean { return EVENT_TYPES.has(msg.type);}
/** Whether a message is an orchestrator-level event (§10.2, daemon-only, no routing agent_id). */export function isOrchestratorMessage(msg: Envelope): boolean { return ( msg.type === 'agent_spawned' || msg.type === 'agent_exited' || msg.type === 'agent_status_changed' || msg.type === 'agent_activity' || msg.type === 'session_runtime_changed' || msg.type === 'workspace_created' || msg.type === 'workspace_attached' || msg.type === 'workspace_retired' || msg.type === 'workspace_conflict' || msg.type === 'subagent_run_changed' );}
// ─── §5 rule table (correlation mode + agent_id requirement) ────────────────────
/** * Whether a per-agent message type requires `agent_id` on a daemon connection (§5.2). * Orchestrator messages (agent.* / daemon.*) do NOT carry routing agent_id. This mirrors * the type-level rule encoded in `commands.ts` (additionalProperties:false on orchestrator * commands) for runtime validation of inbound messages. */export function requiresAgentIdOnDaemon(type: string): boolean { // Orchestrator commands + events never carry a routing agent_id. if (ORCHESTRATOR_TYPES.has(type)) return false; // hello is connection-level (no agent context yet). if (type === 'hello') return false; // Everything else is per-agent: prompt, steer, abort, set_mode, list_models, set_model, // list_reasoning_levels, set_reasoning_level, session_op, permission_response, // ui_response, and all §9.4/§9.5 events. On a daemon connection these carry agent_id; // on a direct connection it's omitted (envelope.validateAgentId). return true;}
/** * Whether an orchestrator command carries `agent_id` as a PAYLOAD field (the target), NOT * a routing key (§10.1). `agent.attach`/`detach`/`kill`/`status` name their target via * `agent_id`; `agent.list`/`agent.create`/`daemon.shutdown` do not carry it at all. The * daemon-side broker dispatches orchestrator commands BEFORE the routing-agent_id check * (§5.2), so this distinction matters only on the CLIENT side (a client wire must not * reject `agent.attach`'s target `agent_id` as a spurious routing key — saga 8usqif). */export function orchestratorCarriesAgentIdPayload(type: string): boolean { return ORCHESTRATOR_AGENT_ID_PAYLOAD_TYPES.has(type);}
const ORCHESTRATOR_TYPES = new Set<string>([ 'agent.list', 'agent.create', 'agent.attach', 'agent.detach', 'agent.terminate', 'agent.kill', 'agent.status', 'daemon.shutdown', 'web.approve', 'web.token.create', 'web.clients', 'web.revoke', 'web.token.rotate', 'session.create', 'session.status', 'session.terminate', 'session.kill', 'session.activate', 'session.list', 'session.replay', // Tool-usage stats aggregate (PROTOCOL §10 stats surface). 'stats.tool_usage', // Token-usage stats aggregate (PROTOCOL §10 stats surface; sibling to tool-usage). 'stats.token_usage', // Workspace lifecycle commands (saga j8dtju.6). 'workspace.create', 'workspace.attach', 'workspace.list', 'workspace.status', 'workspace.retire', // Subagent job orchestrator commands (saga u4uo9m.1). 'subagent.create', 'subagent.wait', 'subagent.steer', 'subagent.relaunch', 'subagent.status', 'subagent.stop', 'agent_spawned', 'agent_exited', 'agent_status_changed', 'agent_activity', 'subagent_run_changed',]);
/** Orchestrator commands that name a target agent via `agent_id` (payload, not routing). */const ORCHESTRATOR_AGENT_ID_PAYLOAD_TYPES = new Set<string>([ 'agent.attach', 'agent.detach', 'agent.terminate', 'agent.kill', 'agent.status',]);
/** * The §5.1 correlation mode for a message type. Commands are request/response or streamed; * events never carry a correlation `id`. */export function correlationModeFor(type: string): CorrelationMode { if (type === 'response') return 'request-response'; // reply direction if (!COMMAND_TYPES.has(type)) return 'event'; // Streamed commands: their real outcome arrives via events (§5.1). if ( type === 'prompt' || type === 'steer' || type === 'permission_response' || type === 'ui_response' ) { return 'streamed'; } // request/response commands: agent.list/create/attach/detach/kill/status, // daemon.shutdown, session.list, abort, set_mode, list_models, set_model, // list_reasoning_levels, set_reasoning_level, session_op. return 'request-response';}
/** Validate an inbound message against the catalog + the §5 rules. */export function validateMessage( msg: unknown,): { ok: true; message: WireMessage } | { ok: false; error: string } { if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) { return { ok: false, error: 'message must be a JSON object' }; } const env = msg as Envelope; if (typeof env.type !== 'string') { return { ok: false, error: 'message missing string `type`' }; } if (!COMMAND_TYPES.has(env.type) && !EVENT_TYPES.has(env.type)) { return { ok: false, error: `unknown message type "${env.type}"` }; } if (COMMAND_TYPES.has(env.type) && !Value.Check(CommandSchema, msg)) { return { ok: false, error: `invalid command payload for "${env.type}"` }; } if (EVENT_TYPES.has(env.type) && !Value.Check(WireMessageSchema, msg)) { return { ok: false, error: `invalid event payload for "${env.type}"` }; } // Events must not carry a correlation `id` (§5.1). if (EVENT_TYPES.has(env.type) && env.type !== 'response' && env.id !== undefined) { return { ok: false, error: `event "${env.type}" must not carry a correlation id (§5.1)`, }; } return { ok: true, message: env as WireMessage };}