Skip to content

Chapter 14 · SDK Programmatic Embedding ​

Chapter goals: Use the @earendil-works/pi-coding-agent SDK to create agent sessions in your own Node application; handle event streams; register custom tools; and完成一个可运行的嵌入示例.

14.1 The SDK's Positioning and Capability边界 ​

The SDK shares the same核心 with the pi CLI: you directly拿到 the AgentSession in your own Node.js process;获得类型安全; in-process直连; and完全访问 to the agent state. Suitable for building custom UIs; automation pipelines; sub-agents etc.

The边界同样清晰:

  • When needing cross-language (Python/Go clients) or process isolation; choose RPC mode (next chapter);
  • Session replacement operations (new/resume/fork) are not on AgentSession; but on AgentSessionRuntime;
  • The SDK is included in the主包;无需单独安装: npm install @earendil-works/pi-coding-agent.

14.2 Ten-Line Minimal Runnable Example ​

typescript
import {
  createAgentSession,
  ModelRuntime,
  SessionManager,
} from "@earendil-works/pi-coding-agent";

// ModelRuntime handles the model目录 and credentials resolution
const modelRuntime = await ModelRuntime.create();

const { session } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),   // Not persisted; use SessionManager.create(cwd) for持久化
  modelRuntime,
});

// Subscribe to the event stream: text_delta is the逐 token streaming output
session.subscribe((event) => {
  if (event.type === "message_update" &&
      event.assistantMessageEvent.type === "text_delta") {
    process.stdout.write(event.assistantMessageEvent.delta);
  }
});

await session.prompt("What files are in the current directory?");

Credential resolution优先级由 ModelRuntime处理: runtime overrides (setRuntimeApiKey; not persisted) → auth.json stored credentials → env vars (ANTHROPIC_API_KEY etc.) → custom provider fallback. For testing use ModelRuntime.create({ credentials: new InMemoryCredentialStore() }) to完全隔离.

14.3 Sending Messages and the Event Stream ​

typescript
// Regular prompt (wait for the完整运行 to end; including auto-retries)
await session.prompt("Summarize this repo's tech stack");

// Prompt with an image
await session.prompt("What's wrong with this screenshot?", {
  images: [{ type: "image"; source: { type: "base64"; mediaType: "image/png"; data: b64 } }],
});

// Must declare排队语义 when streaming; otherwise an error is thrown:
await session.prompt("Switch to error-handling-first"; { streamingBehavior: "steer" });   // Insert after this轮's tool calls结束
await session.followUp("Also update the README when done");                            // Delivered only after the agent stops

Event type quick-reference (event.type in the session.subscribe callback): streaming message_update (内含 text_delta/thinking_delta/toolcall_*); tool three-segment tool_execution_start/update/end; turn turn_start/turn_end; lifecycle agent_start/agent_end/agent_settled; plus queue_update; compaction_*; auto_retry_* etc.

For精细控制工具集 and resources:

typescript
import { Type } from "typebox";
import { createAgentSession; defineTool; DefaultResourceLoader } from "@earendil-works/pi-coding-agent";

const statusTool = defineTool({
  name: "status",
  description: "Return the service status",
  parameters: Type.Object({}),
  execute: async () => ({
    content: [{ type: "text"; text: `uptime: ${process.uptime()}s` }],
    details: {},
  }),
});

const loader = new DefaultResourceLoader({
  systemPromptOverride: () => "You are a极简运维助手.",
});
await loader.reload();   // Don't forget to reload to trigger resource discovery

const { session } = await createAgentSession({
  tools: ["read"; "bash"; "status"];  // Built-in tool whitelist: read/bash/edit/write/grep/find/ls
  customTools: [statusTool];          // Merged with扩展-registered tools
  resourceLoader: loader,
});

DefaultResourceLoader also supports injecting inline extensions (extensionFactories;可用 InlineExtension命名显示); skill overrides (skillsOverride); context file overrides (agentsFilesOverride) etc.; almost全部 programmatic-izing what can be配 in the TUI.

14.4 Embedding pi into a Node Service ​

A common production形态: an HTTP service receives a request → creates a session to跑 the task → streams回传:

typescript
import express from "express";
import { createAgentSession; ModelRuntime; SessionManager } from "@earendil-works/pi-coding-agent";

const app = express();
app.use(express.json());

const modelRuntime = await ModelRuntime.create();

app.post("/agent/tasks"; async (req; res) => {
  res.setHeader("Content-Type"; "text/plain; charset=utf-8");
  const { session } = await createAgentSession({
    cwd: req.body.cwd ?? process.cwd();
    sessionManager: SessionManager.inMemory(req.body.cwd ?? process.cwd());
    modelRuntime,
  });

  const unsubscribe = session.subscribe((event) => {
    if (event.type === "message_update" &&
        event.assistantMessageEvent.type === "text_delta") {
      res.write(event.assistantMessageEvent.delta);      // Stream-write back to the client
    }
  });

  try {
    await session.prompt(req.body.task ?? "Check project health");
  } finally {
    unsubscribe();
    session.dispose();                                   // Release session resources
    res.end();
  }
});

app.listen(8080; () => console.log("agent service on :8080"));

工程化要点:

  • Session persistence: SessionManager.continueRecent(cwd) resume the最近 session; SessionManager.open(path) open a specified file; SessionManager.list/listAll(cwd) enumerate;
  • Setting injection: SettingsManager.inMemory({ compaction: { enabled: false } })免文件 I/O; in file mode remember await settingsManager.flush() before退出 to保证持久化边界;
  • Test-friendly: memory-version SessionManager + SettingsManager + InMemoryCredentialStore组合可以完全离线跑单测;
  • After each session replacement (the runtime API's new/fork/switch)要重新 subscribe and rebind extensions.

Chapter Summary ​

  • The SDK = in-process; type-safe pi核心;换 RPC mode for跨-language or needing isolation;
  • Three-piece startup: ModelRuntime.create() + createAgentSession() + session.subscribe();
  • prompt() supports images and streamingBehavior; queue in-stream via steer()/followUp();
  • Tool system programmable: built-in whitelists + defineTool +各类 override from DefaultResourceLoader;
  • Service embedding注意 dispose; flush; and re-subscribe三个生命周期细节.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. For a Node.js/TypeScript application wanting to embed pi capabilities; which approach does the官方更推荐?

2. What happens when calling session.prompt() without streamingBehavior while the agent is already streaming?

3. Regarding传入 custom tools; which is correct?

4. Why should the server code call session.dispose() after the prompt ends?

🛠️ Hands-on Practice ​

  1. Run through the 14.2 minimal example; then换 SessionManager.inMemory() to create(cwd); find the generated .jsonl session file and重新加载续聊 via SessionManager.open().
  2. Add a POST /agent/abort接口 to the 14.4 HTTP service:持有当前请求的 session引用; executing await session.abort() when the call is received.
  3. Use SettingsManager.inMemory to disable auto-compaction; enable retries (maxRetries: 5);对比 default config下的长对话行为差异并记录观察结果.

After completing; continue to Chapter 15: RPC Mode and Process Integration.