用户第二次向 agent 发消息时,运行时首先要解决三个问题:上一轮发生了什么;这次该从哪段历史继续;发给模型的内容超过窗口时删什么、总结什么。本文从这三个问题出发,对比 Pi 的 @earendil-works/pi-agent-core harness 和 Claude Code。
本文的 Pi 对象是 packages/agent/src/harness/,不包含 Pi CLI 的 packages/coding-agent 会话实现。两边都会把会话写入 JSONL,也都会用摘要替代部分旧历史;差别在于,Pi harness 把“历史、当前分支和部分运行配置”放入一棵 entry 树,Claude Code 则把“对话记录、恢复状态和本轮请求优化”分给 transcript、快照及 query 路径上的多个步骤。
本文基于 Pi agent-core harness 和我之前基于泄露版进行的 Claude Code 源码分析 做对比。文中的代码路径与行号均以这些提交为准。
先用一个请求解释四层对象
假设用户已经和 agent 对话十轮,现在补充一句“继续修改刚才的文件”。系统不会直接把 JSONL 原样发给模型,而是经过四层处理:
- 持久化记录:磁盘中的原始 JSONL,例如消息、模型切换、文件历史和摘要记录。
- 运行时状态:重新打开会话后,程序从记录中得到的“当前分支在哪里”“当前配置是什么”等结果。
- Context 投影:从记录和状态中挑出本轮需要的消息。
- provider 请求:在 Context 之外,再加 system prompt、模型、工具、思考等级和缓存参数,才成为实际 API 请求。
| 要回答的问题 | Pi agent-core harness | Claude Code |
|---|---|---|
| 磁盘里保存什么 | 一棵 SessionTreeEntry 树 |
transcript 消息加多类附属记录 |
| 从哪里继续 | leafId 指向当前树节点 |
loader 结合 transcript、压缩边界和保留段恢复视图 |
| 本轮带哪些历史 | 从当前 leaf 回溯一条路径,再投影消息 | 在 messagesForQuery 上按条件做多步变换 |
| 历史太长怎么办 | 写入 compaction entry,用摘要替代旧路径 |
先缩减工具输出和局部历史,必要时完整 compact |
接下来每一层都先说明它解决的实际问题,再说明代码如何实现。
第一层:为什么要保存“消息以外的东西”
要解决的问题: 用户恢复会话时,agent 不能只知道“说过什么”,还要知道当时用了什么模型、哪些工具可用、文件操作处于什么状态。否则恢复后的下一轮可能与中断前的运行条件不一致。
Pi agent-core harness 方案
Pi harness 将这些信息统一写成 SessionTreeEntry。其中,message 表示消息,model_change、thinking_level_change 和 active_tools_change 表示运行配置变化,compaction 表示历史摘要,branch_summary 表示被离开路径的摘要,leaf 表示一次导航跳转(packages/agent/src/harness/types.ts:382-464)。每条 entry 都有 id、parentId 和 timestamp。
这样保存的直接作用是:沿一条历史路径扫描 entry 时,deriveSessionContextState() 能重新得到这条路径最后使用的 model、thinkingLevel 和 activeToolNames;sessionEntryToContextMessages() 则只挑出消息和摘要,转成模型能读的消息(packages/agent/src/harness/session/session.ts:41-138)。注意,这些派生值描述的是会话记录;当前 AgentHarness 发请求时,模型、思考等级和工具仍由 harness 自己持有的当前字段提供,消息才来自 session.buildContext()。
Claude Code 方案
Claude Code 的做法是将不同用途的信息分开记录。TranscriptMessage 保存用户、assistant 和工具相关消息,并带有 parentUuid、logicalParentUuid、isSidechain、agentId 等字段(src/types/logs.ts:221-231)。同一 JSONL 还会出现 file-history-snapshot、attribution-snapshot、content-replacement、marble-origami-commit 和 marble-origami-snapshot。例如,content-replacement 保存工具调用 ID 和模型实际看到的替换文本:超大的工具输出放到 session 目录中,Context 只保留较小的替换内容;resume 时重放同一替换文本,避免改变 token 数和 prompt cache 前缀(src/types/logs.ts:173-185、src/utils/toolResultStorage.ts:369-388)。
因此,Pi harness 的阅读入口是“沿一条 entry 路径重放”;Claude Code 的阅读入口是“先读取 transcript,再按需要读取文件历史、Todo、collapse 等记录”。两边都保存了消息之外的状态,只是记录的归属不同。
第二层:新记录应该接在历史的哪里
要解决的问题: agent 可能在旧消息处回退后继续对话,也可能同时产生多项写入。新记录必须接到正确的历史位置;否则恢复时会把新消息接回错误分支。
Pi agent-core harness 方案
Pi harness 用两个概念解决这个问题:parentId 指向“这条 entry 之前的历史”,leafId 表示“当前正在继续的节点”。追加普通 entry 时,程序将当时的 leafId 写入 parentId;写入成功后,新 entry 成为新的 leaf。appendTail 将写入排成 promise 队列,因此两次同时发起的写入不会读到同一个过期 leaf;单次写入失败后,后续写入仍可继续(packages/agent/src/harness/session/session.ts:237-255)。
用户跳到旧节点时,moveTo() 不修改历史 entry,而是追加一条 leaf entry,记录跳转前位置和 targetId,然后把内存中的 leaf 移到目标节点。后续消息便从目标节点继续,形成新的路径;如果调用方要求生成摘要,branch_summary 会先写入新路径(packages/agent/src/harness/session/session.ts:400-421)。所以 Pi 的 JSONL 有两种顺序:文件顺序是实际写入顺序,当前对话顺序则要从 leaf 沿 parentId 向上回溯。
Claude Code 方案
Claude Code 的 recordTranscript() 先检查哪些 UUID 已经写入,只把新增消息交给 insertMessageChain(),并只允许新增片段的前缀作为 startingParentUuid。这避免压缩后保留的旧消息被误接回已经被摘要替代的前缀(src/utils/sessionStorage.ts:1391-1448)。普通会话位于 ~/.claude/projects/<project>/<sessionId>.jsonl;subagent 的 sidechain 在同一 session 目录下单独保存(src/utils/sessionStorage.ts:198-257)。
备注:什么是 sidechain?
sidechain 是主会话之外的一份独立 transcript,用来记录某个 subagent 或 forked agent 的执行过程。它有自己的 JSONL 文件和消息链,不会被当作主会话的普通历史直接拼进下一轮 Context。主 agent 可以根据任务结果读取或引用它,但主会话的继续路径仍由自己的 transcript 决定。这样可以保留子任务的调用、工具输出和中间过程,又避免这些细节默认占满主会话的上下文。
完整压缩后,Claude Code 会写入一条 parentUuid: null 的 compact boundary。恢复时,loader 遇到这条边界就停止沿 parentUuid 读取更早的历史;logicalParentUuid 保留逻辑关联,保留段的重连信息用于构造当前可继续的 transcript 视图。它处理的是“摘要之后从哪里继续恢复”,不是 Pi 那种在同一文件内切换树分支。
第三层:重启以后如何找到正确的当前状态
要解决的问题: 内存中的索引、当前节点和文件历史会在进程退出时消失。恢复必须从 JSONL 重新得到可继续的会话,同时避免把旧工具输出或已压缩历史重新塞进 Context。
Pi agent-core harness 方案
Pi harness 恢复时,JSONL 后端读取所有 entry,ArraySessionIndex.replace() 建立 entries、按 ID 查找的 byId 和当前 leaf。普通 entry 会把 leaf 推进到自己;leaf entry 则将 leaf 定位到它记录的 targetId(packages/agent/src/harness/session/jsonl-repo.ts:242-252、array-session-index.ts:82-99)。程序随后从这个 leaf 向 parentId 回溯,而不是从根节点猜测应该选择哪个子节点(array-session-index.ts:167-186)。
这一步先得到当前路径。buildSessionContext() 再沿这条路径重放状态 entry;遇到 compaction entry 时,用其中的摘要替代对应的旧历史,然后生成 SessionContext = { messages, model, thinkingLevel, activeToolNames }。结果是:同一个文件可保留所有分支,但本轮 Context 只使用当前 leaf 对应的那一条路径。
Claude Code 方案
Claude Code 恢复时先重建可继续的 transcript 视图,并在 compact boundary 处停止读取早期前缀。restoreSessionStateFromLog() 再恢复 file history、attribution、context-collapse commit/snapshot 和 Todo;从最后一条 TodoWrite 反向提取 Todo 只用于 Todo v2 未启用的路径(src/utils/sessionRestore.ts:95-150)。worktree 与 agent 设置也在恢复流程中处理。
这里要把“恢复”与“发请求”分开。恢复阶段读取记录并重建运行时对象;下一轮 query 才重新计算消息数组。content-replacement 需要在恢复阶段重放,是为了不把外置的大工具输出重新放回消息数组。文件、工具定义和部分说明则可能来自当前进程的配置,并不都来自 Session 文件。
第四层:用户回头尝试另一条方案时,历史怎样保留
要解决的问题: 用户可能让 agent 回到前一条消息,换一种方案继续。系统既要保留原来的尝试供查看,又要让新尝试成为当前上下文,必要时还要把放弃的过程压缩成可读摘要。
Pi agent-core harness 方案
Pi harness 把这件事放在同一个 Session 树内处理。导航时,它计算旧 leaf 与目标节点路径的共同祖先;旧 leaf 到共同祖先之间的 entry 就是“这次离开的历史”。调用方要求摘要且这段历史存在时,harness 将其写成 branch_summary,再从目标节点继续(packages/agent/src/harness/compaction/branch-summarization.ts:71-100、session.ts:400-421)。新路径可以看到摘要和读写文件清单,旧路径仍保留在日志中。
Claude Code 方案
Claude Code 的 /branch 选择另一种持久化单位:新建一个 session ID 和新的 JSONL 文件。它复制原 transcript 中非 sidechain 的主会话消息,按复制后的顺序重建 parentUuid 和 sessionId,并用 forkedFrom = { sessionId, messageUuid } 记录来源;content-replacement 也会复制,以免新 Session 恢复时重新注入大工具输出(src/commands/branch/branch.ts:56-172、:274-280)。
运行时 forked agent 是另一种操作。它用于隔离执行,例如让一个没有工具权限的 agent 生成 compaction 摘要;它复用父请求的 cache-safe 参数,但使用独立 ToolUseContext,并可把记录写到 sidechain(src/utils/forkedAgent.ts:464-555)。
所以本节的结论很具体:Pi harness 的“另一条尝试”保存在同一棵 Session 树里;Claude Code 的 /branch 将“另一条尝试”保存成新的 Session 文件。两边都保留来源,只是读取和恢复的入口不同。
第五层:最终发给模型的消息是怎样得到的
要解决的问题: 磁盘中的完整历史通常不能直接发给模型。系统需要挑出当前对话路径,补齐当前运行条件,并在不破坏工具调用顺序的前提下减少输入。
Pi agent-core harness 方案
Pi harness 的路径较短:先由 leafId 选出一条 entry 路径,再重放状态、执行 compaction transform、投影出 context.messages。最后 AgentHarness 将这组消息与当前模型、思考等级、工具和独立生成的 system prompt 组合成 provider 请求:
entries = getBranch(leafId)
context = session.buildContext(entries)
request = {
systemPrompt,
messages: context.messages,
model: harness.model,
thinkingLevel: harness.thinkingLevel,
tools: harness.activeTools,
}代码中的 buildContext() 完成的是“从会话历史得到消息”的工作;AgentHarness.createTurnState() 负责再把当前 harness 的配置加入请求(packages/agent/src/harness/agent-harness.ts:395-440)。自动压缩何时触发属于宿主的调度策略,不由这一段固定投影逻辑决定。
Claude Code 方案
Claude Code 的 messagesForQuery 是一份会在请求路径上反复处理的工作数组。下面的步骤共同解决“在不破坏工具调用顺序的前提下缩短输入”的问题;它们并非每轮都会执行,是否运行取决于功能开关、模型能力与当前状态(src/query.ts:379-467):
- 工具结果预算:工具输出过大时存到磁盘,消息中留下引用和预览,优先降低最容易膨胀的输入。
- Snip(裁剪):删去一部分可省略的历史,并把节省的 token 数交给后续判断。
- Microcompact(工具结果压缩):按
tool_use_id清理旧工具结果;缓存模式下生成cache_edits,由 API 请求层应用删除。 - Context collapse(局部摘要):用较小摘要替代一小段历史。原消息仍在 transcript 中,collapse commit 只保存这次替换的范围和摘要占位。
- Autocompact(完整压缩):输入仍达到阈值且未被 Context Collapse 抑制时,先尝试 session-memory compaction;没有结果才执行完整的
compactConversation()。
Pi 的主要选择是“当前走哪条历史路径”;Claude Code 还要处理“这条路径中的哪些工具输出和旧消息还能放进本轮窗口”。这就是两边 Context 构造复杂度不同的直接原因。
第六层:窗口不够时,摘要之后如何继续工作
要解决的问题: 直接删除旧消息会丢失任务目标、已修改文件和工具调用关系;直接保留又会超过模型窗口。压缩必须减少 token,同时留下继续执行需要的事实。
Pi agent-core harness 方案
Pi harness 先找安全切点。findValidCutPoints() 不把 toolResult 作为切点,避免保留侧只剩工具结果却没有对应的 tool call。切点若落在一个 turn 中间,compact() 会先单独总结被切掉的 turn 前缀,再和已有摘要组合(packages/agent/src/harness/compaction/compaction.ts:328-366、:762-793)。已有摘要会作为 previous summary 增量更新,读写过的文件也被提取为结构化清单,补足自然语言摘要可能漏掉的文件路径。
压缩得到的结果是一条新的 compaction entry,不会删除原始 entry。后续 defaultContextEntryTransform() 在构造 Context 时用摘要替代早期路径,因此“减少的是本轮输入,不是日志中的历史”。摘要请求使用独立 sessionId 与 cacheRetention: "none",即这次一次性摘要调用不保留主会话的 cache retention(packages/agent/src/harness/compaction/compaction.ts:118-138)。
备注:什么是 cache retention?
provider 的 prompt cache 会暂存一段请求前缀,例如 system prompt、工具定义和前面的消息;下一次请求前缀相同时,provider 可以复用这段已处理内容,减少输入 token 的重复计算。
cacheRetention是 Pi 向 provider 表达“这次请求的缓存应保留多久或是否保留”的参数。这里设为"none",表示 compaction 的一次性摘要请求不请求保留其缓存;因此摘要的大段输入不会占用或干扰主会话后续请求使用的缓存。
Claude Code 方案
Claude Code 的完整压缩由 compactConversation() 执行。它先运行 pre-compact hooks,再用没有工具权限的 forked agent 生成摘要;摘要 prompt 仍过长时,从最旧的 API round 开始裁掉后重试(src/services/compact/compact.ts:387-491、:1136-1200)。该调用使用与主线程一致的 cache-safe 参数。
Claude Code 还处理一个 Pi harness 本节不直接处理的问题:摘要生成后,下一轮仍需知道当前工作环境。完整压缩后的请求会重新加入最近读取的文件、当前 plan、已激活 Skill、工具定义增量、agent 列表和 MCP 指令。这些信息与摘要、compact boundary 和保留尾部消息共同构成下一轮请求,但它们并不都是摘要条目的一部分。
自动压缩会为摘要输出预留空间,再从有效窗口减去 13,000 token buffer;连续三次自动压缩失败后熔断。收到 413 时,query loop 尝试 reactive compact;成功就用 post-compact messages 重试,没有恢复方案则返回 prompt-too-long,并停止执行可能继续增加输入的 stop hooks(src/services/compact/autoCompact.ts:62-90、:160-351;src/query.ts:1119-1182)。
第七层:SubAgent 如何隔离会话、上下文和存储
要解决的问题: 主 agent 需要把一项任务交给其他 agent 时,子任务既要拥有足够的上下文和工具,又不能把所有中间消息、工具输出和失败细节默认塞回主会话。这里需要分别看子 agent 的 Context 从哪里来、运行记录存在哪里,以及结果如何回到主 agent。
Pi agent-core harness 方案
packages/agent/src/harness 没有内建的 SubAgent 或 delegation API。AgentHarness 每个 turn 都从它绑定的一个 Session 构造 Context,运行一个 agent loop,再把消息写回同一个 Session(packages/agent/src/harness/agent-harness.ts:395-440、:656-689)。SessionRepository.fork 可以复制会话,navigateTree 可以在历史树中导航,但它们是会话存储能力,不能直接等同于创建子 agent。
Pi 仓库中最接近 SubAgent 的实现位于 packages/coding-agent/examples/extensions/subagent/。它是需要用户自行安装的示例扩展,不是 agent-core harness 的正式能力。父 agent 调用 subagent 工具后,扩展为每项任务启动一个独立 pi 子进程(packages/coding-agent/examples/extensions/subagent/index.ts:267-339)。
这个子进程显式接收的是任务文本、子 agent 定义中的 system prompt、可选模型、可选工具和工作目录;代码没有把父 agent 的完整消息历史、Session ID、压缩摘要或工具结果传入。它固定以 --no-session 启动,因此使用内存 Session,不创建持久化的 JSONL 子 Session,也没有 parentSession 关系(packages/coding-agent/examples/extensions/subagent/index.ts:294-330;packages/coding-agent/src/main.ts:312-320)。
子进程通过 JSON Lines stdout 返回完成消息和工具结果。示例扩展收集这些数据,将最终 assistant 文本包装成父 agent 看到的普通工具结果;完整收集结果放在 tool result 的 details 中,供当前 UI 使用(packages/coding-agent/examples/extensions/subagent/index.ts:342-377、:666-690)。父 Session 保存的是父侧的 assistant 与 tool-result 消息,不会因此生成一个可恢复、可浏览的子 Session 日志。
Claude Code 方案
Claude Code 的本地 Task / subagent 运行在独立 query loop 中,并拥有 agentId、独立的可变运行时状态和 sidechain。普通 Task 的初始消息只有任务 prompt,不默认继承主会话完整历史;fork subagent 才显式接收父消息,并继承父线程已渲染的 system prompt、工具定义和相关模型配置,以复用请求前缀缓存(src/tools/AgentTool/runAgent.ts:368-378;src/tools/AgentTool/AgentTool.tsx:483-512、:603-635)。
子 agent 的记录写入主 session 目录内的 subagents/agent-<agentId>.jsonl,旁边的 metadata 文件保存 agent 类型、任务描述和可选 worktree 路径(src/utils/sessionStorage.ts:247-303)。普通 subagent 的 sidechain 主要保存自己的消息;fork subagent 还会把继承的父消息写入 sidechain。这样 fork 即使不读取主 transcript,也能从自己的文件恢复完整上下文(src/utils/sessionStorage.ts:1230-1261、:4190-4235)。
子 agent 完成时,Claude Code 不会把 sidechain 中的每条工具调用合并进主 transcript。它提取最终文本、任务状态、输出文件路径和用量,排入一条 <task-notification> 作为主 agent 的后续输入;完整执行过程仍留在 sidechain,主 agent 需要细节时再通过 task output 或 sidechain 读取(src/tools/AgentTool/agentToolUtils.ts:597-637;src/tasks/LocalAgentTask/LocalAgentTask.tsx:246-261)。
还需要区分 compaction 使用的 fork。compactConversation() 调用 runForkedAgent() 生成摘要时,会为了 prompt cache 复用父请求的关键前缀,但只运行一轮,并通过 createCompactCanUseTool() 拒绝所有工具调用。它的摘要直接用于重建主会话下一轮的消息数组,没有 Task 注册和 <task-notification> 回传,因此不能把它当作处理用户子任务的 SubAgent(src/services/compact/compact.ts:1125-1134、:1179-1200)。
本层的差异可以概括为:Pi 的示例扩展采用“独立进程、内存 Session、工具结果回传”;Claude Code 的本地 subagent 采用“独立 query loop、可恢复 sidechain、任务通知回传”。前者隔离得更彻底,但父子之间只显式传递任务和结果;后者保存更多子任务过程,并为普通任务和继承父 Context 的 fork 提供不同路径。
第八层:工具输出、缓存和失败为什么影响 Session
要解决的问题: coding agent 的工具输出可能远大于用户消息。若每轮都把完整输出带回模型,请求会迅速超窗;若恢复后替换文本变化,又会破坏 prompt cache,使相同前缀无法复用。
Claude Code 方案
Claude Code 先将大工具输出外置,再持久化 content-replacement,从而让 resume 后继续使用同一段替换文本。microcompact 的 cache_edits 不直接改写消息,而由 provider 请求层处理缓存删除。为判断请求前缀是否因内容变化而失去缓存复用,请求前会记录 system prompt、工具 schema、模型和 cache control 等状态;响应后,只有 cache read token 相比前次同时满足相对跌幅和绝对跌幅条件时,才记录一次 cache break。cached microcompact 的预期删除不计入 break,compaction 后会重置比较基线(src/services/api/promptCacheBreakDetection.ts:247-698)。
Pi agent-core harness 方案
Pi harness 在本层的职责更窄:compaction 确保 tool call 与 tool result 不被切断,并把读写文件清单加入摘要;appendTail 确保一次落盘失败不会让后续写入永远阻塞(packages/agent/src/harness/session/session.ts:237-255)。agent-core harness 没有实现 Claude Code query loop 中的自动压缩和 cache-break 归因管道;这只说明这部分不在本文分析的 harness 模块中,不代表 Pi CLI 或其他宿主没有额外策略。
Claude Code 还为失败循环设置了边界:autocompact 连续失败三次后熔断;413 的 reactive compact 失败后不再进入 stop hook,避免 hook 继续增加消息,又再次触发 413。
技术选型:两套运行时优先解决的问题不同
Pi harness 与 Claude Code 都不会把“模型消息数组”当作完整 Session。两边都会保存原始记录、在重启后恢复状态,并在请求前生成较小的 Context。差异在于,它们把复杂度放在不同位置。
Pi agent-core harness:先把会话历史定义清楚
Pi harness 的中心对象是 entry 树。消息、模型切换、工具集合、压缩和历史跳转都作为 entry 保存;leafId 选择当前路径,Context 由这条路径重放得到。它优先回答三个问题:
- 当前会话正在继续哪一条历史。
- 这条历史是在什么模型、思考等级和工具配置下产生的。
- 回到旧节点后,如何保留原路径并从目标节点创建另一条路径。
这是一种以可回放会话事实为中心的选型。它的优势是历史导航、分支、审计和状态恢复都有明确的数据结构:查看一个 Session 时,可以从 leaf 出发解释“为什么本轮会得到这些消息和配置”。代价是实现必须维护 entry 的父关系、文件追加顺序、索引和写入队列;同时,自动压缩阈值、缓存归因、子代理调度等策略需要由宿主在 harness 之外组合。
Pi harness 更适合以下场景:构建可嵌入其他产品的 Agent Runtime;需要保存、检查和回放多条对话路径;希望宿主自行决定 UI、模型策略、自动压缩和多 agent 协作方式。它提供的是 Session 和 Context 的基础能力,而不是完整 coding agent 产品的全部控制面。
Claude Code:先保证长任务在资源约束下继续执行
Claude Code 的中心对象不是单一 Session 树,而是“可恢复的 transcript 加上本轮请求的资源管理”。它把文件历史、任务、工具输出替换、collapse 等状态分别保存;每次请求前,再在 messagesForQuery 上处理大工具输出、局部裁剪、局部摘要、完整压缩和缓存相关变换。它优先回答三个问题:
- 长时间编程任务恢复后,哪些文件、任务和工具状态仍然可用。
- 大量命令输出和历史消息怎样不超过模型窗口。
- 在缩短输入的同时,怎样尽量保持 prompt cache 可复用,并避免 413 或重复压缩失败。
这是一种以连续执行和请求资源管理为中心的选型。它的优势是对长任务、大工具输出、Skills、MCP、subagent、缓存和失败重试有较完整的工程处理;普通 Task、fork subagent 和 compaction fork 也各自有存储和回传路径。代价是一次请求的实际 Context 来自 transcript、sidechain、快照、当前环境和多项 query 变换,排查“为什么模型看到了这段内容”通常需要跨多个模块追踪。
Claude Code 的做法更适合以下场景:完整的 coding agent 产品;任务会持续读取大量文件、执行命令、委派子任务;模型窗口、延迟、缓存成本和恢复可靠性需要成为运行时的一等约束。它为这些需求提供了更多内建机制,但宿主替换其中某一个环节的成本也更高。
对比结果
| 维度 | Pi agent-core harness | Claude Code |
|---|---|---|
| 主要设计中心 | 当前 Session 路径及其可回放事实 | 长任务的可继续执行与请求资源管理 |
| 数据模型 | entry 树、parentId、leafId、状态重放 |
transcript、sidechain、快照和 compact boundary |
| Context 的主要来源 | 当前 leaf 对应路径的投影 | transcript 加上每轮 query 变换和当前运行环境 |
| 长上下文策略 | 将摘要作为 entry,替换路径中的旧消息投影 | 工具输出外置、局部裁剪/摘要、完整压缩、环境重新附加 |
| 多 agent | harness 未内建调度;示例扩展使用隔离进程和工具结果回传 | 内建 Task、fork 和 sidechain,任务结果通过通知回到主流程 |
| 优势 | 历史、分支和状态的归属清晰;便于宿主组合策略 | 对长 coding 任务、工具输出、缓存和恢复有较完整的内建处理 |
| 代价 | 产品级调度与资源管理需要宿主补齐 | 状态分散在多个记录和请求步骤,排查 Context 来源更复杂 |
因此,选型不应简化为“树优于链”或“轻量优于完整”。如果首要问题是让宿主精确控制会话历史、分支和状态重放,Pi harness 的数据模型更直接;如果首要问题是让 coding agent 在长任务和大量工具输出下稳定继续,Claude Code 的请求侧管道覆盖更多实际约束。
补充:Pi CLI 在 harness 之外做了什么
本文刻意将 Pi CLI 排除在主要比较对象外,但这不表示 Pi CLI 只有 harness。CLI 在 packages/coding-agent 中补上了面向终端产品的部分:SessionManager 负责保存和加载 JSONL 会话、维护当前 leaf 与历史导航;AgentSession 负责把用户输入、工具执行和 turn 生命周期接入运行时;CLI 层还实现了基于上下文阈值和 overflow 的自动 compaction,并提供扩展、Skills、agent 定义等产品入口(packages/coding-agent/src/core/session-manager.ts、agent-session.ts:1943-2157、core/compaction/compaction.ts:125-136)。
因此,更准确的关系是:agent-core harness 提供可复用的 Session、Context 和 agent loop 原语;Pi CLI 是一个使用并补充这些原语的终端宿主。本文将 harness 与 Claude Code 对比,是为了观察两种 Runtime 接口的边界;它不等同于 Pi CLI 与 Claude Code 的完整产品功能对比。
这个结论只覆盖本文对应的 Pi agent-core harness 与 Claude Code 源码模块,不涵盖 Pi CLI 的完整会话实现,也不表示任一方案在所有产品中更优。

