07 SDK 和 RPC 学习记录
本节目标
本节是进阶集成章节。前面 01 到 06 看的是 Pi 内部怎么运行;07 看的是同一套 agent 能力如何通过 CLI、interactive、SDK 和 RPC 暴露给不同宿主。
重点问题:
- SDK 和 CLI 的关系是什么。
createAgentSession()创建的到底是什么。- 为什么 SDK 支持
SessionManager.inMemory()。 - 为什么长期宿主要用
AgentSessionRuntime。 - 为什么要拆
createAgentSessionServices()和createAgentSessionFromServices()。 - Runtime replacement 时发生了什么。
- RPC 命令如何映射到
AgentSession/AgentSessionRuntime。 - 为什么 RPC 模式比 print/json 更像“远程控制一个长期 agent”。
我们怎么学的
- 先看
packages/coding-agent/src/core/sdk.ts,理解createAgentSession()如何组装完整产品层 session。 - 看
packages/coding-agent/docs/sdk.md,确认 SDK 的对外使用方式。 - 看
packages/coding-agent/src/core/agent-session-runtime.ts,理解长期宿主如何替换当前 session。 - 看
packages/coding-agent/src/core/agent-session-services.ts,理解 services 和 session 创建为什么拆开。 - 看
packages/coding-agent/src/main.ts,确认 CLI 也是通过 runtime factory 创建会话。 - 看
packages/coding-agent/src/modes/rpc/rpc-types.ts,理解 JSONL 协议形状。 - 看
packages/coding-agent/src/modes/rpc/rpc-mode.ts和rpc-client.ts,理解外部进程如何控制 Pi。
总体图
SDK
= 代码里直接 import createAgentSession / createAgentSessionRuntime
AgentSessionRuntime
= 长期宿主,负责当前 session replacement
RPC
= 把 AgentSessionRuntime 暴露成 JSONL 长连接协议
换一个角度:
CLI
= 一个宿主
interactive mode
= TUI 宿主
print/json mode
= 一次性输出宿主
RPC mode
= 长期进程通信宿主
SDK
= 给你自己写宿主用的入口
所以 07 的核心不是“又多了一个 API”,而是:
Pi 的产品级 AgentSession 可以被不同宿主复用。
Mermaid 源码:
flowchart TD
CLI[CLI print/json] --> S[AgentSession]
TUI[Interactive TUI] --> Runtime[AgentSessionRuntime]
SDK[SDK host] --> S
RPC[RPC mode] --> Runtime
Runtime --> S
S --> A[Agent]
A --> L[agent loop]
最短判断:
命令行一次性使用:print/json
终端交互:interactive
Node 应用内嵌:SDK
外部进程长期控制 Pi:RPC
SDK:创建完整 AgentSession
createAgentSession() 的定位是:
给外部代码创建一个完整的 Pi AgentSession
它不是只创建底层 Agent。
它会组装:
AuthStorage
ModelRegistry
SettingsManager
SessionManager
ResourceLoader
Agent
AgentSession
tools
extensions
sessions
compaction
event stream
主线:
createAgentSession(options)
-> resolve cwd / agentDir
-> create authStorage / modelRegistry
-> create settingsManager / sessionManager
-> resourceLoader.reload()
-> sessionManager.buildSessionContext()
-> restore model / thinkingLevel
-> create Agent
-> restore messages or append initial model/thinking entries
-> create AgentSession
-> return { session, extensionsResult, modelFallbackMessage }
这说明 SDK 复用的是我们前面学过的同一个 AgentSession。
所以 SDK 不是“轻量 wrapper”,而是 Pi 的产品级 agent runtime 入口。
CreateAgentSessionOptions
CreateAgentSessionOptions 允许外部宿主替换关键组件。
常见选项:
cwd / agentDir
= 控制项目和全局配置发现
authStorage / modelRegistry
= 控制鉴权和模型来源
model / thinkingLevel
= 控制初始模型状态
tools / excludeTools / noTools / customTools
= 控制工具能力
resourceLoader
= 控制 skills / prompts / extensions / context files
sessionManager
= 控制持久化或 in-memory session
settingsManager
= 控制配置来源
所以 SDK 有两种用法:
简单嵌入
= 什么都不传,用 Pi 默认发现和配置
深度集成
= 自己传 sessionManager、customTools、resourceLoader、modelRegistry
最小例子:
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
const { session } = await createAgentSession({
sessionManager: SessionManager.inMemory(),
});
session.subscribe((event) => {
if (event.type === "message_update") {
// handle stream event
}
});
await session.prompt("What files are in the current directory?");
SDK 创建 Agent 时接入的边界
createAgentSession() 创建底层 Agent 时,会塞入几个关键边界:
convertToLlm
= AgentMessage[] -> provider Message[]
streamFn
= 从 modelRegistry 拿 auth,再调用 streamSimple()
onPayload
= 触发 before_provider_request extension hook
onResponse
= 触发 after_provider_response extension hook
transformContext
= 触发 context extension hook
steeringMode / followUpMode
= 从 settings 读队列策略
这说明 06 学过的 extension hooks,在 SDK 模式里同样生效。
为什么支持 SessionManager.inMemory()
SDK 不一定只给 CLI 用,也可能用于:
测试
临时自动化
嵌入式 UI
子 agent
服务端 pipeline
如果总是写 JSONL,会带来问题:
测试不方便
= 每次都要清理文件,副作用更大
临时任务不需要持久化
= 比如一次性分析、一段 pipeline、一个子 agent
宿主可能有自己的存储
= web app / desktop app / 服务端系统可能想把历史放数据库
隐私和隔离
= 有些集成不希望默认落盘
所以 SDK 允许:
sessionManager: SessionManager.inMemory()
这表示:
我需要完整 AgentSession 能力;
但这次历史只存在内存里,不写 Pi 默认 session 文件。
这也说明 SDK 的设计目标不是“复刻 CLI”,而是让外部宿主选择自己的存储、工具、模型和资源加载方式。
AgentSessionRuntime:长期宿主管当前 session
createAgentSession() 适合创建一个 session。
但有些宿主需要替换当前 session:
/new
/resume
/fork
/clone
/import
switch cwd-bound session
reload resources
这些操作会让当前 AgentSession 失效,换成新的 AgentSession。
所以需要:
AgentSessionRuntime
它的边界:
AgentSession
= 当前这一个会话的 agent 控制器
AgentSessionRuntime
= 长期宿主,负责持有“当前 session”,并在 new/fork/switch/import 时替换它
为什么不能只清空 messages:
AgentSession 绑定了很多当前 session 状态:
sessionManager
agent.state.messages
extensionRunner
resourceLoader
settingsManager
modelRegistry
cwd
event listeners
UI / extension bindings
换 session 时,这些东西可能都要重新创建或重新绑定。
Runtime replacement 固定流程
大多数 replacement 都是这个套路:
1. emit session_before_switch / session_before_fork
2. 如果 extension cancel,停止
3. 创建目标 SessionManager
4. teardownCurrent()
-> emit session_shutdown
-> beforeSessionInvalidate()
-> old session.dispose()
5. createRuntime(...)
-> createAgentSessionServices()
-> createAgentSessionFromServices()
6. apply(result)
-> this._session = result.session
-> this._services = result.services
-> diagnostics / fallback message 更新
7. finishSessionReplacement()
-> rebindSession(newSession)
-> withSession(new ctx)
这就是 Runtime 比 AgentSession 多管的东西:
session replacement 生命周期
services 和 session 为什么拆开
源码里有两个函数:
createAgentSessionServices()
= 创建 cwd-bound runtime services,不创建 AgentSession
createAgentSessionFromServices()
= 用已经创建好的 services 创建 AgentSession
拆开的原因是宿主在创建 services 后,可能还要基于这些 services 决定 session 参数。
例如 CLI 在 main.ts 里会:
createAgentSessionServices()
-> collect diagnostics
-> resolveModelScope()
-> buildSessionOptions()
-> 处理 --api-key
-> createAgentSessionFromServices()
中间需要读取:
settings
resourceLoader diagnostics
extension flags
enabled models
scoped models
tools / noTools / excludeTools
thinkingLevel
所以拆开不是多余,而是给 CLI/RPC/interactive 这类宿主一个中间决策点。
Runtime 和 extension stale
06 里我们讲过 extension ctx 的 stale 检查。
Runtime replacement 正是 stale 的来源之一。
当旧 session 被替换:
teardownCurrent()
-> old session.dispose()
-> extension ctx invalidate
然后宿主通过:
rebindSession(newSession)
重新绑定 UI、订阅事件、extension UI context 等。
长期宿主要记住:
runtime.session 可能变化
旧 session 的事件订阅不自动跟着新 session
旧 extension ctx 不应该继续使用
SDK 文档也强调:
event subscriptions are attached to a specific AgentSession
所以 session replacement 后要重新订阅。
RPC:JSONL 长期控制协议
RPC 的定位:
外部进程不直接 import SDK;
而是启动 pi --mode rpc;
通过 stdin/stdout JSONL 发送命令、接收事件。
协议形状:
stdin
-> RpcCommand JSON line
stdout
-> RpcResponse JSON line
-> AgentSessionEvent JSON line
-> extension_ui_request JSON line
RpcCommand 覆盖:
Prompting
= prompt / steer / follow_up / abort / new_session
State
= get_state
Model
= set_model / cycle_model / get_available_models
Thinking
= set_thinking_level / cycle_thinking_level
Queue modes
= set_steering_mode / set_follow_up_mode
Compaction
= compact / set_auto_compaction
Retry
= set_auto_retry / abort_retry
Bash
= bash / abort_bash
Session
= get_session_stats / export_html / switch_session / fork / clone
Messages
= get_messages
Commands
= get_commands
这些命令基本都映射回:
AgentSession 方法
AgentSessionRuntime 方法
RPC 时序 Mermaid 源码:
sequenceDiagram
participant Client
participant RPC as RPC mode
participant Runtime
participant Session as AgentSession
Client->>RPC: prompt command
RPC->>Runtime: current session
RPC->>Session: prompt(message)
RPC-->>Client: response accepted
Session-->>RPC: agent events
RPC-->>Client: event lines
rpc-mode.ts 主线
runRpcMode(runtimeHost) 做的事:
takeOverStdout()
-> 创建 output(),保证 stdout 只输出 JSONL
-> 创建 RPC extension UI context
-> rebindSession()
-> attach stdin JSONL reader
-> handleCommand(command)
-> 输出 response
-> 同时把 session event 流输出到 stdout
rebindSession() 很重要:
session = runtimeHost.session
session.bindExtensions({ mode: "rpc", uiContext, commandContextActions })
unsubscribe old event listener
subscribe new session events
一旦:
new_session
switch_session
fork
clone
换了 session,RPC 必须重新 bind 当前 session。
prompt command 为什么特殊
RPC 的 prompt command 不像 get_state 那样同步返回完整结果。
它的逻辑是:
收到 prompt command
-> session.prompt(...) 异步跑
-> preflightResult(true) 后先输出 success response
-> 后续 message_update / tool_execution_update / agent_end 等事件继续流式输出
所以 RPC 的 prompt response 只代表:
这个 prompt 被接受了
不代表:
模型已经回答完了
真正完成要看事件流里的:
agent_end
对外部 host 来说:
response success
= command 通过 preflight,agent run 已经开始或已排队
agent_end
= 这一轮 agent run 结束
这和 RpcClient.promptAndWait() 对应:
collectEvents()
-> prompt()
-> 等 agent_end
extension UI 在 RPC 中如何处理
RPC 没有 TUI,但 extension 可能调用:
ctx.ui.select()
ctx.ui.confirm()
ctx.ui.input()
ctx.ui.notify()
ctx.ui.setStatus()
ctx.ui.setWidget()
所以 RPC mode 把这些变成:
extension_ui_request
发给外部 host。
host 再回:
extension_ui_response
这意味着 RPC 不是“没有 UI”,而是:
UI 能力由外部宿主实现。
但有些 TUI 专属能力 RPC 不支持,例如:
custom component
footer/header factory
autocomplete provider
theme switching
custom editor component
这些需要真实 TUI 环境。
RpcClient:typed wrapper
RpcClient 是 RPC 的 typed wrapper。
它做的事:
spawn node dist/cli.js --mode rpc
-> 给每个 command 加 id
-> 写 stdin JSONL
-> 读 stdout JSONL
-> response 用 id 匹配 pending request
-> 非 response 当成 AgentEvent 分发给 listeners
外部代码可以这样:
const client = new RpcClient({ cwd });
await client.start();
client.onEvent((event) => {
// message_update / tool_execution_update / agent_end ...
});
await client.prompt("Read README.md");
await client.waitForIdle();
也可以用:
await client.promptAndWait("Read README.md");
它内部就是:
collectEvents()
-> prompt()
-> 等 agent_end
为什么 RPC 是长期 agent server
print/json 更像一次性运行:
启动 pi
-> 创建 session/runtime
-> 接收一个 prompt
-> 输出文本或 JSON event
-> 跑完退出
它主要服务:
pi -p "..."
pi --mode json -p "..."
即使 json 会流式输出事件,它仍然围绕一次 prompt 生命周期。
RPC 是长期进程:
启动 pi --mode rpc
-> 保持进程常驻
-> stdin 持续接收多个 command
-> stdout 持续输出 response + events
-> session 可以被远程切换/替换
-> extension UI 请求也走协议往返
外部程序不是只拿一次输出,而是在控制一个活着的 agent runtime。
关键差异:
print/json
= 一次运行的输出模式
RPC
= 长期 agent server / remote control protocol
尤其是这些行为有长期控制特征:
streaming 中发 steer / follow_up
切 session 后 rebind
extension UI request / response
用 id 匹配多次 command response
持续订阅 AgentSessionEvent
07 能力对照
| 层 | 本质 | 典型使用者 | 关键文件 |
| --- | --- | --- | --- |
| SDK | 直接嵌入 Pi AgentSession | Node 应用、测试、自定义 UI | core/sdk.ts |
| AgentSessionRuntime | 长期宿主的当前 session 管理器 | interactive / RPC / 自定义长期宿主 | core/agent-session-runtime.ts |
| Services split | 创建 cwd-bound services,再决定 session options | CLI / RPC / interactive | core/agent-session-services.ts |
| RPC mode | JSONL agent server | 外部进程、编辑器、桌面应用 | modes/rpc/rpc-mode.ts |
| RpcClient | typed process wrapper | Node 集成方 | modes/rpc/rpc-client.ts |
集成方式决策表
| 场景 | 选择 | 原因 | 完成信号 |
| --- | --- | --- | --- |
| 命令行一次性使用 | print/json | 启动一次、处理一个 prompt、输出后退出 | 进程退出或 agent_end |
| 终端交互 | interactive | 需要 TUI、快捷键、session replacement | UI 事件流 |
| Node 应用内嵌 | SDK | 同进程直接创建 AgentSession,可替换 storage/tools/resources | agent_end 或宿主订阅 |
| 外部进程长期控制 Pi | RPC | JSONL 长连接,远程 command、events、extension UI request | agent_end,不是 prompt response |
常见误解
- CLI、interactive、SDK、RPC 不是四套 agent 核心;最终都复用
AgentSession,再进入Agent和 agent loop。 - SDK 不是 RPC client。SDK 是同进程直接嵌入,RPC 是跨进程 JSONL 协议。
- RPC 不是 json mode。json mode 偏一次性 prompt 输出;RPC 是长期 agent server。
- prompt response 不是最终回答。它只说明 command accepted,最终完成要等事件流里的
agent_end。 - session replacement 后,旧 session 的事件订阅和 extension ctx 不能继续当成当前 session。
自测题
- SDK 创建的是
Agent还是完整AgentSession? - CLI、interactive、SDK、RPC 最后共用的核心对象是什么?
- 什么时候选 SDK,什么时候选 RPC?
- 为什么 RPC 比 print/json 更像长期 agent server?
- 为什么 prompt response 不代表最终回答?
本节完成度
本节已经完成:
- 理解 SDK 是创建完整
AgentSession,不是只创建底层Agent。 - 理解 SDK 选项如何允许外部宿主替换存储、模型、工具和资源加载。
- 理解为什么
SessionManager.inMemory()对测试和嵌入式宿主重要。 - 理解
AgentSessionRuntime管 session replacement,而不是 agent loop。 - 理解 services/session 创建拆分给宿主中间决策点。
- 理解 RPC 的 JSONL command/response/event 协议。
- 理解 RPC prompt success 只代表 accepted,完成要等
agent_end。 - 理解 RPC extension UI request/response 的宿主化设计。
这一节结束后,Pi agent 的主线已经闭环:
CLI 入口
-> 模型接口
-> agent loop
-> tools
-> session
-> extensions
-> SDK/RPC
Source: 07-sdk-rpc-notes.md