PiAgent 12|无头 SDK:从 Agent 到 AgentSession#
脱离 TUI,把 Pi 的循环、资源、会话和事件装进自己的 Node 服务
把 Pi 嵌入应用时,最容易犯的错误是只看到一个 Agent 类,然后把模型、工具、会话文件、UI 和业务状态全部塞进去。Pi 官方 SDK 的价值,恰恰在于把这些职责拆开,让宿主可以按场景替换基础设施。
1. 三个对象,三种责任#
Agent:让循环跑起来#
核心 Agent 持有 transcript、工具、模型流函数和生命周期。它知道如何 prompt、continue、steer、follow-up、abort 和等待空闲,但它本身不应该决定会话文件如何持久化,也不应该知道 HTTP 响应怎么写。
AgentSession:产品能力的装配层#
AgentSession 在核心 Agent 之上增加模型与 thinking level 控制、会话管理、上下文压缩、扩展、工具运行时和事件订阅。它把 Agent 的运行事件连接到 SessionManager,并提供应用更容易使用的生命周期 API。
SessionManager:管理可恢复历史#
SessionManager 负责 JSONL 文件、entry 父子关系、当前 leaf、树遍历、标签、回退和分支。它不负责模型推理,也不应该把 UI 的瞬时状态塞进每个消息节点。
可以把三者想成一条装配线:
Agent 产生运行事件
→ AgentSession 接上模型、扩展和产品策略
→ SessionManager 把需要恢复的事实落盘2. 一个最小的 headless 会话#
import {
createAgentSession,
ModelRuntime,
SessionManager,
} from "@earendil-works/pi-coding-agent";
const modelRuntime = await ModelRuntime.create();
const model = (await modelRuntime.getAvailable())[0];
if (!model) throw new Error("没有可用模型");
const { session } = await createAgentSession({
model,
modelRuntime,
sessionManager: SessionManager.inMemory(),
});
const off = session.subscribe((event) => {
if (
event.type === "message_update" &&
event.assistantMessageEvent.type === "text_delta"
) {
process.stdout.write(event.assistantMessageEvent.delta);
}
});
try {
await session.prompt("概括当前项目的目录结构");
} finally {
off();
session.dispose();
}使用 SessionManager.inMemory() 的含义是运行时不会把会话历史自动写入本地文件,适合测试或由应用完全接管持久化的场景;它不等于会话自动具备跨请求恢复能力。
3. 自定义 ResourceLoader 与工具#
垂直 Agent 常常需要替换系统提示词和工具集合:
const loader = new DefaultResourceLoader({
cwd: process.cwd(),
agentDir: getAgentDir(),
systemPromptOverride: () =>
"你是企业数据分析助手。所有数字必须来自 query_data 工具。",
extensionFactories: [
(pi) => {
pi.registerTool(queryDataTool);
},
],
});
await loader.reload();
const { session } = await createAgentSession({
model,
modelRuntime,
resourceLoader: loader,
sessionManager: SessionManager.inMemory(),
});这里的关键不是某个工厂函数的写法,而是装配边界:系统提示词由资源层提供,工具由扩展层注册,会话存储由 SessionManager 决定,核心 Agent 不需要知道 DataAgent 的业务身份。
4. 接入 Web 服务的最小链路#
一个 Node 服务可以把 Session 接到 HTTP/SSE:
浏览器 POST /chat
→ 服务端 session.prompt(message)
→ session.subscribe 收到 Agent 事件
→ translateEvent 转成公共事件
→ SSE 写回浏览器
→ prompt resolve / abort / errorapp.post("/chat", async (req, res) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
const off = session.subscribe((event) => {
const payload = translateEvent(event);
if (payload) res.write(`data: ${JSON.stringify(payload)}\n\n`);
});
const onClose = () => session.abort();
req.on("close", onClose);
try {
await session.prompt(req.body?.message ?? "");
res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
} catch (error) {
res.write(`data: ${JSON.stringify({ type: "error" })}\n\n`);
} finally {
req.off("close", onClose);
off();
res.end();
}
});Pi SDK 提供事件和会话能力,但 HTTP/SSE 桥接是宿主服务的职责。服务端还要处理并发、身份验证、请求超时、错误脱敏和一个用户多个 Session 的隔离;不能把单个全局 Session 直接暴露给所有请求。
5. RPC 与进程内 SDK 的选择#
如果宿主本身是 Node,进程内 SDK 通常控制力最高:可以直接访问 AgentSession、扩展和模型状态。如果宿主是 Python、Go 或 Java,官方 RPC 模式提供了另一条边界:启动 pi --mode rpc,通过 stdin 写入 JSON 命令,从 stdout 读取 JSONL 事件。
宿主进程
⇄ JSONL stdin/stdout
pi --mode rpc
→ Agent Loop / tools / eventsRPC 的代价是子进程管理和进程间通信,但它把语言依赖收敛到 JSON 协议。无论选哪条路径,都要把 Session ID、取消、错误、退出和重启语义设计清楚。
6. 资源释放与请求边界#
一个 Session 可以跨多个 prompt 复用,但不能跨不相关用户复用。每个请求需要明确:
- 谁拥有这个 Session。
- 请求是否可以在已有运行中到达。
- 客户端断开时调用哪个
abort()。 - prompt 结束后是否继续保留 Session。
- 何时
dispose(),由谁负责。
服务端常见的安全边界是“用户 → Session → 运行”。不要把 session.abort() 挂在全局变量上,也不要在请求异常时无条件 dispose 其他用户的 Session。
常见误区
- 以为
Agent自动包含数据库持久化、用户隔离和 Web 协议。 - 一个全局 Session 服务所有用户,造成上下文串线。
- 使用
inMemory()后却期待进程重启能恢复历史。 - 把内部事件、工具参数和异常堆栈原样发送给浏览器。
- 客户端断开后不调用 abort,留下后台模型流和工具任务。
小结
无头接入的核心是依赖装配,而不是把 TUI 删除。Agent 提供循环,AgentSession 提供产品生命周期,SessionManager 提供可恢复历史,ResourceLoader 和扩展提供业务能力,宿主再负责 HTTP、用户隔离和公共事件协议。层次分清,Pi 才能从终端工具变成可维护的应用组件。
源码定位
- SDK 入口:
packages/coding-agent/src/sdk.ts - AgentSession:
packages/coding-agent/src/agent-session.ts - SessionManager:
packages/coding-agent/src/core/session-manager.ts - RPC:
packages/coding-agent/src/modes/rpc/ - 资源加载:
packages/coding-agent/src/core/resource-loader.ts