Skip to content

Latest commit

 

History

History
252 lines (186 loc) · 8.49 KB

File metadata and controls

252 lines (186 loc) · 8.49 KB

第十三章 · SDK 与嵌入集成

前面十二章都在"读源码理解 Pi"。这一章反过来:把 Pi 当库用—— 嵌进你自己的 Web/桌面/移动应用,或做成自动化流水线。 核心就三个入口:createAgentSessionAgentSessionRuntime、三种 Run Mode。


13.1 最小可用示例

一个能跑的 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 的认证解析优先级和第十章一致,只是多了一层运行时覆盖:

  1. 运行时覆盖(setRuntimeApiKey,不落盘)
  2. auth.json 里的存储凭据(API key 或 OAuth token)
  3. 环境变量(ANTHROPIC_API_KEY 等)
  4. 兜底解析器(models.json 里自定义 Provider 的 key)

13.2 createAgentSession:一次会话的门面

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

13.3 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()

13.4 三种 Run Mode

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
Print runPrintMode(runtime, opts) CI 脚本、批处理、一次性任务
RPC runRpcMode(runtime) 从别的语言/进程调 Pi

三者都接同一个 runtime,所以你可以用同一套认证、会话、扩展配置驱动不同前端。Print 模式示例:

await runPrintMode(runtime, {
  mode: "text",
  initialMessage: "分析这个仓库",
  initialImages: [],
  messages: ["再补一句:只看 src/"],
});

SDK vs RPC:怎么选

不想用 SDK 构建、只想子进程集成的话,也可以直接跑 CLI:pi --mode rpc --no-session

选 SDK 当… 选 RPC 当…
想要类型安全 从其他语言集成
在同一个 Node 进程 想要进程隔离
需要直接访问 agent 状态 构建语言无关的客户端
想用代码定制工具/扩展

13.5 常见定制点速查

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 白名单时,自定义/扩展工具的名字也要写进去才会被启用。

13.6 本章检查清单

  • 能说出 ModelRuntime / SessionManager / AgentSession 各管什么
  • 知道流式中调 prompt() 为何必须指定 streamingBehavior
  • 理解为什么替换会话后必须重新 subscribe
  • 记得 AgentSessionRuntime 而非 AgentSession 拥有会话替换 API
  • 能对号入座三种 Run Mode 的适用场景
  • 知道 SDK 与 RPC 集成各自的取舍

上一章第十二章 · Compaction 内部机制
下一章第十四章 · 术语表 — 所有关键术语速查