研究日期: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-ai、pi-agent-core、pi-coding-agent 与 pi-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 的极简主义因此不是“少做一层”,而是让每一层只等待自己负责的事情。
对旧稿最重要的修正
-
当前 coding agent 没有使用新的
AgentHarness。
packages/coding-agent/src/core/sdk.ts仍构造低层Agent,外面包着自己的AgentSession。packages/agent/docs/agent-harness.md明确把 coding-agent migration 列为 planned,完成项为 none。不能把 generic harness 的 save-point / phase 语义写成当前产品已经采用的主链。 -
当前仓库有两条并行的 harness 路径。
产品路径是AgentSession → Agent → runAgentLoop;新通用路径是AgentHarness → runAgentLoop。后者已经主动移除对Agent的依赖,但仍有 lifecycle、hook、retry、auto-compaction 与 recovery TODO。 -
agent_end不等于产品层真正 idle。
对低层 loop,它只表示不再产生 loop event。Agent会继续等待agent_endsubscribers;AgentSession还会检查 retry、compaction 和 handler 新排入的消息,最后才发agent_settled。 -
raw
agentLoop()stream 与Agent的背压语义不同。
stream wrapper 把事件 push 进EventStream,不会把任意消费者工作作为 loop barrier;Agent则直接使用 awaited event sink,先归约 state,再按 注册顺序等待 listener。观测者与控制者不能混为一谈。 -
并行完成顺序与 transcript 顺序被刻意分开。
工具可并行,tool_execution_end按真实完成顺序出现;Promise.all返回值仍按 source array 排列,随后 tool-result messages 按模型原始调用 顺序写回。这同时保留实时反馈和确定性历史。 -
append-only JSONL 不等于“模型永远看见完整历史”。
session tree 保存所有 branch entries;模型上下文是从 active branch 加上 compaction projection 编译出来的有损视图。持久化真相与下一次推理输入是 两个对象。 -
project trust 不是 tool sandbox。
trust gate 控制是否加载项目.pisettings、extensions 和 resources; 它防止未经确认的仓库代码自动进入进程,却不限制已经启用的 read / write / bash 工具。官方 README 明确:pi 默认继承启动进程对文件、进程、网络和 凭据的权限。 -
provider normalization 是统一接口,不是抹平全部语义。
pi-ai提供 commonAssistantMessagestream,但 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:
- 先等待 extension event;
- 再通知 UI / SDK listeners;
- 最后把最终 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_endextension 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 完成后:
- 先 flush agent-emitted messages 后排队的 session writes;
- 若 low-level loop 还会继续,获取新的 model、thinking、resources、tools、 stream options、session id 与 system prompt snapshot;
- 只把新 snapshot 应用到下一次 provider request,不修改 in-flight request。
这是很干净的“何时允许配置变化生效”语义。它同时避免两个极端:整次 run 冻结所有配置,或中途修改正在发送的 provider payload。
尚未完成的部分
官方 implementation TODO 在快照中仍列出:
- finalize phase / idle semantics;
- audit
settled是否过早; - 让
settledcallbacks 中的 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.
默认工具继承启动用户与进程的权限。官方列出三种外部边界:
- Gondolin extension:pi 与 provider auth 在 host,built-in tools 和
!进入 microVM;其它 extension tools 仍需单独审查; - Docker:整个 pi 在 container 中;bind mount 写入仍会影响 host,provider keys 通常进入 container;
- 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_endhandler 与 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文档自己写着 exactsettledtiming 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。
个人思考素材
-
“完成”会变成 Agent 产品的商业接口。
billing、SLA、spinner、通知、人工接管和下一任务调度都要绑定某个 settlement level。只记录模型停止生成会低估 extension、tool 和 recovery 的尾部工作。 -
市场会分成 kernel、distribution、policy 三层。
pi 这样的 kernel 提供 event / session / provider contracts;coding-agent distribution 负责交互与资源;enterprise policy layer 再补 sandbox、审批、 identity、预算与审计。三层不必来自同一家公司。 -
极简会把集成税从作者转移给使用者。
边界透明有利于高级用户,却可能形成 extension fragmentation、语义不一致和 治理缺口。未来的竞争点不只是 extension 数量,而是可机器验证的 lifecycle / authority contracts。 -
最有共鸣的人群。
Unix / CLI 用户、infra engineers、framework authors,以及不喜欢隐藏 magic 的开发者会喜欢 pi;需要默认保护和一站式流程的用户未必会。 -
额外判断:Agent observability 必须从 event log 升级为 settlement log。
“模型说了什么”不足以解释系统状态。trace 还要记录哪一层已经 settled、哪些 writes 已 flush、哪些 side effects 仍未确认。
文章守则
- 不把新 generic
AgentHarness写成当前 coding-agent 主路径; - 不把
agent_end、Agentidle、agent_settled与 genericsettled合并; - 不把 project trust 写成 sandbox;
- 不把 provider normalization 写成完全同构;
- 不把 JSONL append-only 写成模型上下文无损;
- 不把 closed issue 当成当前仍存在,也不把 open #5886 写成必现 bug;
- 源码片段如有省略必须标注“简化”或“伪代码”;
- 只声称 18 个定向 tests 通过,不声称 full suite。
来源索引
S1:仓库、版本与发布
S2:包与权限边界
S3:低层 loop、Agent 与工具顺序
runAgentLoop固定源码- parallel execution 与 source-order result
Agentawaited subscriber contractAgent.processEventsstate-first reductionpi-agent-coreREADME
S4:当前 coding-agent 主路径
sdk.ts构造new AgentAgentSessionevent / persistence_runAgentPrompt与agent_settled- Coding agent README
- Session JSONL format
- Compaction
S5:新 generic harness 与 durable 边界
AgentHarness源码- Harness save point / settlement 设计
- 迁移就绪 TODO
- Later coding-agent migration plan
- Durable harness design
S6:维护者解释与问题记录
- Mario Zechner:pi coding agent 设计
- Mario Zechner:不把 MCP 当默认核心
- #5886:agent fully settled
- #2113:
message_end/ tool race,已关闭 - #3468:parallel tool result reorder,已关闭
- #2608:repeated compaction loss,已关闭
三个继续追问的问题
- coding agent 迁到 generic
AgentHarness时,怎样证明旧agent_settled、extension、retry 和 compaction 的 observable behavior 没有漂移? - 如果 provider stream 无法续传、tool 又非幂等,semi-durable recovery 的 最小安全 checkpoint 应由 harness、tool author 还是业务应用定义?
- 当 kernel 刻意不内置 permission system,extension ecosystem 需要怎样的 capability manifest、签名与审计格式,才能避免“可组合”变成“不可治理”?