source dump of claude code
at main 290 lines 9.0 kB view raw
1import type { BetaContentBlock } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' 2import { randomUUID, type UUID } from 'crypto' 3import { getSessionId } from 'src/bootstrap/state.js' 4import { 5 LOCAL_COMMAND_STDERR_TAG, 6 LOCAL_COMMAND_STDOUT_TAG, 7} from 'src/constants/xml.js' 8import type { 9 SDKAssistantMessage, 10 SDKCompactBoundaryMessage, 11 SDKMessage, 12 SDKRateLimitInfo, 13} from 'src/entrypoints/agentSdkTypes.js' 14import type { ClaudeAILimits } from 'src/services/claudeAiLimits.js' 15import { EXIT_PLAN_MODE_V2_TOOL_NAME } from 'src/tools/ExitPlanModeTool/constants.js' 16import type { 17 AssistantMessage, 18 CompactMetadata, 19 Message, 20} from 'src/types/message.js' 21import type { DeepImmutable } from 'src/types/utils.js' 22import stripAnsi from 'strip-ansi' 23import { createAssistantMessage } from '../messages.js' 24import { getPlan } from '../plans.js' 25 26export function toInternalMessages( 27 messages: readonly DeepImmutable<SDKMessage>[], 28): Message[] { 29 return messages.flatMap(message => { 30 switch (message.type) { 31 case 'assistant': 32 return [ 33 { 34 type: 'assistant', 35 message: message.message, 36 uuid: message.uuid, 37 requestId: undefined, 38 timestamp: new Date().toISOString(), 39 } as Message, 40 ] 41 case 'user': 42 return [ 43 { 44 type: 'user', 45 message: message.message, 46 uuid: message.uuid ?? randomUUID(), 47 timestamp: message.timestamp ?? new Date().toISOString(), 48 isMeta: message.isSynthetic, 49 } as Message, 50 ] 51 case 'system': 52 // Handle compact boundary messages 53 if (message.subtype === 'compact_boundary') { 54 const compactMsg = message 55 return [ 56 { 57 type: 'system', 58 content: 'Conversation compacted', 59 level: 'info', 60 subtype: 'compact_boundary', 61 compactMetadata: fromSDKCompactMetadata( 62 compactMsg.compact_metadata, 63 ), 64 uuid: message.uuid, 65 timestamp: new Date().toISOString(), 66 }, 67 ] 68 } 69 return [] 70 default: 71 return [] 72 } 73 }) 74} 75 76type SDKCompactMetadata = SDKCompactBoundaryMessage['compact_metadata'] 77 78export function toSDKCompactMetadata( 79 meta: CompactMetadata, 80): SDKCompactMetadata { 81 const seg = meta.preservedSegment 82 return { 83 trigger: meta.trigger, 84 pre_tokens: meta.preTokens, 85 ...(seg && { 86 preserved_segment: { 87 head_uuid: seg.headUuid, 88 anchor_uuid: seg.anchorUuid, 89 tail_uuid: seg.tailUuid, 90 }, 91 }), 92 } 93} 94 95/** 96 * Shared SDK→internal compact_metadata converter. 97 */ 98export function fromSDKCompactMetadata( 99 meta: SDKCompactMetadata, 100): CompactMetadata { 101 const seg = meta.preserved_segment 102 return { 103 trigger: meta.trigger, 104 preTokens: meta.pre_tokens, 105 ...(seg && { 106 preservedSegment: { 107 headUuid: seg.head_uuid, 108 anchorUuid: seg.anchor_uuid, 109 tailUuid: seg.tail_uuid, 110 }, 111 }), 112 } 113} 114 115export function toSDKMessages(messages: Message[]): SDKMessage[] { 116 return messages.flatMap((message): SDKMessage[] => { 117 switch (message.type) { 118 case 'assistant': 119 return [ 120 { 121 type: 'assistant', 122 message: normalizeAssistantMessageForSDK(message), 123 session_id: getSessionId(), 124 parent_tool_use_id: null, 125 uuid: message.uuid, 126 error: message.error, 127 }, 128 ] 129 case 'user': 130 return [ 131 { 132 type: 'user', 133 message: message.message, 134 session_id: getSessionId(), 135 parent_tool_use_id: null, 136 uuid: message.uuid, 137 timestamp: message.timestamp, 138 isSynthetic: message.isMeta || message.isVisibleInTranscriptOnly, 139 // Structured tool output (not the string content sent to the 140 // model — the full Output object). Rides the protobuf catchall 141 // so web viewers can read things like BriefTool's file_uuid 142 // without it polluting model context. 143 ...(message.toolUseResult !== undefined 144 ? { tool_use_result: message.toolUseResult } 145 : {}), 146 }, 147 ] 148 case 'system': 149 if (message.subtype === 'compact_boundary' && message.compactMetadata) { 150 return [ 151 { 152 type: 'system', 153 subtype: 'compact_boundary' as const, 154 session_id: getSessionId(), 155 uuid: message.uuid, 156 compact_metadata: toSDKCompactMetadata(message.compactMetadata), 157 }, 158 ] 159 } 160 // Only convert local_command messages that contain actual command 161 // output (stdout/stderr). The same subtype is also used for command 162 // input metadata (e.g. <command-name>...</command-name>) which must 163 // not leak to the RC web UI. 164 if ( 165 message.subtype === 'local_command' && 166 (message.content.includes(`<${LOCAL_COMMAND_STDOUT_TAG}>`) || 167 message.content.includes(`<${LOCAL_COMMAND_STDERR_TAG}>`)) 168 ) { 169 return [ 170 localCommandOutputToSDKAssistantMessage( 171 message.content, 172 message.uuid, 173 ), 174 ] 175 } 176 return [] 177 default: 178 return [] 179 } 180 }) 181} 182 183/** 184 * Converts local command output (e.g. /voice, /cost) to a well-formed 185 * SDKAssistantMessage so downstream consumers (mobile apps, session-ingress 186 * v1alpha→v1beta converter) can parse it without schema changes. 187 * 188 * Emitted as assistant instead of the dedicated SDKLocalCommandOutputMessage 189 * because the system/local_command_output subtype is unknown to: 190 * - mobile-apps Android SdkMessageTypes.kt (no local_command_output handler) 191 * - api-go session-ingress convertSystemEvent (only init/compact_boundary) 192 * See: https://anthropic.sentry.io/issues/7266299248/ (Android) 193 * 194 * Strips ANSI (e.g. chalk.dim() in /cost) then unwraps the XML wrapper tags. 195 */ 196export function localCommandOutputToSDKAssistantMessage( 197 rawContent: string, 198 uuid: UUID, 199): SDKAssistantMessage { 200 const cleanContent = stripAnsi(rawContent) 201 .replace(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/, '$1') 202 .replace(/<local-command-stderr>([\s\S]*?)<\/local-command-stderr>/, '$1') 203 .trim() 204 // createAssistantMessage builds a complete APIAssistantMessage with id, type, 205 // model: SYNTHETIC_MODEL, role, stop_reason, usage — all fields required by 206 // downstream deserializers like Android's SdkAssistantMessage. 207 const synthetic = createAssistantMessage({ content: cleanContent }) 208 return { 209 type: 'assistant', 210 message: synthetic.message, 211 parent_tool_use_id: null, 212 session_id: getSessionId(), 213 uuid, 214 } 215} 216 217/** 218 * Maps internal ClaudeAILimits to the SDK-facing SDKRateLimitInfo type, 219 * stripping internal-only fields like unifiedRateLimitFallbackAvailable. 220 */ 221export function toSDKRateLimitInfo( 222 limits: ClaudeAILimits | undefined, 223): SDKRateLimitInfo | undefined { 224 if (!limits) { 225 return undefined 226 } 227 return { 228 status: limits.status, 229 ...(limits.resetsAt !== undefined && { resetsAt: limits.resetsAt }), 230 ...(limits.rateLimitType !== undefined && { 231 rateLimitType: limits.rateLimitType, 232 }), 233 ...(limits.utilization !== undefined && { 234 utilization: limits.utilization, 235 }), 236 ...(limits.overageStatus !== undefined && { 237 overageStatus: limits.overageStatus, 238 }), 239 ...(limits.overageResetsAt !== undefined && { 240 overageResetsAt: limits.overageResetsAt, 241 }), 242 ...(limits.overageDisabledReason !== undefined && { 243 overageDisabledReason: limits.overageDisabledReason, 244 }), 245 ...(limits.isUsingOverage !== undefined && { 246 isUsingOverage: limits.isUsingOverage, 247 }), 248 ...(limits.surpassedThreshold !== undefined && { 249 surpassedThreshold: limits.surpassedThreshold, 250 }), 251 } 252} 253 254/** 255 * Normalizes tool inputs in assistant message content for SDK consumption. 256 * Specifically injects plan content into ExitPlanModeV2 tool inputs since 257 * the V2 tool reads plan from file instead of input, but SDK users expect 258 * tool_input.plan to exist. 259 */ 260function normalizeAssistantMessageForSDK( 261 message: AssistantMessage, 262): AssistantMessage['message'] { 263 const content = message.message.content 264 if (!Array.isArray(content)) { 265 return message.message 266 } 267 268 const normalizedContent = content.map((block): BetaContentBlock => { 269 if (block.type !== 'tool_use') { 270 return block 271 } 272 273 if (block.name === EXIT_PLAN_MODE_V2_TOOL_NAME) { 274 const plan = getPlan() 275 if (plan) { 276 return { 277 ...block, 278 input: { ...(block.input as Record<string, unknown>), plan }, 279 } 280 } 281 } 282 283 return block 284 }) 285 286 return { 287 ...message.message, 288 content: normalizedContent, 289 } 290}