Local Agent

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 审批流程。grepfind 会遵守 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_schemaexamples 用于 plugin_read 返回给模型或 UI。action 通过 session tool 执行时会收到 run_context;CLI、HTTP 或 scheduler 直接调用时该字段可能不存在。当前 Session step 已提交生效的 env 应读取 run_context.agentEnvcontext.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" },
    ],
  },
});

promptcontent 是 ImagePlugin 面向 Agent 的两种公开输入格式。纯文本生图用 prompt;只要有参考图、改图或多段上下文,就用 content。如果两者同时出现,content 优先,prompt 不会继续传给下游。

content[].url 可以是在线 URL、本地绝对路径,或相对 Agent 项目根目录的路径。本地图片会由 ImagePlugin 读取并转成 City 图片任务需要的 data URL,不需要模型自己传 base64。Agent 不应该传 messagesdata_url

default_model 是插件级默认图片模型,可以是字符串,也可以是同步或异步函数。Agent 调用 image_create 时如果没有传 modelImagePlugin 会自动把 default_model 的字符串值或函数返回值补给下游的 image_create(input);如果 payload 里显式传了 model,则以 payload 为准。没有配置 default_model 时,Agent 需要先确认模型 ID,可以调用 models 查看可用图片模型。

image_result 默认只读取一次当前任务状态。如果返回 queuedrunning,保留 job_id,稍后继续调用 image_result。如果希望短任务在一次 tool 调用里等到终态,可以传 until_done: true,并按需设置 max_wait_ms / poll_interval_ms。如果返回 succeededdata 会是包含图片 file parts 的 AI SDK UIMessage

ImagePlugin 不直接绑定 City,也不关心 OpenAI、DeepSeek 等 provider 的具体协议。传入 image_createimage_result 任务函数后,插件只负责把两步式任务 action 暴露给 Agent;默认轮询节奏由产品、Agent 或调用方根据 poll_after_ms 决定,until_done 只是插件层的简单等待能力。

所谓“provider 输入解析”指的是:Agent 调用 plugin_call 时只传简单的 promptcontentImagePlugin 在内部把本地图片读成 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_pathfiles[].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 上

继续看:

相关文档