查看 Markdown

构建 Provider 插件

如果你之前没有构建过 Paseo 插件,请从插件快速上手开始。

Provider 插件把一个编程 Agent 接入 Paseo,而无需把它加入 Paseo 核心。在 npm 或 Git 仓库中发布插件,用户就可以用 paseo plugin install 安装它。

选择一种实现方式:

Agent方式
已经实现了 ACP用 runAcpProvider() 注册它,只添加少量针对厂商的转换器。
提供 TypeScript SDK、JSON-RPC API 或自定义进程协议直接实现 ProviderRegistration。

示例有:

  • provider-direct:会话、设置、提示词、持久化、子会话,以及一个由 Provider 拥有的时间线渲染器;
  • provider-acp-transformer:一个 ACP 命令模板,带有一个用 Zod 校验的厂商编辑转换器。加载之前,请把 example-acp --stdio 替换为一个已安装的 ACP Agent;
  • inline-thinking:一个只包含渲染器、不实现 Provider 的插件。

注册直接实现的 Provider

添加一个服务端入口:

// index.server.ts
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { createProvider } from "./server/provider";

export default function contribute(server: PluginServerContext) {
  server.registerProvider(createProvider());
  return () => {};
}

createProvider() 返回一个 ProviderRegistration:

import {
  negotiateProviderCapabilities,
  type ProviderRegistration,
} from "@getpaseo/plugin/server/provider";

const supported = ["prompt.message"] as const;

export function createProvider(): ProviderRegistration {
  return {
    id: "my-agent",
    label: "My agent",
    icon: "icon.svg",
    async connect(request) {
      const capabilities = negotiateProviderCapabilities(request.capabilities, supported);
      return createConnection(capabilities);
    },
  };
}

连接有三个操作:

  • send(input) 接收工作后立即返回。结果以事件的形式发布,而不是返回值。
  • onEvent(listener) 发布会话状态和完整的时间线快照。
  • close() 停止所有会话,并释放进程、订阅和待处理的工作。

把原生 SDK、进程和流保留在连接的实现内部。在发布之前,把它的输出转换为 ProviderEvent 对象。

启动 Provider CLI

对于进程传输和 CLI 探测,请使用 @getpaseo/plugin/server 中的 spawnProcess 和 execCommand。两者都能处理 Windows 的 .cmd/.bat 启动器,并为 shell 参数加引号。原生的 Node spawn 和 execFile 在 Windows 上会拒绝这些启动器。

daemon 在 request.launch 中提供解析后的命令、参数前缀和完整的环境变量。请显式传入这些环境变量;这些辅助函数不会应用 daemon 的环境策略。把进程生命周期和协议截止时间保留在你的 Provider 内部。正常关闭时结束 stdin;强制停止时使用 terminateProcess(child)。它在 Windows 上会结束整个进程树,包括命令脚本启动器背后的 CLI。为最后等待流关闭的过程设定上限。

import { spawnProcess, execCommand } from "@getpaseo/plugin/server";

const child = spawnProcess(launch.command, [...launch.args, "serve"], {
  env: launch.env,
  cwd,
  stdio: "pipe",
});
const { stdout } = await execCommand(launch.command, [...launch.args, "--version"], {
  env: launch.env,
  timeout: 5000,
});

返回模型、模式和思考选项

Paseo 在创建会话之前会请求目录。返回 Agent 表单需要的选项:

if (input.type === "catalog") {
  emit({
    type: "catalog",
    requestId: input.requestId,
    catalog: {
      models: [
        { id: "agent-large", label: "Agent Large" },
        { id: "agent-small", label: "Agent Small" },
      ],
      modes: [
        { id: "build", label: "Build" },
        { id: "plan", label: "Plan" },
      ],
      thinkingOptions: [
        { id: "standard", label: "Standard" },
        { id: "extended", label: "Extended" },
      ],
      defaultModel: "agent-large",
      defaultMode: "build",
      defaultThinkingOption: "standard",
    },
  });
}

对于 Agent 不提供的类别,返回空数组。选中的 model、mode 和 thinkingOption 会出现在会话配置中。

目录描述的是会话存在之前可用的选项。会话专属的控件之后通过 session.config 提供。

打开会话

session.open 包含完整的启动配置:工作目录、环境变量、系统提示词、MCP 服务器、工具策略、模型、模式、设置、不透明的 Provider 选项以及持久化偏好。

