// SDK Message Adapter — insulates UI from Claude Agent SDK wire format changes // This is the ONLY place that knows SDK internals. export type AgentMessageType = | 'init' | 'text' | 'thinking' | 'tool_call' | 'tool_result' | 'status' | 'compaction' | 'cost' | 'error' | 'unknown'; export interface AgentMessage { id: string; type: AgentMessageType; parentId?: string; content: unknown; timestamp: number; } export interface InitContent { sessionId: string; model: string; cwd: string; tools: string[]; } export interface TextContent { text: string; } export interface ThinkingContent { text: string; } export interface ToolCallContent { toolUseId: string; name: string; input: unknown; } export interface ToolResultContent { toolUseId: string; output: unknown; } export interface StatusContent { subtype: string; message?: string; } export interface CostContent { totalCostUsd: number; durationMs: number; inputTokens: number; outputTokens: number; numTurns: number; isError: boolean; result?: string; errors?: string[]; } export interface CompactionContent { trigger: 'manual' | 'auto'; preTokens: number; } export interface ErrorContent { message: string; } /** Runtime guard — returns value if it's a string, fallback otherwise */ function str(v: unknown, fallback = ''): string { return typeof v === 'string' ? v : fallback; } /** Runtime guard — returns value if it's a number, fallback otherwise */ function num(v: unknown, fallback = 0): number { return typeof v === 'number' ? v : fallback; } /** * Adapt a raw SDK stream-json message to our internal format. * When SDK changes wire format, only this function needs updating. */ export function adaptSDKMessage(raw: Record): AgentMessage[] { const uuid = str(raw.uuid) || crypto.randomUUID(); const timestamp = Date.now(); const parentId = typeof raw.parent_tool_use_id === 'string' ? raw.parent_tool_use_id : undefined; switch (raw.type) { case 'system': return adaptSystemMessage(raw, uuid, timestamp); case 'assistant': return adaptAssistantMessage(raw, uuid, timestamp, parentId); case 'user': return adaptUserMessage(raw, uuid, timestamp, parentId); case 'result': return adaptResultMessage(raw, uuid, timestamp); default: return [{ id: uuid, type: 'unknown', content: raw, timestamp, }]; } } function adaptSystemMessage( raw: Record, uuid: string, timestamp: number, ): AgentMessage[] { const subtype = str(raw.subtype); if (subtype === 'init') { return [{ id: uuid, type: 'init', content: { sessionId: str(raw.session_id), model: str(raw.model), cwd: str(raw.cwd), tools: Array.isArray(raw.tools) ? raw.tools.filter((t): t is string => typeof t === 'string') : [], } satisfies InitContent, timestamp, }]; } if (subtype === 'compact_boundary') { const meta = typeof raw.compact_metadata === 'object' && raw.compact_metadata !== null ? raw.compact_metadata as Record : {}; return [{ id: uuid, type: 'compaction', content: { trigger: str(meta.trigger, 'auto') as 'manual' | 'auto', preTokens: num(meta.pre_tokens), } satisfies CompactionContent, timestamp, }]; } return [{ id: uuid, type: 'status', content: { subtype, message: typeof raw.status === 'string' ? raw.status : undefined, } satisfies StatusContent, timestamp, }]; } function adaptAssistantMessage( raw: Record, uuid: string, timestamp: number, parentId?: string, ): AgentMessage[] { const messages: AgentMessage[] = []; const msg = typeof raw.message === 'object' && raw.message !== null ? raw.message as Record : undefined; if (!msg) return messages; const content = Array.isArray(msg.content) ? msg.content as Array> : undefined; if (!content) return messages; for (const block of content) { switch (block.type) { case 'text': messages.push({ id: `${uuid}-text-${messages.length}`, type: 'text', parentId, content: { text: str(block.text) } satisfies TextContent, timestamp, }); break; case 'thinking': messages.push({ id: `${uuid}-think-${messages.length}`, type: 'thinking', parentId, content: { text: str(block.thinking ?? block.text) } satisfies ThinkingContent, timestamp, }); break; case 'tool_use': messages.push({ id: `${uuid}-tool-${messages.length}`, type: 'tool_call', parentId, content: { toolUseId: str(block.id), name: str(block.name), input: block.input, } satisfies ToolCallContent, timestamp, }); break; } } return messages; } function adaptUserMessage( raw: Record, uuid: string, timestamp: number, parentId?: string, ): AgentMessage[] { const messages: AgentMessage[] = []; const msg = typeof raw.message === 'object' && raw.message !== null ? raw.message as Record : undefined; if (!msg) return messages; const content = Array.isArray(msg.content) ? msg.content as Array> : undefined; if (!content) return messages; for (const block of content) { if (block.type === 'tool_result') { messages.push({ id: `${uuid}-result-${messages.length}`, type: 'tool_result', parentId, content: { toolUseId: str(block.tool_use_id), output: block.content ?? raw.tool_use_result, } satisfies ToolResultContent, timestamp, }); } } return messages; } function adaptResultMessage( raw: Record, uuid: string, timestamp: number, ): AgentMessage[] { const usage = typeof raw.usage === 'object' && raw.usage !== null ? raw.usage as Record : undefined; return [{ id: uuid, type: 'cost', content: { totalCostUsd: num(raw.total_cost_usd), durationMs: num(raw.duration_ms), inputTokens: num(usage?.input_tokens), outputTokens: num(usage?.output_tokens), numTurns: num(raw.num_turns), isError: raw.is_error === true, result: typeof raw.result === 'string' ? raw.result : undefined, errors: Array.isArray(raw.errors) ? raw.errors.filter((e): e is string => typeof e === 'string') : undefined, } satisfies CostContent, timestamp, }]; }