前面十二章都在"读源码理解 Pi"。这一章反过来:把 Pi 当库用—— 嵌进你自己的 Web/桌面/移动应用,或做成自动化流水线。 核心就三个入口:
createAgentSession、AgentSessionRuntime、三种 Run Mode。
一个能跑的 Agent,只要几行:
import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
const modelRuntime = await ModelRuntime.create();
const { session } = await createAgentSession({
sessionManager: SessionManager.inMemory(),
modelRuntime,
});
session.subscribe((event) => {
if (event.type === "message_update" &&
event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
});
await session.prompt("列出当前目录的文件");三个角色一眼看清:
ModelRuntime—— 认证与模型解析(第十章的Models在 coding-agent 里的实现)SessionManager—— 会话树存储(第十一章)AgentSession—— 一次会话的生命周期门面
ModelRuntime 的认证解析优先级和第十章一致,只是多了一层运行时覆盖:
- 运行时覆盖(
setRuntimeApiKey,不落盘) auth.json里的存储凭据(API key 或 OAuth token)- 环境变量(
ANTHROPIC_API_KEY等) - 兜底解析器(
models.json里自定义 Provider 的 key)
createAgentSession() 是构造单个 AgentSession 的工厂。不传参数时,它用 DefaultResourceLoader 做标准发现(扩展、技能、模板、上下文文件):
// 最小:全默认
const { session } = await createAgentSession();
// 定制:覆盖特定选项
const { session } = await createAgentSession({
model: myModel,
tools: ["read", "bash"],
sessionManager: SessionManager.inMemory(),
});AgentSession 把一次会话的所有操作收敛到一个对象上:
interface AgentSession {
// 发消息
prompt(text, options?): Promise<void>;
steer(text): Promise<void>; // 流式中插入(回想第三章 Steering)
followUp(text): Promise<void>; // 等 Agent 停下再发
// 订阅事件(返回取消订阅函数)
subscribe(listener): () => void;
// 模型控制
setModel(model): Promise<void>;
setThinkingLevel(level): void;
cycleModel(): Promise<...>;
// 状态只读访问
agent: Agent;
messages: AgentMessage[];
isStreaming: boolean;
// 树导航(文件内)与压缩
navigateTree(targetId, options?): Promise<...>;
compact(customInstructions?): Promise<CompactionResult>;
abort(): Promise<void>;
dispose(): void;
}一个容易踩的边界:prompt() 在流式进行中调用时,必须指定 streamingBehavior,否则会抛错——因为 Pi 不替你猜是要打断(steer)还是排队(followUp):
await session.prompt("停下,改做这个", { streamingBehavior: "steer" });
await session.prompt("做完后再查 X", { streamingBehavior: "followUp" });注意:新建/恢复/fork/import 这类"替换会话"的操作不在 AgentSession 上——它们在下一层 AgentSessionRuntime。
AgentSession 管一次会话。但真实应用需要"切换会话"——开新会话、恢复旧会话、fork。这些操作会把整个活跃会话换掉,并重建绑定到 cwd 的运行时状态(工具、资源)。这是 AgentSessionRuntime 的职责,也是内置 interactive / print / rpc 三种模式共用的那一层。
@startuml
skinparam backgroundColor transparent
rectangle "AgentSessionRuntime" as RT #FFF3E0 {
rectangle "runtime.session\n(当前 AgentSession)" as S #E8F5E9
}
rectangle "newSession()" as N
rectangle "switchSession(path)" as SW
rectangle "fork(entryId)" as F
rectangle "importFromJsonl()" as I
N --> RT
SW --> RT
F --> RT
I --> RT
note bottom of RT
这些操作后 runtime.session 会变成新对象
end note
@enduml最重要的一条使用纪律:事件订阅是绑在具体某个 AgentSession 上的。会话被替换后,旧订阅就失效了,必须重新订阅:
let session = runtime.session;
let unsubscribe = session.subscribe(() => { /* ... */ });
await runtime.newSession(); // runtime.session 变成新对象
unsubscribe(); // 取消旧订阅
session = runtime.session; // 抓新会话
unsubscribe = session.subscribe(() => { /* ... */ }); // 重新订阅同理,如果用了扩展,替换后要对新会话重新 runtime.session.bindExtensions(...)。运行时创建或替换失败会抛错,由调用方决定怎么处理。
AgentSessionRuntime 拥有这些替换操作:newSession()、switchSession()、fork()、通过 fork(entryId, { position: "at" }) 的 clone、以及 importFromJsonl()。
SDK 在 createAgentSession() 之上导出三种开箱即用的运行模式,覆盖绝大多数集成形态:
@startuml
skinparam backgroundColor transparent
rectangle "AgentSessionRuntime" as RT #FFF3E0
rectangle "InteractiveMode" as I #E8F5E9
rectangle "runPrintMode" as P #E3F2FD
rectangle "runRpcMode" as R #F3E5F5
RT --> I
RT --> P
RT --> R
note bottom of I: 完整 TUI\n编辑器+历史+全部命令
note bottom of P: 单发:发 prompt→输出→退出\n适合脚本/流水线
note bottom of R: JSON-RPC over stdio\n跨进程/跨语言集成
@enduml| 模式 | 入口 | 典型场景 |
|---|---|---|
| Interactive | new InteractiveMode(runtime, opts).run() |
做一个带 UI 的终端 Agent |
runPrintMode(runtime, opts) |
CI 脚本、批处理、一次性任务 | |
| RPC | runRpcMode(runtime) |
从别的语言/进程调 Pi |
三者都接同一个 runtime,所以你可以用同一套认证、会话、扩展配置驱动不同前端。Print 模式示例:
await runPrintMode(runtime, {
mode: "text",
initialMessage: "分析这个仓库",
initialImages: [],
messages: ["再补一句:只看 src/"],
});不想用 SDK 构建、只想子进程集成的话,也可以直接跑 CLI:pi --mode rpc --no-session。
| 选 SDK 当… | 选 RPC 当… |
|---|---|
| 想要类型安全 | 从其他语言集成 |
| 在同一个 Node 进程 | 想要进程隔离 |
| 需要直接访问 agent 状态 | 构建语言无关的客户端 |
| 想用代码定制工具/扩展 | — |
createAgentSession() 的选项覆盖了嵌入时几乎所有需求,按需取用:
| 需求 | 怎么做 |
|---|---|
| 只读 Agent | tools: ["read", "grep", "find", "ls"] |
| 禁用全部工具 | noTools: "all" |
| 加自定义工具 | defineTool(...) + customTools: [myTool] |
| 换 system prompt | DefaultResourceLoader({ systemPromptOverride }) |
| 内联扩展 | DefaultResourceLoader({ extensionFactories }) |
| 关掉压缩 | SettingsManager.inMemory({ compaction: { enabled: false } }) |
| 不落盘(测试) | SessionManager.inMemory() + SettingsManager.inMemory() |
| 临时 API key | modelRuntime.setRuntimeApiKey(provider, key) |
自定义工具的最小形态:
import { Type } from "typebox";
import { defineTool } from "@earendil-works/pi-coding-agent";
const statusTool = defineTool({
name: "status",
label: "Status",
description: "获取系统状态",
parameters: Type.Object({}),
execute: async () => ({
content: [{ type: "text", text: `Uptime: ${process.uptime()}s` }],
details: {},
}),
});
const { session } = await createAgentSession({
tools: ["read", "bash", "status"], // 用 tools 白名单时要带上自定义工具名
customTools: [statusTool],
});提示:传了
tools白名单时,自定义/扩展工具的名字也要写进去才会被启用。
- 能说出
ModelRuntime/SessionManager/AgentSession各管什么 - 知道流式中调
prompt()为何必须指定streamingBehavior - 理解为什么替换会话后必须重新
subscribe - 记得
AgentSessionRuntime而非AgentSession拥有会话替换 API - 能对号入座三种 Run Mode 的适用场景
- 知道 SDK 与 RPC 集成各自的取舍
上一章:第十二章 · Compaction 内部机制
下一章:第十四章 · 术语表 — 所有关键术语速查