7 min read教程

PiAgent 12|无头 SDK:从 Agent 到 AgentSession

脱离 TUI,把 Pi 的循环、资源、会话和事件装进自己的 Node 服务

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 的瞬时状态塞进每个消息节点。

可以把三者想成一条装配线:

text
Agent 产生运行事件
  → AgentSession 接上模型、扩展和产品策略
  → SessionManager 把需要恢复的事实落盘

2. 一个最小的 headless 会话#

ts
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 常常需要替换系统提示词和工具集合:

ts
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:

text
浏览器 POST /chat
  → 服务端 session.prompt(message)
  → session.subscribe 收到 Agent 事件
  → translateEvent 转成公共事件
  → SSE 写回浏览器
  → prompt resolve / abort / error
ts
app.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 事件。

text
宿主进程
  ⇄ JSONL stdin/stdout
pi --mode rpc
  → Agent Loop / tools / events

RPC 的代价是子进程管理和进程间通信,但它把语言依赖收敛到 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

相关文章