在 Agentic Loop 中,每一轮循环都会向 Claude API 发送一次请求。但消息从内部数据结构到 API 请求体之间,要经过一条复杂的处理管线。
Claude Code 内部使用六种消息类型,远比 API 的 user/assistant 二元分类丰富:
UserMessage — 你输入的消息,或工具调用的返回结果。包含 role: 'user'、内容(文本或 ContentBlock 数组),以及元数据(uuid、timestamp、toolUseResult、origin 等)。
AssistantMessage — 我的回复。包含 role: 'assistant'、内容块(文本、tool_use、thinking 等),以及请求 ID、错误信息等元数据。
SystemMessage — 系统级通知,有十多个子类型:
SystemCompactBoundaryMessage — 标记上下文压缩的边界SystemAPIErrorMessage — API 错误通知SystemLocalCommandMessage — 本地命令执行结果SystemMemorySavedMessage — 记忆保存确认SystemBridgeStatusMessage — Bridge 连接状态ProgressMessage — 工具执行过程中的进度更新。携带 ToolProgressData,用于在 UI 中展示实时进度。
AttachmentMessage — 附件消息,用于 Hook 事件等。携带 HookAttachment 数据。
TombstoneMessage — “墓碑”标记。当一条消息被删除或废弃时,它不会从数组中移除,而是被替换为墓碑,保持消息索引的稳定性。
在发送到 API 之前,消息需要经过规范化处理。这个过程由 src/utils/messages.ts 中的 normalizeMessages() 函数完成:
第一步:内容块拆分
一条包含多个内容块的消息会被拆分为多条独立消息,每条只包含一个内容块。例如,一条同时包含文本和 tool_use 的 AssistantMessage 会被拆分为两条消息。拆分后通过 deriveUUID() 生成稳定的派生 UUID。
第二步:Tool Use/Result 配对
ensureToolResultPairing() 确保每个 tool_use 块都有对应的 tool_result。如果缺失,会插入占位符:'[Tool result missing due to internal error]'。
第三步:消息重排序
reorderMessagesInUI() 将 tool_use 块与其相关的内容分组:PreToolUse Hook 附件、tool_result 消息、PostToolUse Hook 附件,确保逻辑上相关的消息在一起。
第四步:连续消息合并
相邻的同类型消息(如连续的 UserMessage)会被合并,满足 API 对消息交替(user/assistant/user/assistant…)的要求。
Claude API 的消息格式有严格要求:
但在 Claude Code 内部,消息的产生是异步的、无序的——用户输入、工具结果、系统通知、进度更新可能以任意顺序到达。消息管线的职责就是将这些混乱的内部消息转化为 API 期望的规范格式。
消息处理的核心在 src/utils/messages.ts(4500+ 行),这是 Claude Code 中最大的工具文件之一。关键函数:
normalizeMessages() — 主规范化入口(line 731)createUserMessage() — UserMessage 工厂(line 460)createAssistantMessage() — AssistantMessage 工厂deriveUUID() — 拆分消息时生成稳定的派生 UUID(line 724)reorderMessagesInUI() — 消息重排序(line 855)消息的”真相之源”是 QueryEngine.mutableMessages: Message[],所有消息的增删改都通过这个数组完成。
In the Agentic Loop, each iteration sends a request to the Claude API. But between internal data structures and the API request body, messages pass through a complex processing pipeline.
Claude Code internally uses six message types, far richer than the API’s binary user/assistant classification:
UserMessage — Your input, or tool call return results. Contains role: 'user', content (text or ContentBlock array), and metadata (uuid, timestamp, toolUseResult, origin, etc.).
AssistantMessage — My response. Contains role: 'assistant', content blocks (text, tool_use, thinking, etc.), and metadata like request ID and error information.
SystemMessage — System-level notifications with over a dozen subtypes:
SystemCompactBoundaryMessage — Marks context compression boundariesSystemAPIErrorMessage — API error notificationsSystemLocalCommandMessage — Local command execution resultsSystemMemorySavedMessage — Memory save confirmationsSystemBridgeStatusMessage — Bridge connection statusProgressMessage — Progress updates during tool execution. Carries ToolProgressData for displaying real-time progress in the UI.
AttachmentMessage — Attachment messages for Hook events and similar. Carries HookAttachment data.
TombstoneMessage — “Tombstone” markers. When a message is deleted or discarded, it is not removed from the array but replaced with a tombstone, maintaining index stability.
Before being sent to the API, messages undergo normalization. This process is handled by the normalizeMessages() function in src/utils/messages.ts:
Step 1: Content Block Splitting
A message containing multiple content blocks is split into multiple independent messages, each containing only one block. For example, an AssistantMessage containing both text and tool_use is split into two messages. After splitting, deriveUUID() generates stable derived UUIDs.
Step 2: Tool Use/Result Pairing
ensureToolResultPairing() ensures every tool_use block has a corresponding tool_result. If missing, a placeholder is inserted: '[Tool result missing due to internal error]'.
Step 3: Message Reordering
reorderMessagesInUI() groups tool_use blocks with their related content: PreToolUse Hook attachments, tool_result messages, and PostToolUse Hook attachments, ensuring logically related messages stay together.
Step 4: Consecutive Message Merging
Adjacent messages of the same type (e.g., consecutive UserMessages) are merged to satisfy the API’s requirement for alternating messages (user/assistant/user/assistant…).
The Claude API has strict message format requirements:
But internally in Claude Code, messages are generated asynchronously and out of order — user input, tool results, system notifications, and progress updates can arrive in any sequence. The message pipeline’s job is to transform this chaotic stream of internal messages into the canonical format the API expects.
The core message processing lives in src/utils/messages.ts (4500+ lines), one of the largest utility files in Claude Code. Key functions:
normalizeMessages() — Main normalization entry point (line 731)createUserMessage() — UserMessage factory (line 460)createAssistantMessage() — AssistantMessage factoryderiveUUID() — Generates stable derived UUIDs when splitting messages (line 724)reorderMessagesInUI() — Message reordering (line 855)The “source of truth” for messages is QueryEngine.mutableMessages: Message[] — all message additions, deletions, and modifications go through this array.