source dump of claude code
at main 302 lines 9.1 kB view raw
1import type { 2 SDKAssistantMessage, 3 SDKCompactBoundaryMessage, 4 SDKMessage, 5 SDKPartialAssistantMessage, 6 SDKResultMessage, 7 SDKStatusMessage, 8 SDKSystemMessage, 9 SDKToolProgressMessage, 10} from '../entrypoints/agentSdkTypes.js' 11import type { 12 AssistantMessage, 13 Message, 14 StreamEvent, 15 SystemMessage, 16} from '../types/message.js' 17import { logForDebugging } from '../utils/debug.js' 18import { fromSDKCompactMetadata } from '../utils/messages/mappers.js' 19import { createUserMessage } from '../utils/messages.js' 20 21/** 22 * Converts SDKMessage from CCR to REPL Message types. 23 * 24 * The CCR backend sends SDK-format messages via WebSocket. The REPL expects 25 * internal Message types for rendering. This adapter bridges the two. 26 */ 27 28/** 29 * Convert an SDKAssistantMessage to an AssistantMessage 30 */ 31function convertAssistantMessage(msg: SDKAssistantMessage): AssistantMessage { 32 return { 33 type: 'assistant', 34 message: msg.message, 35 uuid: msg.uuid, 36 requestId: undefined, 37 timestamp: new Date().toISOString(), 38 error: msg.error, 39 } 40} 41 42/** 43 * Convert an SDKPartialAssistantMessage (streaming) to a StreamEvent 44 */ 45function convertStreamEvent(msg: SDKPartialAssistantMessage): StreamEvent { 46 return { 47 type: 'stream_event', 48 event: msg.event, 49 } 50} 51 52/** 53 * Convert an SDKResultMessage to a SystemMessage 54 */ 55function convertResultMessage(msg: SDKResultMessage): SystemMessage { 56 const isError = msg.subtype !== 'success' 57 const content = isError 58 ? msg.errors?.join(', ') || 'Unknown error' 59 : 'Session completed successfully' 60 61 return { 62 type: 'system', 63 subtype: 'informational', 64 content, 65 level: isError ? 'warning' : 'info', 66 uuid: msg.uuid, 67 timestamp: new Date().toISOString(), 68 } 69} 70 71/** 72 * Convert an SDKSystemMessage (init) to a SystemMessage 73 */ 74function convertInitMessage(msg: SDKSystemMessage): SystemMessage { 75 return { 76 type: 'system', 77 subtype: 'informational', 78 content: `Remote session initialized (model: ${msg.model})`, 79 level: 'info', 80 uuid: msg.uuid, 81 timestamp: new Date().toISOString(), 82 } 83} 84 85/** 86 * Convert an SDKStatusMessage to a SystemMessage 87 */ 88function convertStatusMessage(msg: SDKStatusMessage): SystemMessage | null { 89 if (!msg.status) { 90 return null 91 } 92 93 return { 94 type: 'system', 95 subtype: 'informational', 96 content: 97 msg.status === 'compacting' 98 ? 'Compacting conversation…' 99 : `Status: ${msg.status}`, 100 level: 'info', 101 uuid: msg.uuid, 102 timestamp: new Date().toISOString(), 103 } 104} 105 106/** 107 * Convert an SDKToolProgressMessage to a SystemMessage. 108 * We use a system message instead of ProgressMessage since the Progress type 109 * is a complex union that requires tool-specific data we don't have from CCR. 110 */ 111function convertToolProgressMessage( 112 msg: SDKToolProgressMessage, 113): SystemMessage { 114 return { 115 type: 'system', 116 subtype: 'informational', 117 content: `Tool ${msg.tool_name} running for ${msg.elapsed_time_seconds}s…`, 118 level: 'info', 119 uuid: msg.uuid, 120 timestamp: new Date().toISOString(), 121 toolUseID: msg.tool_use_id, 122 } 123} 124 125/** 126 * Convert an SDKCompactBoundaryMessage to a SystemMessage 127 */ 128function convertCompactBoundaryMessage( 129 msg: SDKCompactBoundaryMessage, 130): SystemMessage { 131 return { 132 type: 'system', 133 subtype: 'compact_boundary', 134 content: 'Conversation compacted', 135 level: 'info', 136 uuid: msg.uuid, 137 timestamp: new Date().toISOString(), 138 compactMetadata: fromSDKCompactMetadata(msg.compact_metadata), 139 } 140} 141 142/** 143 * Result of converting an SDKMessage 144 */ 145export type ConvertedMessage = 146 | { type: 'message'; message: Message } 147 | { type: 'stream_event'; event: StreamEvent } 148 | { type: 'ignored' } 149 150type ConvertOptions = { 151 /** Convert user messages containing tool_result content blocks into UserMessages. 152 * Used by direct connect mode where tool results come from the remote server 153 * and need to be rendered locally. CCR mode ignores user messages since they 154 * are handled differently. */ 155 convertToolResults?: boolean 156 /** 157 * Convert user text messages into UserMessages for display. Used when 158 * converting historical events where user-typed messages need to be shown. 159 * In live WS mode these are already added locally by the REPL so they're 160 * ignored by default. 161 */ 162 convertUserTextMessages?: boolean 163} 164 165/** 166 * Convert an SDKMessage to REPL message format 167 */ 168export function convertSDKMessage( 169 msg: SDKMessage, 170 opts?: ConvertOptions, 171): ConvertedMessage { 172 switch (msg.type) { 173 case 'assistant': 174 return { type: 'message', message: convertAssistantMessage(msg) } 175 176 case 'user': { 177 const content = msg.message?.content 178 // Tool result messages from the remote server need to be converted so 179 // they render and collapse like local tool results. Detect via content 180 // shape (tool_result blocks) — parent_tool_use_id is NOT reliable: the 181 // agent-side normalizeMessage() hardcodes it to null for top-level 182 // tool results, so it can't distinguish tool results from prompt echoes. 183 const isToolResult = 184 Array.isArray(content) && content.some(b => b.type === 'tool_result') 185 if (opts?.convertToolResults && isToolResult) { 186 return { 187 type: 'message', 188 message: createUserMessage({ 189 content, 190 toolUseResult: msg.tool_use_result, 191 uuid: msg.uuid, 192 timestamp: msg.timestamp, 193 }), 194 } 195 } 196 // When converting historical events, user-typed messages need to be 197 // rendered (they weren't added locally by the REPL). Skip tool_results 198 // here — already handled above. 199 if (opts?.convertUserTextMessages && !isToolResult) { 200 if (typeof content === 'string' || Array.isArray(content)) { 201 return { 202 type: 'message', 203 message: createUserMessage({ 204 content, 205 toolUseResult: msg.tool_use_result, 206 uuid: msg.uuid, 207 timestamp: msg.timestamp, 208 }), 209 } 210 } 211 } 212 // User-typed messages (string content) are already added locally by REPL. 213 // In CCR mode, all user messages are ignored (tool results handled differently). 214 return { type: 'ignored' } 215 } 216 217 case 'stream_event': 218 return { type: 'stream_event', event: convertStreamEvent(msg) } 219 220 case 'result': 221 // Only show result messages for errors. Success results are noise 222 // in multi-turn sessions (isLoading=false is sufficient signal). 223 if (msg.subtype !== 'success') { 224 return { type: 'message', message: convertResultMessage(msg) } 225 } 226 return { type: 'ignored' } 227 228 case 'system': 229 if (msg.subtype === 'init') { 230 return { type: 'message', message: convertInitMessage(msg) } 231 } 232 if (msg.subtype === 'status') { 233 const statusMsg = convertStatusMessage(msg) 234 return statusMsg 235 ? { type: 'message', message: statusMsg } 236 : { type: 'ignored' } 237 } 238 if (msg.subtype === 'compact_boundary') { 239 return { 240 type: 'message', 241 message: convertCompactBoundaryMessage(msg), 242 } 243 } 244 // hook_response and other subtypes 245 logForDebugging( 246 `[sdkMessageAdapter] Ignoring system message subtype: ${msg.subtype}`, 247 ) 248 return { type: 'ignored' } 249 250 case 'tool_progress': 251 return { type: 'message', message: convertToolProgressMessage(msg) } 252 253 case 'auth_status': 254 // Auth status is handled separately, not converted to a display message 255 logForDebugging('[sdkMessageAdapter] Ignoring auth_status message') 256 return { type: 'ignored' } 257 258 case 'tool_use_summary': 259 // Tool use summaries are SDK-only events, not displayed in REPL 260 logForDebugging('[sdkMessageAdapter] Ignoring tool_use_summary message') 261 return { type: 'ignored' } 262 263 case 'rate_limit_event': 264 // Rate limit events are SDK-only events, not displayed in REPL 265 logForDebugging('[sdkMessageAdapter] Ignoring rate_limit_event message') 266 return { type: 'ignored' } 267 268 default: { 269 // Gracefully ignore unknown message types. The backend may send new 270 // types before the client is updated; logging helps with debugging 271 // without crashing or losing the session. 272 logForDebugging( 273 `[sdkMessageAdapter] Unknown message type: ${(msg as { type: string }).type}`, 274 ) 275 return { type: 'ignored' } 276 } 277 } 278} 279 280/** 281 * Check if an SDKMessage indicates the session has ended 282 */ 283export function isSessionEndMessage(msg: SDKMessage): boolean { 284 return msg.type === 'result' 285} 286 287/** 288 * Check if an SDKResultMessage indicates success 289 */ 290export function isSuccessResult(msg: SDKResultMessage): boolean { 291 return msg.subtype === 'success' 292} 293 294/** 295 * Extract the result text from a successful SDKResultMessage 296 */ 297export function getResultText(msg: SDKResultMessage): string | null { 298 if (msg.subtype === 'success') { 299 return msg.result 300 } 301 return null 302}