Pi 是一套用 TypeScript 构建 AI Agent 的工具包。各包分层叠用:
pi-ai:跨厂商 LLM 通信(Models 集合、流式、工具定义、用量与费用)pi-agent-core:在 pi-ai 之上加 Agent 循环(调模型 → 执行工具 → 回填 → 重复)pi-coding-agent:完整编码 Agent 运行时(内置文件工具、JSONL 会话、压缩、扩展)pi-tui:终端 UI(差分渲染、Markdown、多行编辑器、自动补全)
本教程按层递进。理解如何组合这些层之后,你可以按自己的产品边界组装 Agent,而不被某一层抽象锁死。
使用的包 scope 为 @earendil-works/*(与上游 @mariozechner/* API 同源演进)。0.82 起 pi-ai 以 Models + Provider 工厂为主路径;旧的全局 getModel / streamSimple 仅在 @earendil-works/pi-ai/compat 过渡,正文不再使用。
┌─────────────────────────────────────────┐
│ 你的应用 │
│ (CLI、Slack bot、OpenClaw 等) │
├────────────────────┬────────────────────┤
│ pi-coding-agent │ pi-tui │
│ 会话、工具、扩展 │ 终端 UI、编辑器 │
├────────────────────┴────────────────────┤
│ pi-agent-core │
│ Agent 循环、工具执行、事件 │
├─────────────────────────────────────────┤
│ pi-ai │
│ Models 集合、多厂商流式 LLM │
└─────────────────────────────────────────┘
按需取用:只调模型用 pi-ai;要工具循环加 pi-agent-core;要编码助手与会话用 pi-coding-agent;要漂亮终端再加 pi-tui。
- Node.js 20+
- 目标 LLM 厂商的 API Key(如 DeepSeek、OpenAI 等)
安装依赖包:
npm install @earendil-works/pi-ai @earendil-works/pi-agent-core \
@earendil-works/pi-coding-agent @earendil-works/pi-tui chalk设置对应厂商的环境变量密钥,例如 DeepSeek 需要在 .env 中写入:
DEEPSEEK_API_KEY=sk-...
核心概念:Provider 拥有模型目录、鉴权与 stream;Models 是已注册 Provider 的集合,按 model.provider 路由请求。
使用 createModels() 建空集合,然后调用目标厂商的 provider 工厂注册(如 deepseekProvider())。按需注册利于 tree-shaking。若要一次注册全部内置厂商,可使用 builtinModels()。
调用模型时用 models.completeSimple(),传入 model 对象和消息上下文,等待整段结束后返回 AssistantMessage:
import { createModels } from "@earendil-works/pi-ai";
import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
const models = createModels();
models.setProvider(deepseekProvider());
const model = models.getModel("deepseek", "deepseek-v4-flash");
const response = await models.completeSimple(model, {
systemPrompt: "你是一个简洁的助手。",
messages: [
{ role: "user", content: "法国的首都是哪里?", timestamp: Date.now() },
],
});
for (const block of response.content) {
if (block.type === "text") console.log(block.text);
}返回的 content 是类型化块:text / thinking / toolCall;另有 usage(用量)、stopReason(stop | toolUse | length | error | aborted)。
DeepSeek 走 OpenAI Completions 兼容协议(api: "openai-completions"),鉴权读 DEEPSEEK_API_KEY。
models.streamSimple() 将各厂商的原生流式格式归一成同一套事件:start、text_delta、thinking_delta、toolcall_*、done、error。多数场景只关心 text_delta(逐字输出)与 done(最终消息与用量)。
const stream = models.streamSimple(model, {
systemPrompt: "你是一个简洁的助手。",
messages: [
{ role: "user", content: "用三句话解释 TCP。", timestamp: Date.now() },
],
});
for await (const event of stream) {
switch (event.type) {
case "text_delta":
process.stdout.write(event.delta);
break;
case "done":
console.log(`\nTokens: ${event.message.usage.input} / ${event.message.usage.output}`);
break;
case "error":
console.error(event.error.errorMessage);
break;
}
}
// 也可:const final = await stream.result();切换模型只需改 getModel 的模型 id;切换厂商则需先 setProvider 对应工厂,并设置对应的环境变量:
const model = models.getModel("deepseek", "deepseek-v4-flash");
// 换厂商时:先 setProvider(openaiProvider()),并设置 OPENAI_API_KEY
// const model = models.getModel("openai", "gpt-4o");支持扩展思考的模型可通过 reasoning 参数打开(默认关闭),可选值:minimal | low | medium | high | xhigh。开启后流里会出现 thinking_delta 等事件。
const stream = models.streamSimple(model, context, {
reasoning: "high",
});用 createProvider() 挂载 OpenAI 兼容端点(Ollama、vLLM 等本地推理服务),而不是只塞一个裸 Model 对象。
pi-ai 让你跟 LLM 说话;pi-agent-core 让 LLM 通过工具做事。
Agent 跑标准循环:发消息 → 若有 tool call 则执行 → 把结果塞回上下文 → 再调模型,直到模型不再要工具。
工具用 TypeBox 描述参数。schema 先赋给变量再作为 AgentTool<typeof schema> 的泛参,这样 execute 里 params 有正确类型。
import { Type } from "@earendil-works/pi-ai";
import type { AgentTool } from "@earendil-works/pi-agent-core";
const weatherParams = Type.Object({
city: Type.String({ description: "城市名" }),
});
const weatherTool: AgentTool<typeof weatherParams> = {
name: "get_weather",
label: "天气",
description: "查询某城市天气",
parameters: weatherParams,
execute: async (_id, params, _signal, onUpdate) => {
return {
content: [{ type: "text", text: `${params.city}: 晴,22C` }],
details: { city: params.city },
};
},
};字段含义:name(模型调用名)、label(展示名)、description(何时用)、parameters(校验)、execute(执行)。onUpdate 可流式推送长任务的中间结果。
import { Agent } from "@earendil-works/pi-agent-core";
const agent = new Agent({
initialState: {
systemPrompt: "你是带工具的助手。",
model,
tools: [weatherTool],
thinkingLevel: "off",
},
streamFn: models.streamSimple.bind(models),
// toolExecution: "parallel", // 默认并行;可改为 "sequential"
});工具执行方式可选 parallel(默认,并行执行所有 tool call)或 sequential(逐个执行)。
agent.subscribe() 可监听 Agent 生命周期的各类事件:
agent.subscribe((event) => {
switch (event.type) {
case "message_update":
if (event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
break;
case "tool_execution_start":
console.log(`Tool: ${event.toolName}`, event.args);
break;
case "agent_end":
console.log("\n完成");
break;
}
});
await agent.prompt("东京和伦敦天气怎么样?");常见事件一览:
| 事件 | 触发时机 |
|---|---|
agent_start / agent_end |
整个 prompt 的生命周期 |
turn_start / turn_end |
每一轮 LLM 调用(模型回复 + 工具执行)的开始和结束 |
message_start / message_update / message_end |
消息的流式构建 |
tool_execution_start / update / end |
工具的执行过程 |
Agent 跑着时仍可注入用户意图。两种方式:
steer:打断——当前工具结束后注入,跳过剩余待执行工具followUp:不打断——等本轮自然结束后再问(仍属同一次 prompt)
const running = agent.prompt("东京和伦敦天气怎么样?");
// steer:打断——跳过剩余工具直接改做别的
agent.steer({
role: "user",
content: "别查天气了,改读 README。",
timestamp: Date.now(),
});
// followUp:不打断——等本轮结束后再追问
agent.followUp({
role: "user",
content: "再总结一下。",
timestamp: Date.now(),
});
await running; // 会等 followUp 那一轮也跑完两者只是往队列塞消息,消费发生在当前 prompt() 的 agent loop 里。因此必须在 await prompt() 之前(与之并行)调用——若等 prompt resolve 后再调,消息会积压且不会自动开下一轮(除非再 continue() 或新开一次 prompt)。
旧版的 setModel / setSystemPrompt 等方法已去掉;直接改 agent.state(下一 turn 生效):
agent.state.model = anotherModel;
agent.state.thinkingLevel = "high";
agent.state.systemPrompt = "新指令";
agent.state.tools = [...newTools]; // 顶层数组会拷贝一份再存
agent.state.messages = trimmed;还可在构造时或之后挂 beforeToolCall / afterToolCall 做权限拦截或结果改写。
pi-agent-core 给循环;pi-coding-agent 给生产向编码 Agent:内置文件工具、会话持久化、压缩、扩展。内部仍基于 agent-core。
多数场景从这里起步;只有当你不需要内置编码工具/会话系统时,才直接用 agent-core。
默认启用四个文件操作工具;另有可选探索工具:
| 工具 | 作用 |
|---|---|
read |
读文件与图片;支持 offset/limit |
bash |
在工作目录执行命令 |
edit |
精确替换文件中的一段文本 |
write |
写文件(可创建父目录) |
grep |
正则搜内容(ripgrep,尊重 .gitignore) |
find |
按 glob 找文件 |
ls |
列目录 |
工厂函数可绑定特定工作目录,或自定义 operations(例如 Docker / 远程文件系统):
import {
createCodingTools,
createReadOnlyTools,
createBashTool,
} from "@earendil-works/pi-coding-agent";
createCodingTools("/path/to/workspace"); // read, bash, edit, write
createReadOnlyTools("/path/to/workspace"); // read, grep, find, ls
createBashTool("/workspace", {
operations: { exec: async (cmd, cwd, opts) => runInDocker(cmd, cwd, opts) },
});在 createAgentSession 里,启用哪些工具用名字白名单 tools: string[],不是传入 Tool 对象数组。
用 createAgentSession 创建会话,需传入 model、modelRuntime、sessionManager。sessionManager 可以是内存模式(SessionManager.inMemory()),不落盘:
import {
createAgentSession,
ModelRuntime,
SessionManager,
} from "@earendil-works/pi-coding-agent";
const modelRuntime = await ModelRuntime.create();
const model = modelRuntime.getModel("deepseek", "deepseek-v4-flash");
const { session } = await createAgentSession({
model,
modelRuntime,
sessionManager: SessionManager.inMemory(),
});
session.subscribe((event) => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
});
await session.prompt("当前目录有哪些文件?");
session.dispose();在 createAgentSession 中通过 tools 数组指定启用哪些内置工具(名字白名单),通过 customTools 传入用 defineTool 定义的自定义工具。defineTool 用于保留 TypeBox 参数推断;也可在 extension 里注册。
import { Type } from "@earendil-works/pi-ai";
import {
createAgentSession,
defineTool,
ModelRuntime,
SessionManager,
} from "@earendil-works/pi-coding-agent";
const deployTool = defineTool({
name: "deploy",
label: "部署",
description: "部署到指定环境",
parameters: Type.Object({
environment: Type.String({ description: "staging / production" }),
}),
execute: async (_id, params) => ({
content: [{ type: "text", text: `已部署到 ${params.environment}` }],
details: {},
}),
});
const modelRuntime = await ModelRuntime.create();
const model = modelRuntime.getModel("deepseek", "deepseek-v4-flash");
const { session } = await createAgentSession({
model,
thinkingLevel: "off",
modelRuntime,
sessionManager: SessionManager.inMemory(),
tools: ["read", "bash", "edit", "write", "deploy"], // 名字白名单
customTools: [deployTool],
});会话存成 JSONL,条目带 id / parentId,形成树,便于分支与「从某点重试」。
SessionManager.inMemory(); // 内存,不落盘
SessionManager.create(process.cwd()); // 在 ~/.pi/agent/sessions/ 下新建
SessionManager.open("/path/to/session.jsonl");
SessionManager.continueRecent(process.cwd());
await SessionManager.list(process.cwd());底层能力(自建多通道路由时会用到):buildSessionContext()、getLeafEntry()、branch(entryId)、appendMessage()、getTree()。
长对话会顶满上下文窗口。coding-agent 可把旧消息摘要掉、保留最近轮次:
import { estimateTokens } from "@earendil-works/pi-coding-agent";
const total = session.messages.reduce((s, m) => s + estimateTokens(m), 0);
if (total > 100_000) {
await session.compact("保留文件路径与代码改动");
}默认开启 auto-compaction;完整历史仍在 JSONL 里,只压缩内存上下文。
工具让模型做事;扩展改 Agent行为(模型看不见)。典型用途:裁剪旧 tool 结果、替换压缩逻辑、拦截危险工具、注入上下文。
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function myExtension(pi: ExtensionAPI): void {
pi.on("tool_call", async (event) => {
if (event.toolName === "bash" && String(event.input).includes("rm -rf")) {
return { block: true, reason: "危险命令已拦截" };
}
});
pi.registerCommand("stats", {
description: "显示会话统计",
handler: async (_args, ctx) => {
const stats = ctx.session.getSessionStats();
console.log(stats);
},
});
}通过 DefaultResourceLoader 的 extensionFactories / additionalExtensionPaths 挂载。事件还包括 context、session_before_compact、before_agent_start、session_start 等。
把会话持久化、自定义工具、REPL 串起来,就是一个完整的编码助手。核心流程:
- 用
SessionManager.open持久化对话到 JSONL 文件 - 用
ModelRuntime选择可用模型 - 用
defineTool注册自定义工具(如 web search) - 在
createAgentSession中用tools白名单和customTools注册工具 - 用
session.subscribe监听事件,打印流式文本与工具调用 - 启动 REPL 循环;提供
exit退出、new重置会话的命令
重置会话时,需要 dispose 旧 session + 新建 SessionManager + 重建 session。当前 AgentSession 上没有旧版的 newSession(),需要整会话替换或用 createAgentSessionRuntime。
示例交互效果:
PI Assistant
Model: deepseek-v4-flash
Tools: read, bash, edit, write, web_search
You: 这个项目做什么?先看 README 和 package.json。
[read] README.md
[read] package.json
这是一个演示 pi-* 分层用法的 TypeScript 项目……
嵌入自己的产品时,优先显式构造 ModelRuntime,指定自定义的鉴权路径和模型配置路径:
const modelRuntime = await ModelRuntime.create({
authPath: "/my/app/auth.json",
modelsPath: "/my/app/models.json",
});
modelRuntime.setRuntimeApiKey("deepseek", process.env.DEEPSEEK_API_KEY!);
const model = modelRuntime.getModel("deepseek", "deepseek-v4-flash");
const { session } = await createAgentSession({
model,
modelRuntime,
sessionManager: SessionManager.create(cwd, customSessionDir),
// resourceLoader / settingsManager / tools / customTools …
});需要 /new、/resume、/fork 等会话替换流时,使用 createAgentSessionRuntime。替换后要重新 subscribe / bindExtensions 到新的 runtime session。
OAuth、多 Provider 订阅、settings 等细节见各包 README。
纯 readline 版没有 Markdown 渲染、自动补全和流畅的流式刷新。pi-tui 用差分渲染 + 同步输出做无闪烁终端 UI。
- 壳:
new TUI(new ProcessTerminal())创建 TUI 实例,addChild搭建组件树,setFocus(editor)设置焦点,tui.start()启动渲染循环 - 主题:
MarkdownTheme/EditorTheme都是(text) => ANSI着色函数(常用 chalk 实现) - 布局约定:
Editor固定在children末尾;新消息用children.splice(len - 1, 0, comp)插在输入框上方 - 事件驱动渲染:通过
session.subscribe把 agent 生命周期映射成增删/更新组件,每次改完后调tui.requestRender()
| 组件 | 用途 |
|---|---|
Text |
静态文本行 |
Markdown |
渲染 Markdown 内容(支持自定义主题),可动态更新文本 |
Editor |
多行输入编辑器,支持自动补全 |
Loader |
加载动画(如 "Thinking...") |
流式输出时,累加 streamingText,在同一个 Markdown 实例上调用 .setText() 更新内容,而不是每次 delta 都新建组件:
streamingText += event.assistantMessageEvent.delta;
if (!streamingMarkdown) {
streamingMarkdown = new Markdown(streamingText, 1, 0, markdownTheme);
children.splice(children.length - 1, 0, streamingMarkdown);
} else {
streamingMarkdown.setText(streamingText);
}
tui.requestRender();注意:Markdown 第五个参数是 DefaultTextStyle 对象(如 { color: (s) => chalk.bold(s) }),不是函数。
- agent_start:插入
Loader组件 - 首个 text_delta / 工具开始:移除
Loader - text_delta 持续到达:更新
Markdown组件文本 - tool_execution_start:插入一行
Text提示工具名和参数 - agent_end:清理状态,恢复编辑器可提交
- 退出:通过
matchesKey(data, "ctrl+c")或/exit命令触发tui.stop()
Editor 可配置 CombinedAutocompleteProvider,支持命令(如 /exit、/new)和文件路径的 Tab 补全:
editor.setAutocompleteProvider(
new CombinedAutocompleteProvider(
[
{ name: "new", description: "重置会话" },
{ name: "exit", description: "退出" },
],
process.cwd(),
),
);| 层 | 核心概念 | 适用场景 |
|---|---|---|
| pi-ai | Models、Provider、streamSimple、completeSimple | 只需调用 LLM,不需要工具循环 |
| pi-agent-core | Agent、AgentTool、事件订阅、steer/followUp | 需要 LLM 使用工具,但不需内置文件工具 |
| pi-coding-agent | createAgentSession、SessionManager、内建工具、扩展 | 需要完整的编码助手与会话管理 |
| pi-tui | TUI、Markdown、Editor、Loader、差分渲染 | 需要漂亮的终端交互界面 |
- 深入学习各包的 README 文档
- 参考上游 monorepo 的更多示例
- 生产环境集成可参考 OpenClaw 等项目
| 旧 API | 新 API(0.82 @earendil-works) |
|---|---|
全局 getModel(p, id) |
models.getModel(p, id) 或 ModelRuntime / getBuiltinModel |
顶层导入 streamSimple / completeSimple |
models.streamSimple / models.completeSimple |
streamFn: streamSimple |
streamFn: models.streamSimple.bind(models) |
直接赋值 session.agent.streamFn |
已删除;由 ModelRuntime / session 内部自动接线 |
tools: [allBuiltInTools.read, …] |
tools: ["read", "bash", …](名字白名单) |
customTools 传 AgentTool 对象 |
defineTool(...) 返回 ToolDefinition |
| AuthStorage + ModelRegistry 主路径 | ModelRuntime.create() 主路径 |
@mariozechner/pi-* |
@earendil-works/pi-* |
@earendil-works/pi-ai 根上的旧全局符号 |
过渡期 @earendil-works/pi-ai/compat(将移除) |