一个能够调用工具的 Agent Core 并不复杂。最小实现只需要维护消息、请求模型、执行 Tool Call,再把 Tool Result 放回下一次请求。
真正使 Agent Framework 迅速膨胀的部分,通常发生在 Loop 之外。
Coding Agent 很快会增加文件系统工具、Shell、MCP、Skill、权限控制、模型切换、Session、Compaction、命令、UI、遥测和 SubAgent。每增加一项能力,都有两个实现选择:继续修改 Agent Core,或者提供一个稳定的扩展面,让外部模块接入。
Pi 选择了后者。它仍然保留一个相对集中的 Agent Runtime,同时通过 Extension 将大量产品能力放到核心之外。
这篇文章讨论的重点不是 Pi 有哪些插件 API,而是一个更基础的框架设计问题:哪些能力应该进入 Agent Core,哪些能力应该通过 Harness 扩展。
从 Tool Loop 到不断增长的 Core
单元一已经得到一个较稳定的 Agent Loop 边界:
Context
↓
Model
↓
Tool
↓
Input Delivery
↓
Stop Condition
这套结构能够解释一次 Agent 工作如何推进,但无法承载完整产品的所有能力。
以权限控制为例。最直接的实现是在每个 Tool 执行前增加判断:
if (!permission.canExecute(toolCall)) {
return deniedResult
}
return executeTool(toolCall)
随后又需要审计:
telemetry.record(toolCall)
再增加 Hook:
toolCall = await hooks.beforeToolCall(toolCall)
再增加远程执行:
return sandbox.execute(toolCall)
如果所有功能都沿这种方式进入主执行函数,Agent Loop 会逐渐成为整个系统的集成中心。任何功能都能修改它,任何修改也都可能影响其他功能。
这里出现了 Harness 的第一个职责:
Core 定义稳定的执行语义,Harness 负责把外部能力组织到这些语义边界上。
扩展系统的价值因此不在于“允许第三方写插件”,而在于控制 Core 的增长速度。
Pi 的 Extension 选择了稳定 Core + 扩展入口
Pi 将自身定位为 minimal terminal coding harness。默认 Coding Agent 只提供少量基础工具,更多行为通过 Extensions、Skills、Prompt Templates 和 Packages 增加。[1][2]
Extension 可以注册 Tool:
pi.registerTool(...)
也可以监听生命周期:
pi.on('tool_call', ...)
pi.on('context', ...)
pi.on('agent_end', ...)
还可以注册 Command、Shortcut、Provider,或者向运行中的 Session 注入消息。[1][3]
这几类 API 表面上差异很大,实际上可以归入两个方向。
第一类是 Contribution:
Extension
↓
向 Runtime 注册一项能力
Tool
Command
Provider
Renderer
...
第二类是 Hook / Interception:
Runtime lifecycle
↓
Extension observes / modifies / blocks
前者扩展“系统里有什么”,后者扩展“系统运行到某个边界时发生什么”。
因此 Pi Extension 的核心结构可以抽象为:
Pi Runtime
│
┌──────────────┴──────────────┐
│ │
Contributions Hooks
│ │
Tool / Command / ... lifecycle events
│ │
└──────────── Extension ──────┘
这种模型比单独设计 ToolPlugin、CommandPlugin、HookPlugin 更容易形成统一的开发模型。Extension 本身拥有闭包状态,同一扩展中的 Tool、Hook 和 Command 可以直接共享状态。
Pi 曾经将 Hook 和自定义 Tool 分开,后来统一到 Extension,这种演进也反映了同一个方向:扩展单元应该围绕“一个功能模块”组织,而不是围绕注册类型组织。[4]
ExtensionAPI 是边界,不是容器本身
插件架构经常出现一个误区:为了让插件“什么都能做”,直接把整个 Application 或内部对象暴露出去。
短期非常灵活,长期则会形成事实上的内部 API。插件可以访问任何实现细节,Core 也就失去了重构空间。
Pi 采用 ExtensionAPI 作为插件入口。扩展通过它注册能力、发送消息、读取或修改受控状态。[1][3]
可以把关系理解成:
Extension
│
▼
ExtensionAPI
│
▼
Runtime / Session / UI / Registry
API 的意义在于规定插件能够影响哪些系统边界。
例如:
registerTool()
允许 Extension 增加模型可调用能力。
on('tool_call')
允许 Extension 在工具调用边界参与控制。
sendMessage()
允许 Extension 将新的输入送入 Agent Runtime。
这些能力很强,但调用仍然经过 Harness 规定的入口。Core 内部的状态结构可以继续演化。
因此,扩展 API 更值得关注的是能力边界是否稳定,方法数量只是表层形态。
Hook 为什么不能只理解成 Observer
pi.on(...) 很容易被理解成 Event Bus,但 Pi 的部分 Hook 可以修改甚至阻止运行行为。
例如 Tool Call Hook 可以检查、修改或拒绝调用,Context Hook 可以调整模型即将看到的消息。[1]
这类 Hook 更接近 Middleware / Interceptor:
Runtime operation
↓
Extension A
↓
Extension B
↓
Core behavior
Observer 的基本语义是观察已经发生的事实:
event happened
↓
listener notified
Interceptor 则处于行为链内部:
operation requested
↓
interceptor
↓
modify / block / continue
这个区别会直接影响插件系统的复杂度。
纯 Observer 只需要考虑通知和异常隔离。Interceptor 还需要定义顺序、短路、错误传播、异步行为,以及多个扩展同时修改结果时的组合语义。
Pi 当前的 tool_call 就明确规定:前序 Handler 对输入的修改会被后序 Handler 看到,Handler 可以通过返回值阻止调用。事件不是一个简单广播,而是执行协议的一部分。[1]
这也是 Harness 设计必须控制的另一条边界:不是所有事件都应该拥有修改权。
如果一个扩展点只需要观测,就应该保持只读;只有确实需要策略介入的执行边界才提供 Interception。
动态注册解决的是运行时扩展,生命周期仍然是另一件事
Pi Extension 可以在运行期间注册 Tool。注册完成后,新 Tool 可以进入当前 Session 的工具集合。
从使用角度看,这已经具备动态插件系统的一个重要能力:
Runtime
│
├── Tool A
├── Tool B
│
└── Extension loads
↓
Tool C
但动态“增加”能力只是问题的一半。
更复杂的问题发生在能力离开系统时。
假设一个 Extension 做了这些事情:
register Tool A
register Command B
subscribe tool_call
start timer
provide service
插件卸载时,系统需要完整撤销这些变化。
继续增加依赖以后问题进一步扩大:
Plugin A
└── provides Service X
Plugin B
├── depends on Service X
└── provides Service Y
Plugin C
└── depends on Service Y
如果 A 被卸载,B 是否继续运行?B 提供的 Y 是否仍然有效?C 怎么处理?当 A 再次加载后,B 和 C 是否应该重新启动?
这些问题已经超出 Extension Registry 本身,进入动态组合和生命周期管理。
Pi 的设计主要将 Extension 建立在一个相对稳定的 Runtime 周围。它适合表达:
stable runtime
+
dynamic contributions
+
lifecycle hooks
DeepSeek Harness 采用了更激进的结构:Agent Loop、Session、Tool Registry、LLM Adapter 等能力本身也由插件提供。[5] 此时,插件系统必须承担 Runtime 结构本身的动态变化。
这正是 Cordis 进入 DeepSeek Harness 的位置。
判断 Core 边界的一种方法
一个能力是否应该进入 Agent Core,可以从三个问题判断。
首先,它是否参与每一次 Agent Step 的基本语义。
例如 Context 构造、Model Request、Tool Result 回流和停止条件属于 Loop 的基本执行结构。完全移除这些能力以后,Agent Loop 本身就不存在。
其次,它是否拥有很多可替代实现。
持久化、Sandbox、Model Adapter、Telemetry 都存在大量实现选择。把某个实现固定在 Core 中,会让替换成本快速增加。
最后,它是否需要独立生命周期。
一个能力如果需要动态加载、卸载、隔离、重载或者依赖其他能力,它已经具有组件属性,更适合交给 Harness。
可以得到一个较实用的判断:
Agent Core
负责不可再分的执行语义
Harness
负责能力的组合、替换与生命周期
这条边界并不要求 Core 极端微小。过度拆分同样会增加理解成本。关键是避免让所有产品能力都通过修改同一条 Loop 才能接入。
Pi 展示了一种较保守、容易理解的方案:保留稳定 Runtime,通过 Extension 扩展 Contributions 和 Hooks。
DeepSeek Harness 的目标进一步扩大以后,需要解决另一类问题:当组件可以在运行时出现、消失,并相互依赖时,组合关系如何保持正确。
下一篇讨论 Cordis 背后的论文《A Programming Paradigm for Spatiotemporal Composability》。论文试图给这个问题建立一套更一般的模型。
参考资料
[1] Pi Extensions: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md
[2] Pi Coding Agent: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/README.md
[3] Pi Extension types: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts
[4] Pi Extension unified design discussion: https://github.com/earendil-works/pi/issues/454
[5] DeepSeek Harness Architecture: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md