# 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”。

## 我们怎么学的

1. 先看 `packages/coding-agent/src/core/sdk.ts`，理解 `createAgentSession()` 如何组装完整产品层 session。
2. 看 `packages/coding-agent/docs/sdk.md`，确认 SDK 的对外使用方式。
3. 看 `packages/coding-agent/src/core/agent-session-runtime.ts`，理解长期宿主如何替换当前 session。
4. 看 `packages/coding-agent/src/core/agent-session-services.ts`，理解 services 和 session 创建为什么拆开。
5. 看 `packages/coding-agent/src/main.ts`，确认 CLI 也是通过 runtime factory 创建会话。
6. 看 `packages/coding-agent/src/modes/rpc/rpc-types.ts`，理解 JSONL 协议形状。
7. 看 `packages/coding-agent/src/modes/rpc/rpc-mode.ts` 和 `rpc-client.ts`，理解外部进程如何控制 Pi。

## 总体图

```text
SDK
= 代码里直接 import createAgentSession / createAgentSessionRuntime

AgentSessionRuntime
= 长期宿主，负责当前 session replacement

RPC
= 把 AgentSessionRuntime 暴露成 JSONL 长连接协议
```

换一个角度：

```text
CLI
= 一个宿主

interactive mode
= TUI 宿主

print/json mode
= 一次性输出宿主

RPC mode
= 长期进程通信宿主

SDK
= 给你自己写宿主用的入口
```

所以 07 的核心不是“又多了一个 API”，而是：

```text
Pi 的产品级 AgentSession 可以被不同宿主复用。
```

Mermaid 源码：

```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]
```

最短判断：

```text
命令行一次性使用：print/json
终端交互：interactive
Node 应用内嵌：SDK
外部进程长期控制 Pi：RPC
```

## SDK：创建完整 AgentSession

`createAgentSession()` 的定位是：

```text
给外部代码创建一个完整的 Pi AgentSession
```

它不是只创建底层 `Agent`。

它会组装：

```text
AuthStorage
ModelRegistry
SettingsManager
SessionManager
ResourceLoader
Agent
AgentSession
tools
extensions
sessions
compaction
event stream
```

主线：

```text
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` 允许外部宿主替换关键组件。

常见选项：

```text
cwd / agentDir
= 控制项目和全局配置发现

authStorage / modelRegistry
= 控制鉴权和模型来源

model / thinkingLevel
= 控制初始模型状态

tools / excludeTools / noTools / customTools
= 控制工具能力

resourceLoader
= 控制 skills / prompts / extensions / context files

sessionManager
= 控制持久化或 in-memory session

settingsManager
= 控制配置来源
```

所以 SDK 有两种用法：

```text
简单嵌入
= 什么都不传，用 Pi 默认发现和配置

深度集成
= 自己传 sessionManager、customTools、resourceLoader、modelRegistry
```

最小例子：

```ts
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` 时，会塞入几个关键边界：

```text
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 用，也可能用于：

```text
测试
临时自动化
嵌入式 UI
子 agent
服务端 pipeline
```

如果总是写 JSONL，会带来问题：

```text
测试不方便
= 每次都要清理文件，副作用更大

临时任务不需要持久化
= 比如一次性分析、一段 pipeline、一个子 agent

宿主可能有自己的存储
= web app / desktop app / 服务端系统可能想把历史放数据库

隐私和隔离
= 有些集成不希望默认落盘
```

所以 SDK 允许：

```ts
sessionManager: SessionManager.inMemory()
```

这表示：

```text
我需要完整 AgentSession 能力；
但这次历史只存在内存里，不写 Pi 默认 session 文件。
```

这也说明 SDK 的设计目标不是“复刻 CLI”，而是让外部宿主选择自己的存储、工具、模型和资源加载方式。

## AgentSessionRuntime：长期宿主管当前 session

`createAgentSession()` 适合创建一个 session。

但有些宿主需要替换当前 session：

```text
/new
/resume
/fork
/clone
/import
switch cwd-bound session
reload resources
```

这些操作会让当前 `AgentSession` 失效，换成新的 `AgentSession`。

所以需要：

```text
AgentSessionRuntime
```

它的边界：

```text
AgentSession
= 当前这一个会话的 agent 控制器

AgentSessionRuntime
= 长期宿主，负责持有“当前 session”，并在 new/fork/switch/import 时替换它
```

为什么不能只清空 messages：

```text
AgentSession 绑定了很多当前 session 状态：

sessionManager
agent.state.messages
extensionRunner
resourceLoader
settingsManager
modelRegistry
cwd
event listeners
UI / extension bindings
```

换 session 时，这些东西可能都要重新创建或重新绑定。

## Runtime replacement 固定流程

大多数 replacement 都是这个套路：

```text
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` 多管的东西：

```text
session replacement 生命周期
```

## services 和 session 为什么拆开

源码里有两个函数：

```text
createAgentSessionServices()
= 创建 cwd-bound runtime services，不创建 AgentSession

