Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126/** * Envelope — the one shape every protocol message shares (PROTOCOL §5). * * Commands and events use the same envelope; the difference is direction and correlation, * not structure (§4 design rule #4). `id` correlates a command with its `response`; events * never carry `id`. `agent_id` is the daemon-multiplexing key (§5.2). * * Wire-safe by construction: open object schema (`[key: string]: unknown`) so type-specific * payloads ride on the same envelope without a special-cased frame (§4 design rule #2). */import { Type, type Static } from '@sinclair/typebox';
/** * The shared message envelope. `type` selects the message; the rest of the payload is * carried as additional fields (validated by the catalog schema in `catalog.ts`). */export const EnvelopeSchema = Type.Object( { /** Correlation id. If set on a command, the response carries the same id (§5.1). */ id: Type.Optional(Type.String()), /** Message type, e.g. "prompt", "tool_call", "agent.list", "response" (§5). */ type: Type.String(), /** * On daemon connections, identifies the target/owning agent for per-agent messages. * Omitted on orchestrator messages and on direct (stdio / dedicated-socket) connections * (§5.2). The catalog encodes this rule per message type. */ agent_id: Type.Optional(Type.String()), }, // Allow type-specific payload fields — "no special-cased frames" (§4 rule #2). { additionalProperties: Type.Unknown() },);
/** * The envelope type carries an open payload index signature (`[key: string]: unknown`), * matching PROTOCOL §5 — type-specific fields ride on the same envelope without a * special-cased frame. `Static<typeof EnvelopeSchema>` alone only has the declared * fields, so we intersect the index signature for the type-level openness. */export type Envelope = Static<typeof EnvelopeSchema> & { [key: string]: unknown };
/** * Connection multiplexing mode — governs the `agent_id` rule (§5.2). * * - `daemon`: per-agent messages REQUIRE `agent_id`; orchestrator messages MUST NOT carry * one (they're about the registry itself). * - `direct`: the connection is bound to exactly one agent; `agent_id` is always omitted. */export type ConnectionMode = 'daemon' | 'direct';
/** * Correlation mode for a message type (§5.1). * * - `request-response` — command sets `id`; server replies with a `response` carrying the * same `id` (e.g. `agent.list`, `session_op`, `abort`). * - `streamed` — command may set `id`; server replies with a `response` indicating * *acceptance*; the real outcome arrives via events (e.g. `prompt`, `steer`). * - `event` — never carries a correlation `id`; not a response. (The exceptions * `permission_resolved` / `ui_resolved` carry `request_id`, not `id` — §10.) */export type CorrelationMode = 'request-response' | 'streamed' | 'event';
/** * Command/event/response discrimination requires the full message catalog (it can't be * decided from the envelope alone — e.g. `tool_call` is an event but a naive type check * can't tell). Those helpers live in `catalog.ts` (sub-saga m44qnh.4). The envelope layer * only owns the shape + the agent_id rule. *//** * Validate a message's `agent_id` against the connection mode (§5.2). * * @param msg the envelope to check * @param mode connection mode * @param requiresAgentId whether this message type carries a per-agent `agent_id` on a * daemon connection (true) or is an orchestrator message that must NOT carry one (false). * @returns `null` if valid, or a human-readable PROTOCOL_ERROR-style reason. * * On `direct` connections `agent_id` must be absent regardless of type. */export function validateAgentId( msg: Envelope, mode: ConnectionMode, requiresAgentId: boolean,): string | null { if (mode === 'direct') { if (msg.agent_id !== undefined) { return 'agent_id must be omitted on a direct connection (§5.2)'; } return null; } // daemon if (requiresAgentId) { if (msg.agent_id === undefined || msg.agent_id === '') { return 'per-agent message requires agent_id on a daemon connection (§5.2)'; } } else if (msg.agent_id !== undefined) { return 'orchestrator message must not carry agent_id (§10.2 / §5.2)'; } return null;}
/** * Outbound counterpart to {@link validateAgentId} for direct connections (§5.2). * * On a direct connection `agent_id` is always omitted — the connection is bound to * exactly one agent (§5.2). Orchestrator events (`agent_spawned` etc.) never flow on a * direct connection (the daemon is the only source), so unconditionally deleting * `agent_id` is safe for every message a direct server will actually emit. `hello` (§9.1) * already has no `agent_id`, so this is a no-op on it; per-agent events (turn_start, * message_complete, tool_call, …) get the routing key stripped. * * Returns a shallow copy; the input is untouched. Transport-neutral so the future * unix-socket direct path reuses it. * * @param msg the event/response to send on a direct connection * @returns a copy with `agent_id` removed */export function omitRoutingAgentId<T extends Envelope>(msg: T): T { if (msg.agent_id === undefined) return msg; // Shallow copy without the routing key. We don't deep-clone the payload — events are // plain data and the caller doesn't keep a mutated alias. const { agent_id: _drop, ...rest } = msg; void _drop; return rest as T;}