mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 01:32:40 +00:00
* feat(plugins): unify source installation and simplify plugin settings * feat(plugins): install npm sources and review CLI updates Treat install selectors as one-time choices. Keep source identity separate from the installed revision, and resolve ordinary updates from the current upstream default. npm only acquires directories; plugin loading remains independent of npm. * fix(plugins): preserve CLI diagnostics and exercise reviewed updates Move new source capability coverage to real browser journeys. Update the requirements regression to exercise preview and apply, and preserve the existing duplicate-selector diagnostic across positional option scopes. * test(plugins): remove unused pending helper * test(plugins): pack registry fixtures with the Windows command runner * test(plugins): assert failed client contribution setup * docs(plugins): consolidate guides and document npm publishing * docs(plugins): introduce publishing choices before npm instructions * test(plugins): own browser journey cleanup in a fixture * feat(plugins): prepare scaffolded packages for publishing
404 lines
13 KiB
Markdown
404 lines
13 KiB
Markdown
---
|
|
title: Build a provider plugin
|
|
description: Add a coding agent to Paseo directly or adapt an ACP agent.
|
|
nav: Provider plugins
|
|
order: 46
|
|
category: Plugins
|
|
---
|
|
|
|
# Build a provider plugin
|
|
|
|
Start with the [plugin quickstart](/docs/plugins) if you have not built a Paseo plugin before.
|
|
|
|
A provider plugin connects a coding agent to Paseo without adding it to Paseo core. [Publish the plugin](/docs/plugins/publishing) on npm or in a Git repository so users can
|
|
install it with `paseo plugin install`.
|
|
|
|
Choose one implementation path:
|
|
|
|
| Agent | Path |
|
|
| ----------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
| It already implements ACP | Register it with `runAcpProvider()` and add only narrow vendor transformers. |
|
|
| It has a TypeScript SDK, JSON-RPC API, or custom process protocol | Implement `ProviderRegistration` directly. |
|
|
|
|
The examples are:
|
|
|
|
- [`provider-direct`](https://github.com/getpaseo/paseo/tree/main/plugin-examples/provider-direct): sessions, settings, prompts, persistence, child sessions, and a provider-owned timeline renderer;
|
|
- [`provider-acp-transformer`](https://github.com/getpaseo/paseo/tree/main/plugin-examples/provider-acp-transformer): an ACP command template with a Zod-validated vendor edit transformer. Replace `example-acp --stdio` with an installed ACP agent before loading it;
|
|
- [`inline-thinking`](https://github.com/getpaseo/paseo/tree/main/plugin-examples/inline-thinking): a renderer-only plugin that does not implement a provider.
|
|
|
|
## Register a direct provider
|
|
|
|
Add a server entry:
|
|
|
|
```ts
|
|
// 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()` returns one `ProviderRegistration`:
|
|
|
|
```ts
|
|
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);
|
|
},
|
|
};
|
|
}
|
|
```
|
|
|
|
The connection has three operations:
|
|
|
|
- `send(input)` accepts work and returns. Results are events, not return values.
|
|
- `onEvent(listener)` publishes session state and complete timeline snapshots.
|
|
- `close()` stops every session and releases processes, subscriptions, and pending work.
|
|
|
|
Keep the native SDK, process, and stream inside the connection implementation. Convert its output
|
|
to `ProviderEvent` objects before publishing it.
|
|
|
|
## Return models, modes, and thinking options
|
|
|
|
Paseo requests the catalog before creating a session. Return the choices needed by the agent form:
|
|
|
|
```ts
|
|
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",
|
|
},
|
|
});
|
|
}
|
|
```
|
|
|
|
Return an empty array for a category the agent does not expose. The selected `model`, `mode`, and
|
|
`thinkingOption` arrive in the session configuration.
|
|
|
|
The catalog describes choices available before a session exists. Session-specific controls come
|
|
later through `session.config`.
|
|
|
|
## Open a session
|
|
|
|
`session.open` contains the complete launch configuration: working directory, environment, system
|
|
prompt, MCP servers, tool policy, model, mode, settings, opaque provider options, and persistence
|
|
preference.
|
|
|
|
For persisted host-scoped plugin settings, use the handle returned by `server.registerSettings()`.
|
|
Read it in catalog and session callbacks, and subscribe when the provider keeps in-memory
|
|
configuration. See [settings](./reference.md#persisted-values) for the `read()` and `subscribe()`
|
|
contract.
|
|
|
|
Create the native session with the selected catalog values, then publish its effective state:
|
|
|
|
```ts
|
|
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` contains the committed values. Publish what the native agent selected after
|
|
normalization, not a copy of the request.
|
|
|
|
## Complete the first prompt
|
|
|
|
Start with message prompts. Advertise `prompt.message`, pass the content to the native agent, and
|
|
publish the resulting lifecycle:
|
|
|
|
```ts
|
|
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" });
|
|
```
|
|
|
|
Publish exactly one `session.prompt_result` for each `clientMessageId`. Copy `clientMessageId` onto
|
|
the user timeline item so Paseo replaces its optimistic message. Every started turn needs one
|
|
terminal `completed`, `failed`, or `canceled` event.
|
|
|
|
At this point the provider is usable: users can select a model, open a session, send a message, and
|
|
see the response.
|
|
|
|
## Add session-specific composer controls
|
|
|
|
Add `session.configure` only when the native agent has settings users should change after opening a
|
|
session. Publish them in `session.config` as toggle or select controls:
|
|
|
|
```ts
|
|
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 renders these controls in the composer. A user change arrives as `session.configure`; apply it
|
|
and publish the committed `session.config` before `request.completed`. The provider may change the
|
|
available controls at runtime, including after a model change.
|
|
|
|
`settings` are user-facing controls. `providerOptions` is opaque JSON supplied at session creation
|
|
for provider-specific configuration that Paseo does not render. A provider-specific token budget
|
|
belongs in one of those two places; it is not a dedicated prompt field.
|
|
|
|
## Add persistence and replay
|
|
|
|
Advertise `session.persistence` when a native session can be reopened. Open a new native session
|
|
when `session.open.persistence` is absent. Resume the identified native session when it is present.
|
|
Return an opaque persistence value in `session.opened` or `session.persistence`; Paseo stores it
|
|
without inspecting it.
|
|
|
|
When `history` is `"replay"`, publish the native session's existing `timeline.item` snapshots before
|
|
`session.ready`. Use `history: "skip"` to open without replaying old rows.
|
|
|
|
Paseo refreshes an agent by closing its current provider session and opening it again with current
|
|
configuration and persistence. Re-read credentials, environment, global configuration, and MCP
|
|
servers during `session.open`. There is no separate reload operation.
|
|
|
|
Use `restoration: "core"` when Paseo can reopen a session from persistence. A provider-owned child
|
|
uses `restoration: "parent"`; emit it as another `session.opened` event with `parentSessionId` and
|
|
recreate it while restoring the parent.
|
|
|
|
## Add commands and steering
|
|
|
|
All user input arrives through `session.prompt`:
|
|
|
|
- `input.type: "message"` carries text, images, and attachments;
|
|
- `input.type: "command"` carries a selected command and its arguments;
|
|
- `delivery: "steer"` asks you to add input to the active turn;
|
|
- `delivery: "auto"` lets you start a turn or complete a command without one.
|
|
|
|
Publish `session.commands` to add structured commands to the composer. A command can start a normal
|
|
turn or finish as a side effect with a `completed` prompt result.
|
|
|
|
Advertise only capabilities you implement. If steering is unsupported, omit `prompt.steer`; Paseo
|
|
can replace the active turn instead.
|
|
|
|
## Publish timeline items
|
|
|
|
Publish complete snapshots through `timeline.item`. Reuse an item `id` when updating streamed text,
|
|
a running tool, or a todo list. Paseo derives the live delta and keeps the normal timeline.
|
|
|
|
Built-in item types cover user and assistant messages, reasoning, tools, todos, errors,
|
|
notifications, and compaction. Use a plugin item when your provider has a presentation that those
|
|
types cannot express:
|
|
|
|
```ts
|
|
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" },
|
|
},
|
|
});
|
|
```
|
|
|
|
Register the renderer independently in `index.client.tsx`:
|
|
|
|
```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 () => {};
|
|
}
|
|
```
|
|
|
|
The same renderer can display items emitted by a provider, a client timeline transformer, or a
|
|
daemon timeline append. A renderer does not require a provider implementation.
|
|
|
|
## Adapt an ACP agent
|
|
|
|
Use the ACP shim when the agent already speaks ACP:
|
|
|
|
```ts
|
|
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 () => {};
|
|
}
|
|
```
|
|
|
|
The shim owns the ACP process, capability mapping, session lifecycle, prompts, permissions, and
|
|
timeline conversion. Do not copy that machinery into the plugin.
|
|
|
|
Use `transformers` only for vendor differences ACP cannot describe. Validate vendor payloads with
|
|
Zod and leave malformed or unrelated values unchanged:
|
|
|
|
```ts
|
|
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,
|
|
},
|
|
};
|
|
},
|
|
};
|
|
```
|
|
|
|
## Test and publish
|
|
|
|
Test the provider against the real agent, not only mocked frames:
|
|
|
|
1. install the plugin as a directory source;
|
|
2. confirm its SVG, models, modes, and settings appear;
|
|
3. create a session and complete a real prompt;
|
|
4. exercise permissions, steering, cancellation, persistence, and replay when supported;
|
|
5. render every custom timeline item on desktop and mobile-width layouts;
|
|
6. reload and remove the plugin while a session is active and confirm the session terminates.
|
|
|
|
Follow [Publish a plugin](/docs/plugins/publishing) to distribute the provider through npm or Git.
|
|
Keep agent compatibility and releases in the plugin project.
|
|
|
|
See the [plugin reference](/docs/plugins/reference#providers) for the exact runtime and SVG
|
|
rules.
|