消息类型与 convertToLlm
messages.ts 给 pi-agent-core 的 AgentMessage 加四种自定义消息类型:bashExecution、custom、branchSummary、compactionSummary。底层 Agent 只认 user/assistant/toolResult,convertToLlm 把这四种自定义类型翻译成 Message[],过滤掉 excludeFromContext 的 bash 执行结果。声明合并通过 CustomAgentMessages 接口扩展,类型层和运行时层都对齐。
职责
- 类型扩展:用
declare module给CustomAgentMessages接口加四个 role,见packages/coding-agent/src/core/messages.ts:69-77。 - bash 执行消息:
!命令的执行结果,带command、output、exitCode、cancelled、truncated、fullOutputPath,可选excludeFromContext(对应!!前缀)。见packages/coding-agent/src/core/messages.ts:29-40。 - custom 消息:扩展通过
sendMessage注入的消息,customType区分类型,display控制是否在 UI 显示,details是可选结构化数据。见packages/coding-agent/src/core/messages.ts:46-53。 - branch / compaction summary:fork 回主线时插入分支摘要,压缩后插入压缩摘要,都用
<summary>标签包裹,见packages/coding-agent/src/core/messages.ts:55-67。 - 工厂函数:
createBranchSummaryMessage/createCompactionSummaryMessage/createCustomMessage把字符串 timestamp 转成数字毫秒。见packages/coding-agent/src/core/messages.ts:100-138。 - convertToLlm:把自定义消息转成
Message[],bash 用bashExecutionToText转成 user 消息,branch/compaction summary 包<summary>标签,excludeFromContext的 bash 直接返回 undefined 过滤掉。见packages/coding-agent/src/core/messages.ts:148-195。
设计动机
为什么不直接复用 user 消息?因为 UI 层需要区分「这是 bash 执行输出」和「这是用户输入」,渲染样式、可折叠、可重发都不同。但 LLM 只需要看到文本——convertToLlm 在送 provider 前把所有自定义类型拍平成 user 消息,模型完全不知道有 custom 类型存在。这样既保留了 UI 的语义丰富性,又不污染模型上下文。
bashExecutionToText 把 bash 输出格式化成 Markdown 代码块,加上 Ran \command` 前缀和退出码后缀,让 LLM 看到的格式和用户在 UI 看到的差不多。!!前缀的 bash 通过excludeFromContext: true` 完全从 LLM 上下文移除——这是用户主动选择「这步执行不让模型看到」,常见于敏感操作或噪音输出。
branchSummary 和 compactionSummary 用 <summary> XML 标签包裹,这是 Anthropic 推荐的格式,模型会更倾向于把标签内容当成完整单元对待,而不是分散成段落。
关键文件
packages/coding-agent/src/core/messages.ts:11-24—COMPACTION_SUMMARY_PREFIX/SUFFIX和BRANCH_SUMMARY_PREFIX/SUFFIX常量。packages/coding-agent/src/core/messages.ts:29-40—BashExecutionMessage接口。packages/coding-agent/src/core/messages.ts:46-53—CustomMessage<T>接口。packages/coding-agent/src/core/messages.ts:55-67—BranchSummaryMessage与CompactionSummaryMessage,后者还存tokensBefore用于诊断。packages/coding-agent/src/core/messages.ts:69-77—declare module声明合并,扩展CustomAgentMessages。packages/coding-agent/src/core/messages.ts:82-98—bashExecutionToText,格式化 bash 输出为 LLM 友好文本。packages/coding-agent/src/core/messages.ts:100-138— 三个 create 工厂函数。packages/coding-agent/src/core/messages.ts:148-195—convertToLlm,switch + filter 模式。
convertToLlm 用 switch + never 穷尽检查,新增类型忘记处理会编译报错:
// packages/coding-agent/src/core/messages.ts:148-195
export function convertToLlm(messages: AgentMessage[]): Message[] {
return messages
.map((m): Message | undefined => {
switch (m.role) {
case "bashExecution":
if (m.excludeFromContext) {
return undefined;
}
return {
role: "user",
content: [{ type: "text", text: bashExecutionToText(m) }],
timestamp: m.timestamp,
};
case "custom": {
const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content;
return { role: "user", content, timestamp: m.timestamp };
}
// ... branchSummary / compactionSummary ...
case "user":
case "assistant":
case "toolResult":
return m;
default:
const _exhaustiveCheck: never = m;
return undefined;
}
})
.filter((m) => m !== undefined);
}bashExecutionToText 把执行结果包成 Markdown 代码块,带上退出码和截断提示:
// packages/coding-agent/src/core/messages.ts:82-98
export function bashExecutionToText(msg: BashExecutionMessage): string {
let text = `Ran \`${msg.command}\`\n`;
if (msg.output) {
text += `\`\`\`\n${msg.output}\n\`\`\``;
} else {
text += "(no output)";
}
if (msg.cancelled) {
text += "\n\n(command cancelled)";
} else if (msg.exitCode !== null && msg.exitCode !== undefined && msg.exitCode !== 0) {
text += `\n\nCommand exited with code ${msg.exitCode}`;
}
if (msg.truncated && msg.fullOutputPath) {
text += `\n\n[Output truncated. Full output: ${msg.fullOutputPath}]`;
}
return text;
}数据流
消息从产生到 LLM 的路径:
边界与失败
- excludeFromContext:
!!前缀的 bash 执行结果在convertToLlm里被完全过滤,模型看不到,但 UI 仍展示,见packages/coding-agent/src/core/messages.ts:152-156。 - custom content 双形态:
CustomMessage.content可以是 string 或Array<TextContent | ImageContent>,string 时转成单元素 TextContent 数组,见packages/coding-agent/src/core/messages.ts:162-168。 - timestamp 字符串转数字:三个 create 函数都用
new Date(timestamp).getTime(),传入已是数字 timestamp 会得到 NaN,调用方需保证传 ISO 字符串,见packages/coding-agent/src/core/messages.ts:105-106。 - blockImages 包装层:
createAgentSession在convertToLlm外再包一层convertToLlmWithBlockImages,动态读 settings 替换图片为占位文本,见 createAgentSession 装配 的边界段。 - compaction 用同一函数:
Agent的transformToLlm和 compaction 的generateSummary都用convertToLlm,保证压缩时看到的上下文和送 provider 时一致。
小结
messages.ts 用 TypeScript 声明合并给 AgentMessage 加四种自定义类型,convertToLlm 在送 provider 前把它们拍平成 user 消息,UI 层保留原类型用于差异化渲染。装配时这个函数被 createAgentSession 包一层 blockImages 过滤,看 createAgentSession 装配;压缩流程产生 CompactionSummaryMessage 的细节在 compaction 目录。