* Run Pi through its RPC interface Replace the embedded Pi runtime packages with a process-backed runtime that starts pi --mode rpc, keeps the runtime port thin, and leaves provider behavior in the Paseo session and mapper modules. Add focused adapter and mapper tests plus real Pi E2E coverage for the installed binary path. * Keep Pi provider code in one module * Add Pi session imports * Enable Paseo MCP tools in Pi agents Route injected MCP servers through Pi's MCP adapter and make structured MCP results visible to model-only clients. Normalize Pi MCP proxy calls so Paseo timelines show the underlying tool names. * Fix Pi import symlink tests on Windows Create directory links explicitly in symlink cwd fixtures so Windows server tests exercise the intended path-equivalence behavior. * Use native realpath for import cwd matching Resolve import cwd matchers with native realpath so Windows symlink and junction sessions compare against the same path shape used by persisted provider data. * Fallback realpath for Windows import matching Use plain realpath when native realpath cannot resolve a cwd so symlink-equivalent persisted sessions still match on Windows runners. * Add daemon system prompt support for Pi * Fix Windows realpath import matching * Remove Pi RPC migration plan doc
401 lines
16 KiB
Markdown
401 lines
16 KiB
Markdown
# Adding a New Provider to Paseo
|
|
|
|
This guide walks through adding a new agent provider end-to-end. There are two integration patterns, and this doc covers both.
|
|
|
|
## Two Integration Patterns
|
|
|
|
### ACP (Agent Client Protocol) -- recommended
|
|
|
|
Extend `ACPAgentClient` from `packages/server/src/server/agent/providers/acp-agent.ts`. The base class handles process spawning, stdio transport, session lifecycle, streaming, permissions, and model discovery. You provide configuration (command, modes, capabilities) and optionally override `isAvailable()` for auth checks.
|
|
|
|
The only built-in ACP provider today is `copilot` (`copilot-acp-agent.ts`). `GenericACPAgentClient` (`generic-acp-agent.ts`) is also ACP-based but is used for user-defined custom providers configured via `extends: "acp"` overrides — see [docs/custom-providers.md](custom-providers.md).
|
|
|
|
### Direct
|
|
|
|
Implement the `AgentClient` and `AgentSession` interfaces from `agent-sdk-types.ts` yourself. This gives full control but requires you to handle process management, streaming, permissions, and session persistence from scratch.
|
|
|
|
Existing direct providers: `claude` (in `providers/claude/agent.ts`), `codex` (`codex-app-server-agent.ts`), `opencode` (`opencode-agent.ts`), `pi` (`providers/pi/agent.ts`). The dev-only `mock` provider (`mock-load-test-agent.ts`) is also direct.
|
|
|
|
Pi is a process-backed provider. Paseo requires the user to have the `pi` binary installed and talks to it through `pi --mode rpc`; the server package does not embed Pi's SDK/runtime packages.
|
|
|
|
Paseo's per-agent and daemon-wide system prompts are passed to Pi with `--append-system-prompt`, so Pi keeps its default coding prompt while receiving Paseo's additional instructions.
|
|
|
|
Pi MCP support depends on the open-source `pi-mcp-adapter` extension being loaded for the agent cwd. Probe with Pi RPC `get_commands`; the adapter registers an extension command named `mcp` (often with `sourceInfo.source` containing `pi-mcp-adapter`). When Paseo injects MCP servers into Pi, write a per-agent MCP config and pass it with `--mcp-config` instead of modifying user or project MCP files. For local HTTP servers such as Paseo's own `/mcp/agents` endpoint, explicitly disable adapter OAuth (`auth: false`, `oauth: false`) in the generated config.
|
|
|
|
Pi import discovery reads Pi's persisted JSONL session files because Pi RPC does not expose a recent-session listing command. Resume and full history hydration still go through `pi --mode rpc` using the session file as `nativeHandle`.
|
|
|
|
Draft metadata lookups should avoid creating provider sessions when the upstream provider has top-level APIs for that metadata. Prefer `AgentClient.listModels`, `listModes`, `listCommands`, or `listFeatures` over creating a scratch `AgentSession`; scratch sessions can show up as empty native sessions in provider import/history UIs.
|
|
|
|
---
|
|
|
|
## Provider Snapshot Refresh Contract
|
|
|
|
The daemon keeps one global provider snapshot, keyed to the home directory, for settings, selectors, and old model/mode list requests. Snapshot reads may probe providers only while the snapshot is cold. Once an entry is warm, its `ready`, `error`, or `unavailable` state stays cached until the user forces a refresh from settings/provider management.
|
|
|
|
Do not add TTL revalidation, focus-triggered refreshes, selector-open refreshes, or config-reload refreshes. Registry/config replacement may update visible metadata such as label, description, default mode, enabled state, and provider membership, but it must not spawn provider processes. If a provider needs to be re-probed after a config change, route that through the explicit settings refresh path.
|
|
|
|
Boundary tests should assert observable behavior: cold reads may call provider availability/model/mode discovery; warm reads and registry replacement must not; explicit full or targeted refreshes must.
|
|
|
|
---
|
|
|
|
## ACP Provider Checklist
|
|
|
|
### 1. Create the provider class
|
|
|
|
Create `packages/server/src/server/agent/providers/{name}-agent.ts`.
|
|
|
|
Define capabilities, modes, and a thin subclass of `ACPAgentClient`:
|
|
|
|
```ts
|
|
import type { Logger } from "pino";
|
|
import type { AgentCapabilityFlags, AgentMode } from "../agent-sdk-types.js";
|
|
import type { ProviderRuntimeSettings } from "../provider-launch-config.js";
|
|
import { ACPAgentClient } from "./acp-agent.js";
|
|
|
|
const MY_PROVIDER_CAPABILITIES: AgentCapabilityFlags = {
|
|
supportsStreaming: true,
|
|
supportsSessionPersistence: true,
|
|
supportsDynamicModes: true,
|
|
supportsMcpServers: true,
|
|
supportsReasoningStream: true,
|
|
supportsToolInvocations: true,
|
|
};
|
|
|
|
const MY_PROVIDER_MODES: AgentMode[] = [
|
|
{
|
|
id: "default",
|
|
label: "Default",
|
|
description: "Standard agent mode",
|
|
},
|
|
// Add more modes as needed
|
|
];
|
|
|
|
type MyProviderClientOptions = {
|
|
logger: Logger;
|
|
runtimeSettings?: ProviderRuntimeSettings;
|
|
};
|
|
|
|
export class MyProviderACPAgentClient extends ACPAgentClient {
|
|
constructor(options: MyProviderClientOptions) {
|
|
super({
|
|
provider: "my-provider", // Must match the ID used everywhere else
|
|
logger: options.logger,
|
|
runtimeSettings: options.runtimeSettings,
|
|
defaultCommand: ["my-agent-binary", "--acp"], // CLI command to spawn
|
|
defaultModes: MY_PROVIDER_MODES,
|
|
capabilities: MY_PROVIDER_CAPABILITIES,
|
|
});
|
|
}
|
|
|
|
// Override isAvailable() if the provider needs specific auth/env vars
|
|
override async isAvailable(): Promise<boolean> {
|
|
if (!(await super.isAvailable())) {
|
|
return false; // Binary not found
|
|
}
|
|
return Boolean(process.env["MY_PROVIDER_API_KEY"]);
|
|
}
|
|
}
|
|
```
|
|
|
|
The `super.isAvailable()` call checks that the binary from `defaultCommand` is on `$PATH`. Override only to add credential checks on top.
|
|
|
|
For reference, here is how Copilot does it -- no auth override needed because the CLI handles auth itself:
|
|
|
|
```ts
|
|
export class CopilotACPAgentClient extends ACPAgentClient {
|
|
constructor(options: CopilotACPAgentClientOptions) {
|
|
super({
|
|
provider: "copilot",
|
|
logger: options.logger,
|
|
runtimeSettings: options.runtimeSettings,
|
|
defaultCommand: ["copilot", "--acp"],
|
|
defaultModes: COPILOT_MODES,
|
|
capabilities: COPILOT_CAPABILITIES,
|
|
});
|
|
}
|
|
|
|
override async isAvailable(): Promise<boolean> {
|
|
return super.isAvailable();
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Add to the provider manifest
|
|
|
|
In `packages/server/src/server/agent/provider-manifest.ts`, add mode definitions with UI metadata (icons, color tiers) and a provider definition entry.
|
|
|
|
First, define the modes with visual metadata:
|
|
|
|
```ts
|
|
const MY_PROVIDER_MODES: AgentProviderModeDefinition[] = [
|
|
{
|
|
id: "default",
|
|
label: "Default",
|
|
description: "Standard agent mode",
|
|
icon: "ShieldCheck",
|
|
colorTier: "safe",
|
|
},
|
|
{
|
|
id: "autonomous",
|
|
label: "Autonomous",
|
|
description: "Runs without prompting",
|
|
icon: "ShieldOff",
|
|
colorTier: "dangerous",
|
|
},
|
|
];
|
|
```
|
|
|
|
Available `colorTier` values: `"safe"`, `"moderate"`, `"dangerous"`, `"planning"`.
|
|
Available `icon` values: `"ShieldCheck"`, `"ShieldAlert"`, `"ShieldOff"`.
|
|
|
|
Then add to the `AGENT_PROVIDER_DEFINITIONS` array:
|
|
|
|
```ts
|
|
export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
|
|
// ... existing providers ...
|
|
{
|
|
id: "my-provider",
|
|
label: "My Provider",
|
|
description: "Short description of the provider",
|
|
defaultModeId: "default",
|
|
modes: MY_PROVIDER_MODES,
|
|
// Optional: enable voice
|
|
voice: {
|
|
enabled: true,
|
|
defaultModeId: "default",
|
|
defaultModel: "some-model",
|
|
},
|
|
},
|
|
];
|
|
```
|
|
|
|
### 3. Add the factory to the provider registry
|
|
|
|
In `packages/server/src/server/agent/provider-registry.ts`, import your class and add a factory entry to `PROVIDER_CLIENT_FACTORIES`:
|
|
|
|
```ts
|
|
import { MyProviderACPAgentClient } from "./providers/my-provider-agent.js";
|
|
|
|
const PROVIDER_CLIENT_FACTORIES: Record<string, ProviderClientFactory> = {
|
|
// ... existing factories ...
|
|
"my-provider": (logger, runtimeSettings) =>
|
|
new MyProviderACPAgentClient({
|
|
logger,
|
|
runtimeSettings,
|
|
}),
|
|
};
|
|
```
|
|
|
|
The factory is invoked with `(logger, runtimeSettings, options)`; `options.workspaceGitService` is also available if you need it (see the `codex` factory for an example). The registry already passes the per-provider runtime settings slice through, so you don't index into the map yourself.
|
|
|
|
### 4. Add a provider icon (app)
|
|
|
|
Create `packages/app/src/components/icons/my-provider-icon.tsx` following the pattern from existing icons (e.g., `claude-icon.tsx`):
|
|
|
|
```tsx
|
|
import Svg, { Path } from "react-native-svg";
|
|
|
|
interface MyProviderIconProps {
|
|
size?: number;
|
|
color?: string;
|
|
}
|
|
|
|
export function MyProviderIcon({ size = 16, color = "currentColor" }: MyProviderIconProps) {
|
|
return (
|
|
<Svg width={size} height={size} viewBox="0 0 24 24" fill={color}>
|
|
<Path d="..." />
|
|
</Svg>
|
|
);
|
|
}
|
|
```
|
|
|
|
Then register it in `packages/app/src/components/provider-icons.ts` by adding an entry to the existing `PROVIDER_ICONS` map (which already covers the built-in providers):
|
|
|
|
```ts
|
|
import { MyProviderIcon } from "@/components/icons/my-provider-icon";
|
|
|
|
const PROVIDER_ICONS: Record<string, typeof Bot> = {
|
|
// ... existing entries ...
|
|
"my-provider": MyProviderIcon as unknown as typeof Bot,
|
|
};
|
|
```
|
|
|
|
If no icon is registered, `getProviderIcon()` falls back to a generic `Bot` icon from lucide.
|
|
|
|
### 5. Add E2E test config
|
|
|
|
In `packages/server/src/server/daemon-e2e/agent-configs.ts`, add your provider:
|
|
|
|
```ts
|
|
export const agentConfigs = {
|
|
// ... existing configs ...
|
|
"my-provider": {
|
|
provider: "my-provider",
|
|
model: "default-model-id",
|
|
modes: {
|
|
full: "autonomous", // Mode with no permission prompts
|
|
ask: "default", // Mode that requires permission approval
|
|
},
|
|
},
|
|
} as const satisfies Record<string, AgentTestConfig>;
|
|
```
|
|
|
|
Add an availability check in `isProviderAvailable()`. Note `isCommandAvailable` is async, so all branches `await` it:
|
|
|
|
```ts
|
|
case "my-provider":
|
|
return (
|
|
(await isCommandAvailable("my-agent-binary")) &&
|
|
Boolean(process.env.MY_PROVIDER_API_KEY)
|
|
);
|
|
```
|
|
|
|
Add to the `allProviders` array (current built-ins are `claude`, `codex`, `copilot`, `opencode`, `pi`):
|
|
|
|
```ts
|
|
export const allProviders: AgentProvider[] = [
|
|
"claude",
|
|
"codex",
|
|
"copilot",
|
|
"opencode",
|
|
"pi",
|
|
"my-provider",
|
|
];
|
|
```
|
|
|
|
### 6. Run typecheck
|
|
|
|
```bash
|
|
npm run typecheck
|
|
```
|
|
|
|
This is required after every change per project rules.
|
|
|
|
---
|
|
|
|
## Direct Provider Checklist
|
|
|
|
If your agent does not speak ACP, implement the interfaces from `agent-sdk-types.ts` directly.
|
|
|
|
### Interfaces to implement
|
|
|
|
The interfaces below are abridged signatures — read `agent-sdk-types.ts` for the full source of truth (option bag types, generics, etc.).
|
|
|
|
**`AgentClient`** -- factory for sessions and model/mode listing:
|
|
|
|
```ts
|
|
interface AgentClient {
|
|
readonly provider: AgentProvider;
|
|
readonly capabilities: AgentCapabilityFlags;
|
|
createSession(
|
|
config: AgentSessionConfig,
|
|
launchContext?: AgentLaunchContext,
|
|
options?: AgentCreateSessionOptions,
|
|
): Promise<AgentSession>;
|
|
resumeSession(
|
|
handle: AgentPersistenceHandle,
|
|
overrides?: Partial<AgentSessionConfig>,
|
|
launchContext?: AgentLaunchContext,
|
|
): Promise<AgentSession>;
|
|
listModels(options: ListModelsOptions): Promise<AgentModelDefinition[]>;
|
|
isAvailable(): Promise<boolean>;
|
|
// Optional:
|
|
listModes?(options: ListModesOptions): Promise<AgentMode[]>;
|
|
listPersistedAgents?(options?: ListPersistedAgentsOptions): Promise<PersistedAgentDescriptor[]>;
|
|
getDiagnostic?(): Promise<{ diagnostic: string }>;
|
|
}
|
|
```
|
|
|
|
**`AgentSession`** -- a running agent conversation:
|
|
|
|
```ts
|
|
interface AgentSession {
|
|
readonly provider: AgentProvider;
|
|
readonly id: string | null;
|
|
readonly capabilities: AgentCapabilityFlags;
|
|
readonly features?: AgentFeature[];
|
|
run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult>;
|
|
startTurn(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<{ turnId: string }>;
|
|
subscribe(callback: (event: AgentStreamEvent) => void): () => void;
|
|
streamHistory(): AsyncGenerator<AgentStreamEvent>;
|
|
getRuntimeInfo(): Promise<AgentRuntimeInfo>;
|
|
getAvailableModes(): Promise<AgentMode[]>;
|
|
getCurrentMode(): Promise<string | null>;
|
|
setMode(modeId: string): Promise<void>;
|
|
getPendingPermissions(): AgentPermissionRequest[];
|
|
respondToPermission(
|
|
requestId: string,
|
|
response: AgentPermissionResponse,
|
|
): Promise<AgentPermissionResult | void>;
|
|
describePersistence(): AgentPersistenceHandle | null;
|
|
interrupt(): Promise<void>;
|
|
close(): Promise<void>;
|
|
// Optional:
|
|
listCommands?(): Promise<AgentSlashCommand[]>;
|
|
setModel?(modelId: string | null): Promise<void>;
|
|
setThinkingOption?(thinkingOptionId: string | null): Promise<void>;
|
|
setFeature?(featureId: string, value: unknown): Promise<void>;
|
|
tryHandleOutOfBand?(prompt: AgentPromptInput): {
|
|
run(ctx: { emit: (event: AgentStreamEvent) => void }): Promise<void>;
|
|
} | null;
|
|
}
|
|
```
|
|
|
|
### Steps
|
|
|
|
1. Create `packages/server/src/server/agent/providers/{name}-agent.ts` implementing both interfaces
|
|
2. Add to the provider manifest (same as ACP step 2 above)
|
|
3. Add factory to the registry (same as ACP step 3 above)
|
|
4. Add icon (same as ACP step 4 above)
|
|
5. Add E2E config (same as ACP step 5 above)
|
|
6. Run typecheck
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
### Manual testing with the CLI
|
|
|
|
Start the daemon if not already running, then:
|
|
|
|
```bash
|
|
# Launch an agent with your provider
|
|
paseo run --provider my-provider
|
|
|
|
# Launch with a specific model and mode
|
|
paseo run --provider my-provider --model some-model --mode default
|
|
|
|
# List running agents
|
|
paseo ls -a -g
|
|
|
|
# Check if the provider reports models
|
|
paseo models --provider my-provider
|
|
```
|
|
|
|
### E2E test patterns
|
|
|
|
The E2E configs in `agent-configs.ts` expose two helpers:
|
|
|
|
- `getFullAccessConfig(provider)` -- returns config for a session with no permission prompts
|
|
- `getAskModeConfig(provider)` -- returns config for a session that triggers permission requests
|
|
|
|
Tests use `isProviderAvailable(provider)` to skip when the binary or credentials are missing, so CI will not fail for providers that are not installed.
|
|
|
|
---
|
|
|
|
## Gotchas
|
|
|
|
**Mode IDs can be URIs.** ACP providers like Copilot use full URIs as mode IDs (e.g., `"https://agentclientprotocol.com/protocol/session-modes#agent"`). Never assume mode IDs are simple strings. The manifest `defaultModeId` must match exactly.
|
|
|
|
**Models and modes are discovered dynamically.** ACP providers report available models and modes at runtime via the protocol. The static definitions in `provider-manifest.ts` are used for UI scaffolding (icons, color tiers) but the runtime values from the agent process are the source of truth.
|
|
|
|
**`AgentProvider` is always `string`.** The type alias is `type AgentProvider = string`. Provider IDs are validated against the manifest at runtime, not at the type level.
|
|
|
|
**Auth patterns vary.** Some providers need API keys in env vars (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`), some use OAuth tokens (`CLAUDE_CODE_OAUTH_TOKEN`), some use auth files (`~/.codex/auth.json`), and some handle auth entirely in their CLI binary (Copilot). Your `isAvailable()` method should check whatever is needed.
|
|
|
|
**The manifest mode list and the agent class mode list are separate.** The manifest in `provider-manifest.ts` includes UI metadata (`icon`, `colorTier`). The agent class defines modes without UI metadata (just `id`, `label`, `description`). Keep them in sync.
|
|
|
|
**`defaultCommand` is a tuple.** The first element is the binary name, the rest are default arguments. The base class uses this to find the executable and spawn the process.
|
|
|
|
**Runtime settings can override the command.** Users can configure custom binary paths or environment variables per provider via `ProviderRuntimeSettings`. Your factory in the registry should pass `runtimeSettings?.["your-provider"]` through to the constructor.
|