对于持久化的、作用于主机的插件设置,请使用 server.registerSettings() 返回的句柄。在目录和会话回调中读取它;当 Provider 在内存中保留配置时,订阅它。read() 和 subscribe() 的契约见设置。

用选中的目录值创建原生会话,然后发布它实际生效的状态:

const nativeSession = await sdk.createSession({
  cwd: input.config.cwd,
  model: input.config.model,
  mode: input.config.mode,
  thinking: input.config.thinkingOption,
  systemPrompt: input.config.systemPrompt,
  env: input.config.env,
  mcpServers: input.config.mcpServers,
});

emit({
  type: "session.opened",
  requestId: input.requestId,
  sessionId: input.sessionId,
  capabilities: ["prompt.message"],
  restoration: "core",
  cwd: input.config.cwd,
});
emit({
  type: "session.config",
  sessionId: input.sessionId,
  config: {
    model: nativeSession.model,
    mode: nativeSession.mode,
    thinkingOption: nativeSession.thinking,
    models,
    modes,
    thinkingOptions,
    settings: [],
  },
});
emit({ type: "session.ready", requestId: input.requestId, sessionId: input.sessionId });

session.config 包含已确定的值。请发布原生 Agent 经过规范化后实际选中的值,而不是请求的副本。

完成第一段提示词

从消息类提示词开始。声明 prompt.message,把内容传给原生 Agent,并发布由此产生的生命周期:

const turnId = crypto.randomUUID();

emit({
  type: "timeline.item",
  sessionId,
  item: {
    type: "user_message",
    id: `user:${turnId}`,
    text,
    clientMessageId: prompt.clientMessageId,
  },
});
emit({
  type: "session.prompt_result",
  sessionId,
  clientMessageId: prompt.clientMessageId,
  result: { type: "turn", turnId },
});
emit({ type: "session.turn", sessionId, turnId, state: "started" });

// Publish complete snapshots as native output changes.
emit({
  type: "timeline.item",
  sessionId,
  item: { type: "assistant_message", id: `assistant:${turnId}`, text: completeText },
});

emit({ type: "session.turn", sessionId, turnId, state: "completed" });

对每个 clientMessageId 恰好发布一个 session.prompt_result。把 clientMessageId 复制到用户的时间线条目上,以便 Paseo 替换它乐观显示的消息。每个已开始的轮次都需要一个终止的 completed、failed 或 canceled 事件。

到这一步,Provider 已经可用:用户可以选择模型、打开会话、发送消息并看到回复。

添加会话专属的 Composer 控件

只有当原生 Agent 有用户在打开会话后需要修改的设置时,才添加 session.configure。在 session.config 中以开关或选择控件的形式发布它们:

emit({
  type: "session.config",
  sessionId,
  config: {
    model: "agent-large",
    models: [{ id: "agent-large", label: "Agent Large" }],
    modes: [],
    thinkingOptions: [],
    settings: [
      {
        type: "toggle",
        id: "fast",
        label: "Fast mode",
        value: nativeSession.fast,
      },
      {
        type: "select",
        id: "approval",
        label: "Approvals",
        value: nativeSession.approval,
        options: [
          { label: "Ask", value: "ask" },
          { label: "Auto accept", value: "auto" },
        ],
      },
    ],
  },
});

Paseo 会在 Composer 中渲染这些控件。用户的修改会以 session.configure 的形式到达;应用它,并在 request.completed 之前发布已确定的 session.config。Provider 可以在运行时改变可用的控件,包括在模型变化之后。

settings 是面向用户的控件。providerOptions 是在创建会话时提供的不透明 JSON,用于 Paseo 不会渲染的 Provider 专属配置。Provider 专属的 token 预算应放在这两处之一;它不是一个专门的提示词字段。

添加持久化与重放

当原生会话可以被重新打开时,声明 session.persistence。当 session.open.persistence 不存在时,打开一个新的原生会话;存在时,恢复它所指定的原生会话。在 session.opened 或 session.persistence 中返回一个不透明的持久化值;Paseo 会保存它而不检查其内容。

当 history 为 "replay" 时,请在 session.ready 之前发布原生会话已有的 timeline.item 快照。使用 history: "skip" 可以在不重放旧记录的情况下打开会话。

Paseo 刷新 Agent 的方式是关闭它当前的 Provider 会话,然后用当前的配置和持久化值重新打开。请在 session.open 期间重新读取凭据、环境变量、全局配置和 MCP 服务器。没有单独的重新加载操作。