createAgentSessionFromServices()
= 用已经创建好的 services 创建 AgentSession
```

拆开的原因是宿主在创建 services 后，可能还要基于这些 services 决定 session 参数。

例如 CLI 在 `main.ts` 里会：

```text
createAgentSessionServices()
-> collect diagnostics
-> resolveModelScope()
-> buildSessionOptions()
-> 处理 --api-key
-> createAgentSessionFromServices()
```

中间需要读取：

```text
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 被替换：

```text
teardownCurrent()
-> old session.dispose()
-> extension ctx invalidate
```

然后宿主通过：

```text
rebindSession(newSession)
```

重新绑定 UI、订阅事件、extension UI context 等。

长期宿主要记住：

```text
runtime.session 可能变化
旧 session 的事件订阅不自动跟着新 session
旧 extension ctx 不应该继续使用
```

SDK 文档也强调：

```text
event subscriptions are attached to a specific AgentSession
```

所以 session replacement 后要重新订阅。

## RPC：JSONL 长期控制协议

RPC 的定位：

```text
外部进程不直接 import SDK；
而是启动 pi --mode rpc；
通过 stdin/stdout JSONL 发送命令、接收事件。
```

协议形状：

```text
stdin
-> RpcCommand JSON line

stdout
-> RpcResponse JSON line
-> AgentSessionEvent JSON line
-> extension_ui_request JSON line
```

`RpcCommand` 覆盖：

```text
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
```

这些命令基本都映射回：

```text
AgentSession 方法
AgentSessionRuntime 方法
```

RPC 时序 Mermaid 源码：

```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)` 做的事：

```text
takeOverStdout()
-> 创建 output()，保证 stdout 只输出 JSONL
-> 创建 RPC extension UI context
-> rebindSession()
-> attach stdin JSONL reader
-> handleCommand(command)
-> 输出 response
-> 同时把 session event 流输出到 stdout
```

`rebindSession()` 很重要：

```text
session = runtimeHost.session
session.bindExtensions({ mode: "rpc", uiContext, commandContextActions })
unsubscribe old event listener
subscribe new session events
```

一旦：

```text
new_session
switch_session
fork
clone
```

换了 session，RPC 必须重新 bind 当前 session。

## prompt command 为什么特殊

RPC 的 `prompt` command 不像 `get_state` 那样同步返回完整结果。

它的逻辑是：

```text
收到 prompt command
-> session.prompt(...) 异步跑
-> preflightResult(true) 后先输出 success response
-> 后续 message_update / tool_execution_update / agent_end 等事件继续流式输出
```

所以 RPC 的 prompt response 只代表：

```text
这个 prompt 被接受了
```

不代表：

```text
模型已经回答完了
```

真正完成要看事件流里的：

```text
agent_end
```

对外部 host 来说：

```text
response success
= command 通过 preflight，agent run 已经开始或已排队

agent_end
= 这一轮 agent run 结束
```

这和 `RpcClient.promptAndWait()` 对应：

```text
collectEvents()
-> prompt()
-> 等 agent_end
```

## extension UI 在 RPC 中如何处理

RPC 没有 TUI，但 extension 可能调用：

```text
ctx.ui.select()
ctx.ui.confirm()
ctx.ui.input()
ctx.ui.notify()
ctx.ui.setStatus()
ctx.ui.setWidget()
```

所以 RPC mode 把这些变成：

```text
extension_ui_request
```

发给外部 host。

host 再回：

```text
extension_ui_response
```

这意味着 RPC 不是“没有 UI”，而是：

```text
UI 能力由外部宿主实现。
```

但有些 TUI 专属能力 RPC 不支持，例如：

```text
custom component
footer/header factory
autocomplete provider
theme switching
custom editor component
```

这些需要真实 TUI 环境。

## RpcClient：typed wrapper

`RpcClient` 是 RPC 的 typed wrapper。

它做的事：

```text
spawn node dist/cli.js --mode rpc
-> 给每个 command 加 id
-> 写 stdin JSONL
-> 读 stdout JSONL
-> response 用 id 匹配 pending request
-> 非 response 当成 AgentEvent 分发给 listeners
```

外部代码可以这样：

```ts
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();
```

也可以用：

```ts
await client.promptAndWait("Read README.md");
```

它内部就是：

```text
collectEvents()
-> prompt()
-> 等 agent_end
```

## 为什么 RPC 是长期 agent server

print/json 更像一次性运行：

```text
启动 pi
-> 创建 session/runtime
-> 接收一个 prompt
-> 输出文本或 JSON event
-> 跑完退出
```

它主要服务：

```text
pi -p "..."
pi --mode json -p "..."
```

即使 json 会流式输出事件，它仍然围绕一次 prompt 生命周期。

RPC 是长期进程：

```text
启动 pi --mode rpc
-> 保持进程常驻
-> stdin 持续接收多个 command
-> stdout 持续输出 response + events
-> session 可以被远程切换/替换
-> extension UI 请求也走协议往返
```

外部程序不是只拿一次输出，而是在控制一个活着的 agent runtime。

关键差异：

```text
print/json
= 一次运行的输出模式

RPC
= 长期 agent server / remote control protocol
```

尤其是这些行为有长期控制特征：

```text
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 的主线已经闭环：

```text
CLI 入口
-> 模型接口
-> agent loop
-> tools
-> session
-> extensions
-> SDK/RPC
```
