/** * Shared ACP session-update โ†’ normalized AgentEvent mapping. * * Extracted verbatim (v2.6 Phase 2) from `AcpStreamContext.handleSessionUpdate` * in `acp-dispatch.ts` so the warm ACP backend (`backends/warm-acp.ts`) and the * one-shot dispatch share ONE mapping. The one-shot path translates the returned * events into broker frames itself (preserving its prior behavior byte-for-byte); * the warm backend forwards them to the dispatcher's `ctx.onEvent` exactly like * the opencode-server backend does. No I/O, no broker โ€” pure, so it's unit-testable. * * Spec: openspec/changes/v2-6-persistent-agent-sessions/design.md ยง2b. */ import type { SessionNotification } from '@agentclientprotocol/sdk'; import type { AgentEvent } from './agent-backend.js'; import { type AcpToolSnapshot, mergeToolSnapshot } from './acp-tool-snapshot.js'; /** * Map one ACP `session/update` notification to zero-or-more normalized AgentEvents. * * `priorSnapshots` is the caller-owned tool-call snapshot accumulator (toolCallId โ†’ * snapshot). For `tool_call` / `tool_call_update` the merged snapshot is written * back into it (mutated in place, mirroring `AcpStreamContext.handleToolUpdate`) * so a later `tool_call_update` merges over the earlier `tool_call`. Pass an empty * Map for a stateless single call. * * Returns an array (never throws) so the caller can splat it onto `onEvent`. */ export function mapSessionUpdate( params: SessionNotification, priorSnapshots: Map = new Map(), ): AgentEvent[] { const update = params.update; switch (update.sessionUpdate) { case 'agent_message_chunk': { const content = update.content; if (content.type === 'text' && 'text' in content) { return [{ type: 'text', text: (content as { text: string }).text }]; } return []; } case 'agent_thought_chunk': { const content = update.content; if (content.type === 'text' && 'text' in content) { return [{ type: 'reasoning', text: (content as { text: string }).text }]; } return []; } case 'tool_call': { const snapshot = mergeToolSnapshot(update.toolCallId, update, priorSnapshots.get(update.toolCallId)); priorSnapshots.set(update.toolCallId, snapshot); return [{ type: 'tool_call', toolCall: snapshot }]; } case 'tool_call_update': { const snapshot = mergeToolSnapshot(update.toolCallId, update, priorSnapshots.get(update.toolCallId)); priorSnapshots.set(update.toolCallId, snapshot); return [{ type: 'tool_update', toolCall: snapshot }]; } case 'available_commands_update': { const commands = update.availableCommands.map((cmd) => ({ name: cmd.name, description: cmd.description ?? undefined, })); return [{ type: 'commands', commands }]; } default: return []; } }