@pi-web/agent-kit
v0.1.0
Published
pi-web 自定义 agent 作者的编写面:defineAgent / defineTool / Type。代理类型层,零 pi 依赖。
Readme
@pi-web/agent-kit
写 pi-web 自定义 agent 时唯一需要的 import。
npm i -D @pi-web/agent-kit// .pi-web/agents/my-agent/index.ts
import { defineAgent, defineTool, Type } from "@pi-web/agent-kit";
const echo = defineTool({
name: "echo",
label: "Echo",
description: "把传进来的文本原样回显。",
parameters: Type.Object({
text: Type.String({ description: "要回显的文本" }),
}),
// 第 5 个参数是工具上下文,目前只有 log。不需要它就照旧写 `execute(_id, params)`。
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
ctx.log("info", "echo 被调用了", { 长度: params.text.length });
return { content: [{ type: "text", text: params.text }], details: undefined };
},
});
export default defineAgent({
systemPrompt: "你是一个最小示例 agent。",
customTools: [echo],
tools: ["echo"], // 在全部平台内置工具之外,再启用 echo
});工具规则很简单:省略 noTools 时启用全部平台内置工具,再加入 tools;
noTools: "builtin" 先关闭全部内置工具,再启用 tools 中点名的工具;
noTools: "all" 关闭所有工具并忽略 tools。tools 里重复写内置工具不会报错,
配合 "builtin" 时还可以只重新启用其中几个。
然后 pi-web --agent my-agent "..."。一个 agent 目录 = 一个 agent。
这个包为什么存在
它是一个代理类型层:里面的类型全部由我们自己声明,靠 TypeScript 的结构类型与 pi 的对应
类型兼容,一次强制转换都没有。于是 agent 定义文件里只出现 @pi-web/*,pi SDK 在整个
pi-web 仓库里只有一个引用点(@pi-web/runner,由 tools/check-layers.mjs 机械强制)。
对你的直接好处:写 agent 只装这一个包(几 KB,运行时只有两个恒等函数),不用把整个 pi SDK 拖进你的项目。
代价是结构漂移 —— pi 升级改了形状,这些代理类型不会自动跟着变。配套的机械守卫在
@pi-web/runner 的 agent-conformance.ts:那是唯一同时看得见两边类型的地方,漂移会让
pi-web 自己的 pnpm typecheck 报红,而不是留到运行时。
为什么装它是必须的(不能靠 CLI 里那份副本)
agent 文件由 jiti 按它自己所在的位置解析 import,
所以 @pi-web/agent-kit 必须能从你的项目里解析到。装在 @pi-web/cli 里的副本它看不见。
顺带,这份 devDependency 也把编辑器里的类型补全给了你。
导出
| | |
|---|---|
| defineAgent(def) | agent 定义的入口,恒等函数。存在的意义是让对象字面量在声明处就被 AgentDefinition 收窄(不写它就得靠 satisfies,容易漏,写错字段名也不报错) |
| defineTool({...}) | 声明单个自定义工具,恒等函数。作用是在数组里保住 parameters 的类型推导 —— 直接把字面量塞进 customTools: [...] 会让 execute 的 params 退化成 unknown |
| Type | typebox 的 schema 构造器,与 pi 精确同版(1.1.38) |
| AgentDefinition / ToolDefinition / ToolContext / ToolLogger / ToolLogLevel / ThinkingLevel / TextContent / ImageContent / ToolResult … | 类型 |
工具里怎么打日志
execute 的第 5 个参数是工具上下文,上面有一个 log(级别, 一句话, 附加数据?),
级别是 debug / info / warn / error 四档(跟服务端 --log-level 用的是同一套词,
不用再学一遍)。
同一条日志会出现在两个地方,而这两个地方的过滤规则不一样 —— 这一点最容易踩:
| 在哪儿看 | 长什么样 | 会不会按级别挡掉 |
|---|---|---|
| 启动 pi-web 的那个终端 | 2026-08-09T… [pi-web:warn] [tool:echo] 你写的那句话 {"toolCallId":"…","你带的字段":…} | 会。低于 --log-level / PI_WEB_LOG 那一档的,一行都不打 |
| 浏览器页面里的「调试信息」面板 →「运行记录」 | 一条带级别标签的记录,附加数据展开在它下面 | 不会。四档全都送过去 |
所以顺手的做法是:终端留 info 及以上看个大概,debug 那些细节去面板里翻 ——
不用为了看一眼 debug 就改环境变量、重启服务。
两句提醒:
- 面板上那个「只看」下拉自己也能按级别筛,默认是「全部」。找不到某条
debug时, 先看看是不是被这个下拉挡住了 —— 服务端一条都没少发,那是浏览器本地的筛选。 - 面板里的记录只在这个会话活着的时候留着,重启后端就没了。要长期留档, 自己把 pi-web 的终端输出重定向到文件。
日志不会出现在用户读的对话里,也不会混进 pi-web 和 agent 之间的数据通道。
log 不抛异常、不返回东西:一条日志写坏了不该把工具带崩。附加数据要能 JSON 序列化;
序列化不了的(函数、循环引用)不会让整条日志消失 —— 你那句话照发,只有附加数据
会被换成一句说明。
要给用户看一句话(不是排障用的日志),那是另一回事,用
@pi-web/extension-kit 里的 ctx.ui。
写 pi 扩展(注册斜杠命令、用 ctx.ui.confirm 弹交互卡片)用配套的
@pi-web/extension-kit。
License
MIT
