/** * v2 → v1 event translation for the SDK event channel (pure mapping layer). * * The v2 engine's per-agent `IEventBus` publishes `DomainEvent`s whose * payloads are already v1-protocol-shaped — the same shapes the v1 core emits * through `Agent.emitEvent` (the run-v2-print runner renders them untranslated * for the same reason). What the bus does not carry is the * `sessionId` / `agentId` stamping: the bus is per-agent, so the engine-side * consumer knows both (kap-server's broadcaster stamps them the same way). * This module restores the stamping and reconciles the two streams' type * sets: v2-only types are dropped (the v1 `Event` union is closed), the task * lifecycle pair is renamed back to the legacy spelling, and the one * v1-visible fact the v2 engine publishes on the process-global * `IEventService` (`session.meta.updated`) is unwrapped from its * `{type, payload}` envelope. */ import type { Event } from '@moonshot-ai/agent-core-v2/events'; import type { Event2 } from '@moonshot-ai/agent-core-v2'; /** * DomainEvent types the v1 SDK event stream never carries: * - v2-internal facts with no v1 protocol counterpart: `context.spliced`, * `task.notified`, `plan.revision`, and the `permission.approval.*` pair * (v1 surfaces approvals through the `requestApproval` callback, never as * events). * - `prompt.*`: the v2 prompt service publishes them on the agent bus, but in * v1 they are synthesized by the daemon services layer onto the global * `IEventService` — the in-process SDK client never sees them. */ const DROPPED_DOMAIN_EVENT_TYPES: ReadonlySet = new Set([ 'context.spliced', 'task.notified', 'plan.revision', 'permission.approval.requested', 'permission.approval.resolved', 'prompt.submitted', 'prompt.completed', 'prompt.aborted', 'prompt.started', 'prompt.steered', ]); /** * Type renames needed to reproduce the v1 stream: the v1 core emits task * lifecycle facts under the legacy `background.task.*` spelling where v2 uses * `task.*`. The payloads are field-identical ports (kap-server fans out both * spellings; the v1 SDK client only ever saw the legacy one). */ const RENAMED_DOMAIN_EVENT_TYPES: Readonly> = { 'task.started': 'background.task.started', 'task.terminated': 'background.task.terminated', }; /** * Translate one agent-bus event into the v1 `Event` shape (payload plus the * `sessionId` / `agentId` stamping), or `undefined` when the type has no * place in the v1 stream (see {@link DROPPED_DOMAIN_EVENT_TYPES}). The cast * only bridges the two packages' type declarations — every type not dropped * or renamed carries a payload that is field-identical with its v1 protocol * counterpart. */ export function translateDomainEvent( event: Event2, sessionId: string, agentId: string, ): Event | undefined { if (DROPPED_DOMAIN_EVENT_TYPES.has(event.type)) return undefined; const type = RENAMED_DOMAIN_EVENT_TYPES[event.type] ?? event.type; if (event.type === 'turn.started') { const { promptAttachments: _internal, ...publicFields } = event as Event2 & { promptAttachments?: unknown; }; return Object.assign({}, publicFields, { type, sessionId, agentId }) as unknown as Event; } return Object.assign({}, event, { type, sessionId, agentId }) as unknown as Event; } /** * Translate one process-global `IEventService` fact (`{type, payload}` * envelope) into the v1 `Event` shape. Only `session.meta.updated` crosses: * it is the one fact v1 publishes through the session RPC (the prompt * metadata path, with the same payload fields nested under `payload`); every * other global-bus type is a daemon/WS-edge event the in-process v1 client * never saw. */ export function translateGlobalEvent(event: Event2): Event | undefined { const payload = (event as { readonly payload?: unknown }).payload; if (event.type !== 'session.meta.updated' || typeof payload !== 'object') { return undefined; } return { type: event.type, ...payload } as unknown as Event; }