tools 和 plugins
如何给本地 Agent 注入工具与显式 plugin 实例
tools 和 plugins
tools
本地 Agent 可以直接接收工具集合:
const agent = new Agent({
id: "repo-helper",
path: "/path/to/project",
tools: {
my_tool: myTool,
},
});这些工具会在 session 执行时直接可用。
如果你的应用需要一套长期稳定的默认工具集合,这种 agent 级注入方式通常最顺手。
shell
如果 Agent 需要拥有 shell 执行能力,使用 @downcity/shell:
import { Agent } from "@downcity/agent";
import { Shell } from "@downcity/shell";
const agent = new Agent({
id: "repo-helper",
path: "/path/to/project",
shell: new Shell(),
});这会挂载七个模型工具:
shell_exec:一次性非交互命令shell_session:长任务和需要 stdin 的 PTY 交互式会话grep:底层直接使用 ripgrep 的结构化项目内容搜索find:使用 POSIX glob 模式发现项目文件read:分页读取文本并把图片注入模型输入write:创建文件或显式原子覆盖完整文件edit:通过唯一文本匹配原子编辑单个文件
文件和搜索工具只允许访问 Agent 项目根目录。它们没有 unrestricted 模式;项目外操作仍然必须通过 Shell 的 unrestricted 审批流程。grep 和 find 会遵守 ignore 文件,默认最多返回 200 项,并允许通过 max_results 提高到 2,000 项。
read 读取 PNG、JPEG、GIF、WebP 或 BMP 图片时,会把图片作为临时 user file part 注入下一个模型 step。其他二进制文件只返回元数据。
read 会返回文件 SHA-256。文件可能被其他进程同时修改时,把该值作为 edit.expected_sha256 传入,可以阻止过期编辑覆盖新内容:
const current = await agent.tools.read.execute({
file_path: "src/index.ts",
});
await agent.tools.edit.execute({
file_path: "src/index.ts",
expected_sha256: current.sha256,
edits: [
{
old_text: "const enabled = false;",
new_text: "const enabled = true;",
},
],
});Unrestricted sandbox 请求归属发起工具调用的 Session:
const approvals = await session.approvals();
await session.resolve_approval({
approval_id: approvals[0].approval_id,
decision: "approved",
});实时 UI 在 session.subscribe((mutation) => {}) 收到 approval-required Tool Part 时,从 mutation.part.approval 读取完整请求,并在用户决定后调用 session.resolve_approval(...)。
plugin 实例
如果你希望本地 Agent 持有长期运行的 plugin 能力,比如 chat 渠道,可以显式传入 plugin 实例:
import { Agent } from "@downcity/agent";
import { ChatPlugin, TelegramChannel } from "@downcity/plugins";
const agent = new Agent({
id: "repo-helper",
path: "/path/to/project",
tools: {},
plugins: [
new ChatPlugin({
channels: [
new TelegramChannel({
env: {
TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN,
},
}),
],
}),
],
});这类写法适合:
- 你希望 SDK 自己启动 chat 渠道
- 你需要把 plugin 生命周期和应用代码绑定在一起
plugins
如果你需要让本地 Agent 持有一组 plugin,可以显式传入实例对象:
import { Agent } from "@downcity/agent";
import { SkillPlugin, WebPlugin } from "@downcity/plugins";
const agent = new Agent({
id: "repo-helper",
path: "/path/to/project",
tools: {},
plugins: [new SkillPlugin(), new WebPlugin()],
});
const plugins = agent.plugins.list();这些 plugin 会注册到当前 Agent 自己的 registry 里。
这意味着:
- plugin action 可以通过
agent.plugins.runAction(...)调用 - plugin hook 里拿到的
context.plugins会指向同一份 registry - 同名 plugin 重复注册会直接报错
- SDK 不会默认注册全部内建 plugin
当前本地 SDK 会接通 plugin registry、AgentContext.plugins,并在 session prompt 中注入已注册 plugin 的 system(context, run_context) 文本。
每个 Agent 都有独立的 plugin registry 和独立的 plugin_read / plugin_call tool 实例。即使同一个进程里同时创建多个 Agent,模型通过 plugin_call 触发 action 时,也只会访问当前 session 所属 Agent 的 registry。
plugin action metadata
带 action 的 plugin 会让 Agent 自动获得两个内置 tool:
plugin_read:读取已注册 plugin 的 action、参数 schema 和示例plugin_call:执行指定 plugin action
如果模型不确定某个 action 怎么传参数,应该先调用 plugin_read({ plugin, action }),再调用 plugin_call(...)。plugin_call 在执行前会按 action 的 input_schema 校验 payload。
自定义 plugin 可以用 createPlugin / createAction 声明 action metadata:
import { Agent, createAction, createPlugin } from "@downcity/agent";
import { z } from "zod";
const demo_plugin = createPlugin({
name: "demo",
title: "Demo",
description: "Demo actions",
actions: {
echo: createAction({
description: "回显一段文本",
input_schema: {
zod: z.object({
text: z.string(),
}),
json_schema: {
type: "object",
required: ["text"],
properties: {
text: { type: "string" },
},
},
},
examples: [
{
title: "回显文本",
payload: { text: "hello" },
},
],
execute: async ({ input, run_context }) => ({
success: true,
data: {
text: input.text,
session_id: run_context?.sessionId,
},
message: "echoed",
}),
}),
},
});
const agent = new Agent({
id: "repo-helper",
path: "/path/to/project",
plugins: [demo_plugin],
});zod schema 负责运行时校验;json_schema 和 examples 用于 plugin_read 返回给模型或 UI。action 通过 session tool 执行时会收到 run_context;CLI、HTTP 或 scheduler 直接调用时该字段可能不存在。当前 Session step 已提交生效的 env 应读取 run_context.agentEnv,context.env 只表示 Agent 的配置基线。旧的 class extends BasePlugin 写法仍然可用,也可以只在 actions 里逐步换成 createAction(...)。
ImagePlugin
如果你希望 Agent 在对话过程中自己触发生图,可以把图片能力包装成 ImagePlugin:
import { Agent } from "@downcity/agent";
import { ImagePlugin } from "@downcity/plugins";
const agent = new Agent({
id: "creative-agent",
path: "/path/to/project",
prepare_session: async (session) => await session.set({ model }),
plugins: [
new ImagePlugin({
default_model: "image-model-id",
list_models: async () => {
const catalog = await city.ai.listModels();
return catalog.forModality("image");
},
image_create: (input) => city.ai.image_create(input),
image_result: (input) => city.ai.image_result(input),
}),
],
});如果默认图片模型需要由业务状态决定,也可以传函数:
new ImagePlugin({
default_model: async ({ input }) => {
return input.aspect_ratio === "16:9" ? "image-wide-model-id" : "image-model-id";
},
image_create: (input) => city.ai.image_create(input),
image_result: (input) => city.ai.image_result(input),
});注册后,Agent 会自动获得内置的 plugin_read / plugin_call tool。模型可以先读取 action metadata:
await plugin_read({
plugin: "image",
action: "image_create",
});图片生成会消耗 provider 额度。Agent 应该先向用户确认具体要创建或编辑的图片请求,拿到明确确认后再调用 image_create。
确认后再使用任务式 action:
const job = await plugin_call({
plugin: "image",
action: "image_create",
payload: {
prompt: "一张雨夜城市街角的电影感插画",
aspect_ratio: "16:9",
},
});
await plugin_call({
plugin: "image",
action: "image_result",
payload: {
job_id: job.data.job_id,
},
});如果要传入参考图或改图,用 content:
const job = await plugin_call({
plugin: "image",
action: "image_create",
payload: {
content: [
{ type: "text", text: "把这张图改成白色影棚背景" },
{ type: "image", url: "./input.png" },
],
},
});prompt 和 content 是 ImagePlugin 面向 Agent 的两种公开输入格式。纯文本生图用 prompt;只要有参考图、改图或多段上下文,就用 content。如果两者同时出现,content 优先,prompt 不会继续传给下游。
content[].url 可以是在线 URL、本地绝对路径,或相对 Agent 项目根目录的路径。本地图片会由 ImagePlugin 读取并转成 City 图片任务需要的 data URL,不需要模型自己传 base64。Agent 不应该传 messages 或 data_url。
default_model 是插件级默认图片模型,可以是字符串,也可以是同步或异步函数。Agent 调用 image_create 时如果没有传 model,ImagePlugin 会自动把 default_model 的字符串值或函数返回值补给下游的 image_create(input);如果 payload 里显式传了 model,则以 payload 为准。没有配置 default_model 时,Agent 需要先确认模型 ID,可以调用 models 查看可用图片模型。
image_result 默认只读取一次当前任务状态。如果返回 queued 或 running,保留 job_id,稍后继续调用 image_result。如果希望短任务在一次 tool 调用里等到终态,可以传 until_done: true,并按需设置 max_wait_ms / poll_interval_ms。如果返回 succeeded,data 会是包含图片 file parts 的 AI SDK UIMessage。
ImagePlugin 不直接绑定 City,也不关心 OpenAI、DeepSeek 等 provider 的具体协议。传入 image_create 和 image_result 任务函数后,插件只负责把两步式任务 action 暴露给 Agent;默认轮询节奏由产品、Agent 或调用方根据 poll_after_ms 决定,until_done 只是插件层的简单等待能力。
所谓“provider 输入解析”指的是:Agent 调用 plugin_call 时只传简单的 prompt 或 content;ImagePlugin 在内部把本地图片读成 data URL,并把 content 转成 ImagePluginResolvedInput.messages 后再交给 image_create(input)。这只是 ImagePlugin 到 City / provider adapter 的内部边界,不是给 Agent 使用的新协议。
生成图片的 file parts 会统一落盘到 .downcity/resources/,以 .downcity/resources/... 这类 Agent 根目录相对路径引用,并自动合并进最终 assistant message。生成结果里的相对文件 URL 会先按 Agent 项目根目录解析,再进行资源落盘。plugin_call 结果也会通过 files[].relative_path 和 files[].path 返回 Agent 根目录相对路径与可直接打开的本机绝对路径。
plugin HTTP
如果 Downcity 暴露 Agent HTTP 网关,已注册 plugin 的 plugin.http.server.register(...) 也会自动挂到同一个 HTTP app 上。
也就是说:
- 插件是否存在,由
Agent构造参数里的plugins决定 - 插件 HTTP 是否真正暴露,由 Downcity 是否发布 Agent HTTP 网关决定
一个重要边界
SDK 这里的 plugins 是“你主动交给 Agent 的实例集合”,不是全局 plugin manager 的镜像。
所以不要把 SDK 模式想成“自动把项目里一切都搬进来”,它更强调显式装配。
如果你希望一次性带上整套内建 plugin,请显式使用 @downcity/plugins 里的 plugins: createBuiltinPlugins()。
如果你要进一步理解:
- plugin 应该怎么挂到本地 Agent 上
继续看:
相关文档
- Shell 与沙箱 — 配置命令执行、沙箱和审批