研究日期:2026-07-23(Asia/Shanghai)
仓库:earendil-works/pi
源码快照:fc85bdd88be93b1e9a6b6bcfa41c684282ec79cc
当前四包版本:0.81.1
最新稳定版:0.81.1
Node.js:>=22.19.0
许可证:MIT

版本与实时校准

本文研究的是 2026-07-23 从官方 main 快进后固定的源码:

fc85bdd88be93b1e9a6b6bcfa41c684282ec79cc
2026-07-23T12:28:12+02:00
feat(coding-agent): emit bash_execution_update events (#6971)

当日 GitHub API 快照:

  • 76,001 stars;
  • 9,345 forks;
  • 71 个 open issues + PRs;
  • 默认分支 main
  • 仓库当日仍有 push。

GitHub Release 与 npm 的最新稳定版都是 0.81.1,发布于 2026-07-21。 pi-aipi-agent-corepi-coding-agentpi-tui 在快照中均为 0.81.1。项目迁移后的当前包名是 @earendil-works/*,旧博客里的 @mariozechner/* 只适合说明历史,不能直接复制成当前安装命令。

Release v0.81.1 还加入了带校验和的确定性源码归档,以及 compaction / branch-summary 的 retry policy 和生命周期事件。本文讨论固定 commit;pi 的 generic harness 正在快速开发,尤其 settled、hook、retry 与 coding-agent migration 的语义可能继续变化。

结论先行

pi 最值得拆的不是“四个包很少”,而是它给“结束”定义了多个不同强度的边界:

runAgentLoop
└─ agent_end
   没有更多 low-level loop events

Agent.prompt() / Agent.waitForIdle()
└─ idle
   agent_end 的 awaited subscribers 也已完成

current pi-coding-agent / AgentSession
└─ agent_settled
   retry、auto-compaction、agent_end 中新排入的消息、
   pending bash persistence 与 extension listeners 已收口

new generic AgentHarness
└─ settled
   save-point writes 与 phase 已处理;
   但精确时机仍在审计,而且 coding agent 尚未迁移到这里

这不是命名洁癖。UI 何时停止 spinner、SDK promise 何时 resolve、session 何时 可安全切换、计费何时封账、下一条命令何时可接受,都依赖 settlement contract。

pi 的极简主义因此不是“少做一层”,而是让每一层只等待自己负责的事情。

对旧稿最重要的修正

  1. 当前 coding agent 没有使用新的 AgentHarness
    packages/coding-agent/src/core/sdk.ts 仍构造低层 Agent,外面包着自己的 AgentSessionpackages/agent/docs/agent-harness.md 明确把 coding-agent migration 列为 planned,完成项为 none。不能把 generic harness 的 save-point / phase 语义写成当前产品已经采用的主链。

  2. 当前仓库有两条并行的 harness 路径。
    产品路径是 AgentSession → Agent → runAgentLoop;新通用路径是 AgentHarness → runAgentLoop。后者已经主动移除对 Agent 的依赖,但仍有 lifecycle、hook、retry、auto-compaction 与 recovery TODO。

  3. agent_end 不等于产品层真正 idle。
    对低层 loop,它只表示不再产生 loop event。Agent 会继续等待 agent_end subscribers;AgentSession 还会检查 retry、compaction 和 handler 新排入的消息,最后才发 agent_settled

  4. raw agentLoop() stream 与 Agent 的背压语义不同。
    stream wrapper 把事件 push 进 EventStream,不会把任意消费者工作作为 loop barrier;Agent 则直接使用 awaited event sink,先归约 state,再按 注册顺序等待 listener。观测者与控制者不能混为一谈。

  5. 并行完成顺序与 transcript 顺序被刻意分开。
    工具可并行,tool_execution_end 按真实完成顺序出现;Promise.all 返回值仍按 source array 排列,随后 tool-result messages 按模型原始调用 顺序写回。这同时保留实时反馈和确定性历史。

  6. append-only JSONL 不等于“模型永远看见完整历史”。
    session tree 保存所有 branch entries;模型上下文是从 active branch 加上 compaction projection 编译出来的有损视图。持久化真相与下一次推理输入是 两个对象。

  7. project trust 不是 tool sandbox。
    trust gate 控制是否加载项目 .pi settings、extensions 和 resources; 它防止未经确认的仓库代码自动进入进程,却不限制已经启用的 read / write / bash 工具。官方 README 明确:pi 默认继承启动进程对文件、进程、网络和 凭据的权限。

  8. provider normalization 是统一接口,不是抹平全部语义。
    pi-ai 提供 common AssistantMessage stream,但 reasoning、signed content、cache usage、aborted request 的 token / cost 仍有 provider 差异; 跨 provider session handoff 是 best effort,不应写成完全可替换。

产品与包边界

当前主仓库有四个发布包:

| 包 | 当前职责 | 不负责什么 | |---|---|---| | pi-ai | provider model、auth、streaming、统一 message events | session tree、工具策略、终端 | | pi-agent-core | low-level loop、stateful Agent、generic AgentHarness | coding UX、完整企业治理 | | pi-coding-agent | AgentSession、工具、session JSONL tree、compaction、resources、extensions、CLI / RPC | 强制 OS sandbox | | pi-tui | retained-mode terminal components、差量渲染、synchronized output | provider 与 agent lifecycle |

Slack / chat automation 位于独立的 earendil-works/pi-chat。这不是简单删功能, 而是把“通用 Agent kernel”和“具体 distribution / channel product”分开。

当前 coding agent 的真实请求链

以下是当前 pi-coding-agent,不是规划中的 generic harness:

CLI / print / RPC / SDK input
  ↓
AgentSessionRuntime
  cwd-bound session + model/settings/auth/resource services
  ↓
AgentSession.prompt()
  command/template expansion
  ResourceLoader: AGENTS/CLAUDE, skills, prompts, extensions, themes
  input + before_agent_start extension hooks
  ↓
Agent.prompt()
  stateful transcript + queue + abort + awaited listeners
  ↓
runAgentLoop()
  transformContext → convertToLlm → pi-ai provider stream
  ↓
message_end(assistant)
  Agent state first
  → AgentSession extension replacement/listeners
  → SessionManager JSONL append
  ↓
tool preflight in model source order
  tool_execution_start
  validate / beforeToolCall
  ↓
prepared tools execute parallel by default
  completion-order updates / tool_execution_end
  ↓
Promise.all restores source order
  toolResult message_end → JSONL append
  ↓
next model turn or agent_end
  ↓
Agent awaits agent_end subscribers and becomes idle
  ↓
AgentSession post-run loop
  retry / compaction / queued continuation
  ↓
flush pending bash messages
  ↓
agent_settled → UI / extension waitForIdle

输入与资源

AgentSession.prompt() 先处理 extension command 和 file-based prompt template,再把输入交给 extension interception。ResourceLoader 组合全局与 逐目录的 AGENTS.md / CLAUDE.md、skills、prompt templates、themes、 SYSTEM.md / APPEND_SYSTEM.md 和 extensions。

项目 trust gate 决定 project-scoped resources 是否加载。interactive 模式可 询问用户;non-interactive 场景如果没有已保存决策,会按配置忽略不受信项目 资源。这个边界针对“仓库能否把代码和 prompt 注入 pi”,不是针对“模型调用 bash 后能做什么”。

provider 与模型消息

sdk.ts 构造 Agent 时注入:

  • model 与 thinking level;
  • convertToLlm,包括 image filtering;
  • streamFn,委托 modelRuntime.streamSimple
  • provider timeout、retry、websocket timeout 和 headers hooks;
  • context transform、session id 与 tool execution mode。

pi-ai 将 provider-specific delta 归约为 common assistant message stream。上层看到统一的 text / thinking / tool-call / usage / error 事件, 但仍需保留 provider metadata,不能假设每家 usage、cache、reasoning 和 abort 都等价。

assistant message 先于工具执行落入状态

Agent.processEvents()message_end

case "message_end":
  this._state.streamingMessage = undefined;
  this._state.messages.push(event.message);
  break;

随后才按订阅顺序 await listeners。当前 AgentSession listener:

  1. 先等待 extension event;
  2. 再通知 UI / SDK listeners;
  3. 最后把最终 message append 到 SessionManager

extension 可在 message_end 替换 message。实现采用 in-place mutation,让 Agent.state.messages、后续 turn event、listener 和最终 JSONL append 看到 同一个版本。由于 listener promise 是 loop barrier,工具 preflight 开始前, assistant tool-use message 已经完成 extension 处理并持久化。

工具并发:快事件,稳历史

parallel batch 的核心可简化为:

// 伪代码;省略 validation、hooks、abort 与 error normalization
const outcomes = calls.map(call => async () => {
  const result = await execute(call);
  await emit(tool_execution_end(result)); // completion order
  return result;
});

const ordered = await Promise.all(outcomes.map(run => run()));
for (const result of ordered) {
  await emit(message_end(toToolResult(result))); // source order
}

Promise.all 并发运行,但返回数组仍对应输入位置。于是一个 100ms 的第二个 工具可以先把完成事件送给 UI,而 transcript 仍保持 tool call 1、2、3 的原始 顺序。任何 called tool 设置 executionMode: "sequential",整批会改为顺序 执行;assistant 若因 stopReason: "length" 截断,则所有潜在 tool calls 被 转成错误结果,不执行可能不完整的 arguments。

agent_end 之后还可能继续

AgentSession._runAgentPrompt()

await this.agent.prompt(messages);
while (await this._handlePostAgentRun()) {
  await this.agent.continue();
}

_handlePostAgentRun() 依次处理:

  • 可重试的 provider error;
  • retry 完结事件;
  • auto-compaction;
  • agent_end extension handler 新排入的 steering / follow-up。

finally 中还要 flush pending bash messages,才发 agent_settled 并 resolve session-level waitForIdle()。这说明 raw loop 终止与产品命令完成是不同边界。

新 generic AgentHarness:已存在,但还不是当前产品主路径

packages/agent/src/harness/agent-harness.ts 不再包装 Agent,而是直接调用 runAgentLoop()。它自己拥有:

  • run lifecycle 与 abort controller;
  • queue draining;
  • provider stream config;
  • event reduction;
  • session persistence;
  • pending write flush;
  • save-point snapshots。

save point

save point 出现在 assistant turn 及其 tool results 完成后:

  1. 先 flush agent-emitted messages 后排队的 session writes;
  2. 若 low-level loop 还会继续,获取新的 model、thinking、resources、tools、 stream options、session id 与 system prompt snapshot;
  3. 只把新 snapshot 应用到下一次 provider request,不修改 in-flight request。

这是很干净的“何时允许配置变化生效”语义。它同时避免两个极端:整次 run 冻结所有配置,或中途修改正在发送的 provider payload。

尚未完成的部分

官方 implementation TODO 在快照中仍列出:

  • finalize phase / idle semantics;
  • audit settled 是否过早;
  • settled callbacks 中的 session write 可确定排序;
  • audit follow-up around agent_end
  • auto-compaction decision point;
  • retry handling;
  • generic hook / event mechanism;
  • broad lifecycle / reentrancy tests;
  • semi-durable recovery;
  • later coding-agent migration。

因此文章可把 AgentHarness 作为架构方向和已经运行的实验性核心分析,却不能 把它的未完成语义冒充 pi-coding-agent 的生产保证。

session tree、compaction 与 durable 边界

当前 coding agent 的 session 是 append-only JSONL tree:

  • 每个 entry 有 id / parentId
  • message、model、thinking、compaction、branch summary、custom、 custom message、label、session info 等都是不同 entry;
  • active leaf 决定当前 branch;
  • branching 不删除旧 history;
  • format 当前为 v3。

compaction 的默认触发判断是:

contextTokens > contextWindow - reserveTokens

默认 reserveTokens = 16,384,目标保留最近约 20,000 tokens。cut point 不会 落在 tool-result 中间;split-turn 有专门处理。旧 coding session 的 compaction 用 firstKeptEntryId 描述保留尾部;新 generic session 格式还支持自包含的 retainedTail checkpoint。

这里必须区分:

  • JSONL tree 是持久化事实;
  • active branch 是导航选择;
  • compaction summary 是供模型继续工作的有损 projection;
  • provider request 是当前 projection 的一次快照。

issue #2608 曾暴露 repeated compaction 丢上下文,后来关闭修复。它说明 “history 没删”不等于“下一轮 context 编译一定正确”。

为什么不是 fully durable workflow

generic harness 的 durable design 文档直接指出:

  • host JavaScript tool implementations 不能自然序列化;
  • model client、auth、hooks、resource loaders 也不能只靠 session log 恢复;
  • provider stream 通常不能从中间断点续传;
  • 一个未完成的非幂等 tool 不能在重启后自动重试。

更现实的目标是 semi-durable:session log 作为事实来源,host 重新注入 capabilities;在安全 save point 恢复。对发邮件、支付、部署等副作用,应用仍 需 idempotency key、external operation ledger 或人工确认。

权限与扩展边界

官方根 README 明确写道:

Pi does not include a built-in permission system for restricting filesystem, process, network, or credential access.

默认工具继承启动用户与进程的权限。官方列出三种外部边界:

  1. Gondolin extension:pi 与 provider auth 在 host,built-in tools 和 ! 进入 microVM;其它 extension tools 仍需单独审查;
  2. Docker:整个 pi 在 container 中;bind mount 写入仍会影响 host,provider keys 通常进入 container;
  3. OpenShell:整个 process 在 policy-controlled sandbox,inference routing 可让 raw provider keys 留在 sandbox 外。

extensions / npm packages 本身能运行任意 TypeScript / JavaScript,并拥有 进程权限。read-only tool allowlist 是 capability selection,不是 OS containment。文章不能把“没有 permission popup”写成漏洞,也不能把它写成 已经安全;准确表述是:maintainer 把 sandbox 责任放在 core 外,部署者必须 显式补齐。

定向验证

环境

npm ci --ignore-scripts
npm --prefix packages/ai run hydrate-model-data

--ignore-scripts 后,部分 provider tests 在收集阶段缺少生成的 model catalog JSON。按仓库官方脚本 hydration 后恢复;这是环境数据缺失,不是断言失败。 hydration 生成的是 ignored data,研究 clone 最终 git status --short 为空。

npm 报告依赖树有 1 个 moderate 和 1 个 high vulnerability;本文没有擅自 升级或把它当作 pi runtime 缺陷。

结果

只运行 package-local Vitest 中与文章主张直接相关的 case,没有跑 full suite, 也没有调用真实 provider:

| 组 | 通过 | 验证内容 | |---|---:|---| | agent-loop.test.ts | 1 | completion-order events、source-order transcript | | agent.test.ts | 3 | awaited subscribers、waitForIdle、late parallel update | | agent-harness.test.ts | 4 | abort queue、save-point refresh、pending write order、settlement | | regression 1717-2113 | 2 | yielded message_end handler 与 tool-result persistence order | | regression 6363-agent-settled | 3 | retry 后单一 settled、idle state、extension wait | | agent-session-retry-events | 3 | delayed handler/retry、tool event order、abort persistence | | agent-session-runtime | 1 | extension replacement 持久化一致 | | agent-session-bash-persistence | 1 | user / assistant / tool / custom / bash 顺序 | | 合计 | 18 | 全部通过 |

这些测试证明固定快照中的时序契约,不证明所有 provider、extension 或长期任务 组合都无缺陷。

已知问题与事实边界

  • open issue #5886 追踪 “agent is fully settled” 的剩余 edge cases。维护者指出 agent_settled 已加入,但 issue 保持开放,且仍有长 turn + compaction 场景报告。文章应写“改进但未封口”,不能写“settlement 已彻底解决”。
  • closed #2113 记录过 message_end handler 与 tool execution race;当前代码 与 regression test 已用 awaited listeners 修复。
  • closed #3468 记录过 parallel tool result reorder;当前实现明确分开 completion-order event 与 source-order message。
  • closed #2608 记录 repeated compaction context loss;说明 projection 层仍需 单独测试。
  • AgentHarness 文档自己写着 exact settled timing under review。
  • loop 没有内建业务级成本预算、权限审批、步骤 SLA 或跨进程副作用 ledger; 上层 distribution 可以添加,但 core 不代替它们。

适合与不适合

更适合

  • 想研究或组装 coding-agent runtime 的框架作者;
  • 需要多个 provider、清楚 event ordering 和 TypeScript extension 的 CLI 团队;
  • 接受自己选择 sandbox、policy 与 workflow 的高级开发者;
  • 希望把 kernel、distribution 与 policy layer 分开部署的产品团队。

不适合直接拿来即用

  • 需要内建企业 RBAC、审批、审计封账和凭据隔离的组织;
  • 需要跨进程 exactly-once tool execution 的 durable workflow;
  • 不想自己判断 extension / project code trust 的普通终端用户;
  • 把 “minimal” 误解成 “默认没有运维与安全责任” 的部署。

比较边界

| 系统 | 主事件 / 动作边界 | 持久化与恢复 | 权限取向 | |---|---|---|---| | pi | structured provider + tool lifecycle;多层 settlement | JSONL branch tree + lossy compaction;semi-durable 方向 | core 外部 sandbox | | smolagents CodeAgent | 一段 Python 常被压成一个 interpreter action | typed transcript 与 live heap 分离 | local evaluator 非 sandbox,remote executor | | OpenClaw | Gateway-owned run 与 channel delivery | control-plane runtime state + transcript artifact | gateway policy + sandbox / tool policy | | LangGraph | graph node / checkpoint transition | explicit thread checkpoint 与 interrupt | 由 graph/app integration 决定 |

比较不是功能排名。pi 优先暴露 kernel contract;OpenClaw 更接近长驻个人 Agent distribution;smolagents 优先 code-as-action;LangGraph 优先显式 workflow state machine。

个人思考素材

  1. “完成”会变成 Agent 产品的商业接口。
    billing、SLA、spinner、通知、人工接管和下一任务调度都要绑定某个 settlement level。只记录模型停止生成会低估 extension、tool 和 recovery 的尾部工作。

  2. 市场会分成 kernel、distribution、policy 三层。
    pi 这样的 kernel 提供 event / session / provider contracts;coding-agent distribution 负责交互与资源;enterprise policy layer 再补 sandbox、审批、 identity、预算与审计。三层不必来自同一家公司。

  3. 极简会把集成税从作者转移给使用者。
    边界透明有利于高级用户,却可能形成 extension fragmentation、语义不一致和 治理缺口。未来的竞争点不只是 extension 数量,而是可机器验证的 lifecycle / authority contracts。

  4. 最有共鸣的人群。
    Unix / CLI 用户、infra engineers、framework authors,以及不喜欢隐藏 magic 的开发者会喜欢 pi;需要默认保护和一站式流程的用户未必会。

  5. 额外判断:Agent observability 必须从 event log 升级为 settlement log。
    “模型说了什么”不足以解释系统状态。trace 还要记录哪一层已经 settled、哪些 writes 已 flush、哪些 side effects 仍未确认。

文章守则

  • 不把新 generic AgentHarness 写成当前 coding-agent 主路径;
  • 不把 agent_endAgent idle、agent_settled 与 generic settled 合并;
  • 不把 project trust 写成 sandbox;
  • 不把 provider normalization 写成完全同构;
  • 不把 JSONL append-only 写成模型上下文无损;
  • 不把 closed issue 当成当前仍存在,也不把 open #5886 写成必现 bug;
  • 源码片段如有省略必须标注“简化”或“伪代码”;
  • 只声称 18 个定向 tests 通过,不声称 full suite。

来源索引

S1:仓库、版本与发布

S2:包与权限边界

S3:低层 loop、Agent 与工具顺序

S4:当前 coding-agent 主路径

S5:新 generic harness 与 durable 边界

S6:维护者解释与问题记录

三个继续追问的问题

  1. coding agent 迁到 generic AgentHarness 时,怎样证明旧 agent_settled、extension、retry 和 compaction 的 observable behavior 没有漂移?
  2. 如果 provider stream 无法续传、tool 又非幂等,semi-durable recovery 的 最小安全 checkpoint 应由 harness、tool author 还是业务应用定义?
  3. 当 kernel 刻意不内置 permission system,extension ecosystem 需要怎样的 capability manifest、签名与审计格式,才能避免“可组合”变成“不可治理”?