构建 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,而不只是模拟的数据帧:
- 以目录来源安装插件;
- 确认它的 SVG、模型、模式和设置都会显示;
- 创建会话并完成一段真实的提示词;
- 在支持的情况下,试用权限、引导、取消、持久化和重放;
- 在桌面和移动端宽度的布局中渲染每一种自定义时间线条目;
- 在会话处于活动状态时重新加载和移除插件,并确认会话随之终止。
按照发布插件通过 npm 或 Git 分发 Provider。把 Agent 兼容性和版本发布保留在插件项目中。
确切的运行时和 SVG 规则见插件参考。
赞助商
Cloud coding agents
- 成为赞助商