import type { SDKAssistantMessage, SDKCompactBoundaryMessage, SDKMessage, SDKPartialAssistantMessage, SDKResultMessage, SDKStatusMessage, SDKSystemMessage, SDKToolProgressMessage, } from '../entrypoints/agentSdkTypes.ts' import type { AssistantMessage, Message, StreamEvent, SystemMessage, } from '../types/message.js' import { logForDebugging } from '../utils/debug.js' import { fromSDKCompactMetadata } from '../utils/messages/mappers.js' import { createUserMessage } from '../utils/messages.js' /** * Converts SDKMessage from CCR to REPL Message types. * * The CCR backend sends SDK-format messages via WebSocket. The REPL expects * internal Message types for rendering. This adapter bridges the two. */ /** * Convert an SDKAssistantMessage to an AssistantMessage */ function convertAssistantMessage(msg: SDKAssistantMessage): AssistantMessage { return { type: 'assistant', message: msg.message, uuid: msg.uuid, requestId: undefined, timestamp: new Date().toISOString(), error: msg.error, } } /** * Convert an SDKPartialAssistantMessage (streaming) to a StreamEvent */ function convertStreamEvent(msg: SDKPartialAssistantMessage): StreamEvent { return { type: 'stream_event', event: msg.event, } } /** * Convert an SDKResultMessage to a SystemMessage */ function convertResultMessage(msg: SDKResultMessage): SystemMessage { const isError = msg.subtype !== 'success' const content = isError ? msg.errors?.join(', ') || 'Unknown error' : 'Session completed successfully' return { type: 'system', subtype: 'informational', content, level: isError ? 'warning' : 'info', uuid: msg.uuid, timestamp: new Date().toISOString(), } } /** * Convert an SDKSystemMessage (init) to a SystemMessage */ function convertInitMessage(msg: SDKSystemMessage): SystemMessage { return { type: 'system', subtype: 'informational', content: `Remote session initialized (model: ${msg.model})`, level: 'info', uuid: msg.uuid, timestamp: new Date().toISOString(), } } /** * Convert an SDKStatusMessage to a SystemMessage */ function convertStatusMessage(msg: SDKStatusMessage): SystemMessage | null { if (!msg.status) { return null } return { type: 'system', subtype: 'informational', content: msg.status === 'compacting' ? 'Compacting conversation…' : `Status: ${msg.status}`, level: 'info', uuid: msg.uuid, timestamp: new Date().toISOString(), } } /** * Convert an SDKToolProgressMessage to a SystemMessage. * We use a system message instead of ProgressMessage since the Progress type * is a complex union that requires tool-specific data we don't have from CCR. */ function convertToolProgressMessage( msg: SDKToolProgressMessage, ): SystemMessage { return { type: 'system', subtype: 'informational', content: `Tool ${msg.tool_name} running for ${msg.elapsed_time_seconds}s…`, level: 'info', uuid: msg.uuid, timestamp: new Date().toISOString(), toolUseID: msg.tool_use_id, } } /** * Convert an SDKCompactBoundaryMessage to a SystemMessage */ function convertCompactBoundaryMessage( msg: SDKCompactBoundaryMessage, ): SystemMessage { return { type: 'system', subtype: 'compact_boundary', content: 'Conversation compacted', level: 'info', uuid: msg.uuid, timestamp: new Date().toISOString(), compactMetadata: fromSDKCompactMetadata(msg.compact_metadata), } } /** * Result of converting an SDKMessage */ export type ConvertedMessage = | { type: 'message'; message: Message } | { type: 'stream_event'; event: StreamEvent } | { type: 'ignored' } type ConvertOptions = { /** Convert user messages containing tool_result content blocks into UserMessages. * Used by direct connect mode where tool results come from the remote server * and need to be rendered locally. CCR mode ignores user messages since they * are handled differently. */ convertToolResults?: boolean /** * Convert user text messages into UserMessages for display. Used when * converting historical events where user-typed messages need to be shown. * In live WS mode these are already added locally by the REPL so they're * ignored by default. */ convertUserTextMessages?: boolean } /** * Convert an SDKMessage to REPL message format */ export function convertSDKMessage( msg: SDKMessage, opts?: ConvertOptions, ): ConvertedMessage { switch (msg.type) { case 'assistant': return { type: 'message', message: convertAssistantMessage(msg) } case 'user': { const content = msg.message?.content // Tool result messages from the remote server need to be converted so // they render and collapse like local tool results. Detect via content // shape (tool_result blocks) — parent_tool_use_id is NOT reliable: the // agent-side normalizeMessage() hardcodes it to null for top-level // tool results, so it can't distinguish tool results from prompt echoes. const isToolResult = Array.isArray(content) && content.some(b => b.type === 'tool_result') if (opts?.convertToolResults && isToolResult) { return { type: 'message', message: createUserMessage({ content, toolUseResult: msg.tool_use_result, uuid: msg.uuid, timestamp: msg.timestamp, }), } } // When converting historical events, user-typed messages need to be // rendered (they weren't added locally by the REPL). Skip tool_results // here — already handled above. if (opts?.convertUserTextMessages && !isToolResult) { if (typeof content === 'string' || Array.isArray(content)) { return { type: 'message', message: createUserMessage({ content, toolUseResult: msg.tool_use_result, uuid: msg.uuid, timestamp: msg.timestamp, }), } } } // User-typed messages (string content) are already added locally by REPL. // In CCR mode, all user messages are ignored (tool results handled differently). return { type: 'ignored' } } case 'stream_event': return { type: 'stream_event', event: convertStreamEvent(msg) } case 'result': // Only show result messages for errors. Success results are noise // in multi-turn sessions (isLoading=false is sufficient signal). if (msg.subtype !== 'success') { return { type: 'message', message: convertResultMessage(msg) } } return { type: 'ignored' } case 'system': if (msg.subtype === 'init') { return { type: 'message', message: convertInitMessage(msg) } } if (msg.subtype === 'status') { const statusMsg = convertStatusMessage(msg) return statusMsg ? { type: 'message', message: statusMsg } : { type: 'ignored' } } if (msg.subtype === 'compact_boundary') { return { type: 'message', message: convertCompactBoundaryMessage(msg), } } // hook_response and other subtypes logForDebugging( `[sdkMessageAdapter] Ignoring system message subtype: ${msg.subtype}`, ) return { type: 'ignored' } case 'tool_progress': return { type: 'message', message: convertToolProgressMessage(msg) } case 'auth_status': // Auth status is handled separately, not converted to a display message logForDebugging('[sdkMessageAdapter] Ignoring auth_status message') return { type: 'ignored' } case 'tool_use_summary': // Tool use summaries are SDK-only events, not displayed in REPL logForDebugging('[sdkMessageAdapter] Ignoring tool_use_summary message') return { type: 'ignored' } case 'rate_limit_event': // Rate limit events are SDK-only events, not displayed in REPL logForDebugging('[sdkMessageAdapter] Ignoring rate_limit_event message') return { type: 'ignored' } default: { // Gracefully ignore unknown message types. The backend may send new // types before the client is updated; logging helps with debugging // without crashing or losing the session. logForDebugging( `[sdkMessageAdapter] Unknown message type: ${(msg as { type: string }).type}`, ) return { type: 'ignored' } } } } /** * Check if an SDKMessage indicates the session has ended */ export function isSessionEndMessage(msg: SDKMessage): boolean { return msg.type === 'result' } /** * Check if an SDKResultMessage indicates success */ export function isSuccessResult(msg: SDKResultMessage): boolean { return msg.subtype === 'success' } /** * Extract the result text from a successful SDKResultMessage */ export function getResultText(msg: SDKResultMessage): string | null { if (msg.subtype === 'success') { return msg.result } return null }