当 Paseo 可以从持久化值重新打开会话时,使用 restoration: "core"。由 Provider 拥有的子会话使用 restoration: "parent";把它作为另一个带有 parentSessionId 的 session.opened 事件发出,并在恢复父会话时重新创建它。

添加命令和引导

所有用户输入都通过 session.prompt 到达:

  • input.type: "message" 携带文本、图片和附件;
  • input.type: "command" 携带所选的命令及其参数;
  • delivery: "steer" 要求你把输入加入当前正在进行的轮次;
  • delivery: "auto" 允许你开始一个轮次,或者在没有轮次的情况下完成一条命令。

发布 session.commands,向 Composer 添加结构化命令。命令可以开始一个普通轮次,也可以作为副作用完成,并返回一个 completed 的提示词结果。

只声明你实现了的能力。如果不支持引导,请省略 prompt.steer;Paseo 可以改为替换当前轮次。

发布时间线条目

通过 timeline.item 发布完整的快照。更新流式文本、运行中的工具或待办列表时,复用条目的 id。Paseo 会推导出实时增量,并保持正常的时间线。

内置的条目类型涵盖用户和助手消息、推理、工具、待办事项、错误、通知和上下文压缩。当你的 Provider 有这些类型无法表达的展示方式时,使用插件条目:

emit({
  type: "timeline.item",
  sessionId,
  item: {
    type: "plugin",
    id: "review-42",
    pluginId: "my-provider-plugin",
    kind: "review-verdict",
    version: 1,
    data: { verdict: "ship", summary: "All checks passed" },
  },
});

在 index.client.tsx 中单独注册渲染器:

import type { PluginClientContext } from "@getpaseo/plugin/client";
import { z } from "zod";
import { ReviewVerdict } from "./client/review-verdict";

const reviewVerdictSchema = z.object({
  verdict: z.enum(["ship", "hold"]),
  summary: z.string(),
});

export default function contribute(client: PluginClientContext) {
  client.addTimelineRenderer({
    kind: "review-verdict",
    version: 1,
    schema: reviewVerdictSchema,
    Component: ReviewVerdict,
  });
  return () => {};
}

同一个渲染器可以显示由 Provider、客户端时间线转换器或 daemon 时间线追加发出的条目。渲染器并不需要 Provider 实现。

适配 ACP Agent

当 Agent 已经支持 ACP 时,使用 ACP 适配层:

import type { PluginServerContext } from "@getpaseo/plugin/server";
import { runAcpProvider } from "@getpaseo/plugin/server/acp";

export default function contribute(server: PluginServerContext) {
  server.registerProvider(
    runAcpProvider({
      id: "vendor-agent",
      label: "Vendor agent",
      icon: "icon.svg",
      command: ["vendor-agent", "acp"],
    }),
  );
  return () => {};
}

适配层负责 ACP 进程、能力映射、会话生命周期、提示词、权限和时间线转换。不要把这些机制复制到插件中。

只在 ACP 无法描述厂商差异时使用 transformers。用 Zod 校验厂商负载,并让格式错误或无关的值保持不变:

import type { AcpTransformer } from "@getpaseo/plugin/server/acp";
import { z } from "zod";

const editSchema = z.object({
  path: z.string(),
  before: z.string(),
  after: z.string(),
});

export const vendorEdits: AcpTransformer = {
  toolCall(toolCall) {
    if (toolCall.name !== "vendor_file_edit") return toolCall;
    const input = editSchema.safeParse(toolCall.input);
    if (!input.success) return toolCall;
    return {
      ...toolCall,
      kind: "edit",
      input: {
        filePath: input.data.path,
        oldString: input.data.before,
        newString: input.data.after,
      },
    };
  },
};

测试与发布

用真实的 Agent 测试 Provider,而不只是模拟的数据帧:

  1. 以目录来源安装插件;
  2. 确认它的 SVG、模型、模式和设置都会显示;
  3. 创建会话并完成一段真实的提示词;
  4. 在支持的情况下,试用权限、引导、取消、持久化和重放;
  5. 在桌面和移动端宽度的布局中渲染每一种自定义时间线条目;
  6. 在会话处于活动状态时重新加载和移除插件,并确认会话随之终止。

按照发布插件通过 npm 或 Git 分发 Provider。把 Agent 兼容性和版本发布保留在插件项目中。

确切的运行时和 SVG 规则见插件参考。

赞助商