Files
paseo/docs/providers.md
Mohamed Boudra 132e572d6e Rewind chat or files from any user message (#1154)
* Add Pi extension-launch plumbing for in-process tree primitives

* Add Rewind controls for agent sessions

* Preserve SDK checkpoint env var so Claude file rewind works

The SDK injects CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true when file checkpointing is enabled. The earlier parent-env stripping from c4782ec71c was too broad and removed that child flag along with real parent-session markers.

Keep stripping parent markers, but preserve the checkpoint flag so the SDK rewind path can run directly. With that boundary fixed, the JSONL file-rewind fallback is no longer needed.

* Update OpenCode history replay tests for rewind state

* Show Rewind actions in an adaptive sheet

* Fix rewind semantics across providers

* Refactor Pi IDs through extension capture

* Restore user message to composer after rewind

* Fix Claude rewind tool result mapping

* Fix Claude rewind session switch roundtrip

* Fix rewind and optimistic message regressions

* Tighten rewind test discipline

* Fix OpenCode optimistic message reconciliation

* Reconcile optimistic user messages by marker

* Reorder user message actions

* Unify provider user message contract

* Add Claude rewind flow Playwright contract

* Add Codex rewind flow Playwright contract

* Add OpenCode rewind flow Playwright contract

* Add Pi rewind flow Playwright contract

* Remove real-provider rewind Playwright contracts

* Gate real-provider rewind specs from CI Playwright
2026-05-25 17:11:34 +08:00

16 KiB

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

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.

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.

Pi RPC extension UI dialog requests (select, input, editor, confirm) are bridged into Paseo question permissions and answered with extension_ui_response. Fire-and-forget extension UI requests such as notifications are intentionally ignored by the provider adapter unless Paseo grows first-class UI for them.

OpenCode MCP injection is dynamic and session-scoped. Call OpenCode's mcp.add endpoint with the MCP server config and do not follow it with mcp.connect; connect only toggles MCP servers already present in OpenCode's own config. New OpenCode versions return McpServerNotFoundError/404 for connect after a dynamic add because the server is not config-backed, while older versions silently swallowed the same missing-config path.

OpenCode owns user message IDs. Do not pass Paseo-generated IDs to OpenCode prompt APIs; let OpenCode create msg* IDs and record the user timeline item from the message.updated event.

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:

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:

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:

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:

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:

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):

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):

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:

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:

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):

export const allProviders: AgentProvider[] = [
  "claude",
  "codex",
  "copilot",
  "opencode",
  "pi",
  "my-provider",
];

6. Run typecheck

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:

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:

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:

# 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.