Files
boocode/openspec/changes/v2-3-provider-lifecycle/design.md
indifferentketchup 93d3f86c2b v2.2-paseo-providers: Paseo provider stack + v2.2.1 pane-scoped chat fixes
Ship Paseo-equivalent provider snapshot, AgentComposerBar, ACP dispatch
rewrite with streaming/persist, permission prompts, and agent commands.
Follow-up: pane-scoped chat resolution, CoderMessageList tool timeline,
WS user-delta replace, and inference orphan tool_call stripping.
Archive openspec v2-2; update CHANGELOG and CURRENT.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 15:18:31 +00:00

727 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# v2.3 Provider lifecycle — design
Detailed implementation plan for Paseo-style provider registration, readiness probing, and enable/disable toggles in BooCoder.
**Audience:** Sam + future agents implementing the batch.
**Paseo reference:** `/opt/forks/paseo/packages/server/src/server/agent/` (registry, snapshot manager, generic ACP), `/opt/forks/paseo/packages/app/src/screens/settings/providers-section.tsx` (UI behavior).
---
## 1. Current state vs target
### 1.1 BooCode today (v2.2)
```
┌─────────────────┐ startup ┌──────────────────┐
│ provider- │ ───────────────► │ available_agents │ (which, version, models JSONB)
│ registry.ts │ agent-probe │ (Postgres) │
│ (7 hardcoded) │ └────────┬─────────┘
└────────┬────────┘ │
│ │
▼ ▼
┌─────────────────┐ cache miss ┌──────────────────┐
│ getProvider │ ──────────────► │ probeAcpProvider │ (full ACP session, 30s)
│ Snapshot() │ per agent │ per installed │
└────────┬────────┘ └──────────────────┘
Omit uninstalled ──► AgentComposerBar never sees them
No enabled flag
status: ready | error only
```
**Key files:**
| File | Role |
|------|------|
| `apps/coder/src/services/provider-registry.ts` | Static `PROVIDERS[]` |
| `apps/coder/src/services/agent-probe.ts` | Boot `which` + DB upsert |
| `apps/coder/src/services/provider-snapshot.ts` | Cache + cold probe + merge |
| `apps/coder/src/services/acp-spawn.ts` | Per-agent argv switch |
| `apps/coder/src/routes/providers.ts` | snapshot + refresh |
| `apps/web/src/components/AgentComposerBar.tsx` | Picker UI |
### 1.2 Target (Paseo-aligned, BooCode-native)
```
┌──────────────────┐
│ Built-in registry│──┐
│ (provider- │ │
│ registry.ts) │ │ merge at boot + on config reload
└──────────────────┘ │
┌──────────────────┐ ┌──────────────────┐
│ /data/coder- │─►│ ResolvedProvider │
│ providers.json │ │ Registry (in-mem) │
└──────────────────┘ └────────┬───────────┘
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
agent-probe (fast) getProviderSnapshot dispatch
which + version tier-1: isAvailable generic ACP for
→ available_agents tier-2: cold ACP config entries
enabled filter
Always emit entry per registered provider
loading → ready | unavailable | error
```
**Principles copied from Paseo** (`docs/providers.md` in fork):
1. **Registration ≠ installation** — config lists what you *want*; probe tells you whats *ready*.
2. **Warm until refresh** — no TTL re-probe on picker open; explicit `POST /api/providers/refresh` only.
3. **Disabled skips probe**`enabled: false``unavailable` without spawning.
4. **Config reload replaces registry** — no redeploy to add an ACP wrapper.
---
## 2. Config file: `/data/coder-providers.json`
### 2.1 Location and loading
| Env var | Default | Notes |
|---------|---------|-------|
| `CODER_PROVIDERS_PATH` | `/data/coder-providers.json` | Same bind-mount pattern as `SKILLS_ROOT`, `MCP_CONFIG_PATH` |
- BooCoder runs on **host systemd** — path resolves to `/opt/boocode/data/coder-providers.json` in dev (add to repo as `data/coder-providers.json` + `.env.host`).
- Missing file → `{}` (built-ins only, all enabled).
- Invalid JSON → log error, fall back to `{}` (do not crash boot).
- **Reload:** on `POST /api/providers/config` success, or `SIGHUP` optional later; v1: restart `boocoder.service` after manual edit is acceptable for solo use.
### 2.2 Schema (Zod)
New file: `apps/coder/src/services/provider-config.ts`
```typescript
const ProviderOverrideSchema = z.object({
extends: z.enum(['acp']).optional(), // v2.3: only 'acp' for custom; built-ins omit extends
label: z.string().min(1).optional(),
description: z.string().optional(),
command: z.array(z.string().min(1)).min(1).optional(), // [binary, ...args]
env: z.record(z.string()).optional(),
enabled: z.boolean().optional(), // default true
order: z.number().int().optional(), // UI sort key
models: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
additionalModels: z.array(z.object({ id: z.string(), label: z.string() })).optional(),
});
const CoderProvidersFileSchema = z.object({
providers: z.record(ProviderOverrideSchema).default({}),
});
```
**Rules:**
| Case | Behavior |
|------|----------|
| Built-in id (e.g. `goose`) | Override merges: `enabled`, `label`, `command` (replace spawn), `env` |
| New id + `extends: "acp"` | New registry entry; requires `label` + `command` |
| New id without `extends` | Reject at load with log (v2.3) |
| `enabled: false` on built-in | Stays in registry; snapshot `enabled: false`, status `unavailable` |
| Custom id collision with built-in | Config wins for overrides only; cannot redefine `boocode` transport |
### 2.3 Example file (ship in `data/coder-providers.json`)
```json
{
"providers": {
"goose": { "enabled": true },
"copilot": { "enabled": false },
"amp-acp": {
"extends": "acp",
"label": "Amp",
"description": "ACP wrapper for Amp",
"command": ["amp-acp"],
"enabled": true
}
}
}
```
### 2.4 Paseo parity notes
Paseo uses `~/.paseo/config.json` under `agents.providers` with the same fields (`extends`, `command`, `enabled`, `models`, …). We intentionally use a **repo-adjacent data file** instead of dotfile — matches `AGENTS.md` / skills layout and survives container/host split (coder reads host path).
---
## 3. Resolved provider registry
### 3.1 New module: `provider-config-registry.ts`
**Responsibility:** Single in-memory source of truth after merge.
```typescript
export interface ResolvedProviderDef extends ProviderDef {
id: string;
enabled: boolean;
isBuiltin: boolean;
isCustomAcp: boolean;
/** Full argv for spawn: [binary, ...args] */
launchCommand: [string, ...string[]] | null;
env: Record<string, string> | undefined;
configLabel?: string;
configDescription?: string;
}
export function buildResolvedRegistry(
builtins: ProviderDef[],
config: CoderProvidersFile,
): Map<string, ResolvedProviderDef>;
export function loadProviderConfig(path: string): CoderProvidersFile;
export function reloadProviderConfig(): void; // called after PATCH
```
**Merge algorithm** (mirror Paseo `buildProviderRegistry` / `addDerivedProviders`):
1. For each built-in in `PROVIDERS`:
- Apply config override if present
- `enabled = override.enabled !== false`
- `launchCommand` = override.command ?? default from `acp-spawn` + `install_path` at dispatch time
2. For each config key not in built-ins:
- Require `extends: "acp"`, `label`, `command`
- Insert as `isCustomAcp: true`, `transport: 'acp'`, `modelSource: 'probe'`
3. **`boocode`** always enabled; ignore `enabled: false` with warn log
**Consumers:** `agent-probe`, `provider-snapshot`, `dispatcher`, `acp-dispatch`, routes.
### 3.2 agent-probe changes
File: `apps/coder/src/services/agent-probe.ts`
- Iterate **`getResolvedProviderIds()`** instead of `PROBED_AGENT_NAMES` only.
- For custom ACP: probe `command[0]` via `which` (not agent name).
- Upsert `available_agents` for custom ids (new rows).
- Store `label`, `transport: 'acp'` from resolved def.
- Skip probe entirely when `enabled: false` (optional: delete row or keep stale — **keep row**, set `install_path null` on disable refresh).
### 3.3 Schema migration (optional column)
File: `apps/coder/src/schema.sql`
```sql
ALTER TABLE available_agents ADD COLUMN IF NOT EXISTS enabled BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE available_agents ADD COLUMN IF NOT EXISTS source TEXT DEFAULT 'builtin';
-- source: 'builtin' | 'config'
```
Mirror `enabled` from config on each probe pass. Custom providers get `source = 'config'`.
**Alternative (simpler v2.3.0):** dont add DB column; read `enabled` only from in-memory registry at snapshot time. DB holds install facts only. Prefer this for phase 1; add column if settings page needs to show state after coder restart without re-reading JSON.
---
## 4. Snapshot lifecycle
### 4.1 Type changes
Files: `apps/coder/src/services/provider-types.ts`, `apps/web/src/api/types.ts`
```typescript
export type ProviderSnapshotStatus = 'loading' | 'ready' | 'unavailable' | 'error';
export interface ProviderSnapshotEntry {
name: string;
label: string;
description?: string;
transport: string;
status: ProviderSnapshotStatus;
enabled: boolean;
installed: boolean; // binary found on last fast probe
models: ProviderModel[];
modes: ProviderMode[];
defaultModeId: string | null;
commands: AgentCommand[];
error?: string;
fetchedAt?: string; // ISO — when tier-2 probe completed
}
```
Restore `unavailable` (removed in stale cleanup — intentional regression for this batch).
### 4.2 `buildProviderEntry` rewrite
File: `apps/coder/src/services/provider-snapshot.ts`
**Stop returning `null` for uninstalled.** Always return an entry for every resolved registry id.
```
for each resolvedProvider:
if !enabled:
return { status: 'unavailable', enabled: false, installed: false, models: [], ... }
if native boocode:
return { status: 'ready', enabled: true, installed: true, models: llamaSwap, ... }
fast = agentRow?.install_path != null // or isCommandAvailable(launchCommand[0])
if !fast:
return { status: 'unavailable', enabled: true, installed: false, models: [], modes: manifest, commands: manifest }
if tier2_skipped: // see §4.3
return { status: 'ready', enabled: true, installed: true, models: from DB, modes: manifest or DB, ... }
cold ACP probe:
ok → ready + models/modes/commands merge
fail → error + error message
```
### 4.3 Two-tier probe (implements deferred work §2)
**Tier 1 — fast (always on cold read if enabled + installed):**
```typescript
async function isProviderAvailable(resolved: ResolvedProviderDef, agentRow: AgentRow): Promise<boolean> {
if (resolved.isNative) return true;
if (agentRow?.install_path) return true;
if (resolved.launchCommand) return isCommandAvailable(resolved.launchCommand[0]);
return false;
}
```
New util: `apps/coder/src/services/command-availability.ts``which`-style check (lift idea from Paseo `utils/executable.ts`, ~20 lines, no full port).
**Tier 2 — slow (ACP session):**
Run only when:
| Condition | Action |
|-----------|--------|
| `force === true` (`POST /refresh`) | Always cold probe installed enabled providers |
| `last_probed_at` older than `PROVIDER_PROBE_TTL_MS` (default 24h, env override) | Cold probe |
| DB models empty AND installed | Cold probe |
| Otherwise | Use `available_agents.models` + manifest modes/commands |
Env: `PROVIDER_PROBE_TTL_MS` default `86400000` (24h). Paseo uses warm-forever until refresh; 24h is a homelab compromise so stale model lists self-heal.
**Paseo contract (adopt explicitly):**
- Opening `AgentComposerBar` does **not** call refresh or force probe.
- `POST /api/providers/refresh` clears cache + forces tier-2 for home cwd.
- Document in `BOOCODER.md`.
### 4.4 Loading state
On cache miss, before async probe completes:
1. Return entries with `status: 'loading'` immediately (sync).
2. Singleflight inflight map (already exists) — on completion, flip to terminal status + emit…
**Tier 2 optional:** WS frame `provider_snapshot_updated` — defer to follow-up; v2.3 can rely on client polling 2s while any entry `loading` (CoderPane already polls when WS disconnected; extend: poll while snapshot has `loading`).
### 4.5 Cache keys
Keep cwd-keyed cache (`resolvedCwd = cwd ?? homedir()`). Settings UI uses snapshot with **no cwd** or explicit `cwd=~` — same as Paseo home-directory snapshot for provider management.
---
## 5. Generic ACP dispatch
### 5.1 Problem
`acp-spawn.ts` switch grows with every agent. Custom config entries cannot dispatch today.
### 5.2 Solution
File: `apps/coder/src/services/acp-spawn.ts`
```typescript
export function resolveLaunchSpec(
resolved: ResolvedProviderDef,
installPath: string | null,
): { binary: string; args: string[]; env?: Record<string, string> } | null {
if (resolved.launchCommand) {
return {
binary: resolved.launchCommand[0],
args: resolved.launchCommand.slice(1),
env: resolved.env,
};
}
// built-in fallback
const args = resolveAcpSpawnArgs(resolved.id);
if (!args || !installPath) return null;
return { binary: installPath, args, env: resolved.env };
}
```
File: `apps/coder/src/services/acp-dispatch.ts`
- Replace `resolveAcpSpawnArgs(agent)` + `spawn(installPath, args)` with `resolveLaunchSpec(resolved, installPath)`.
- Merge `env` into spawn `env: { ...process.env, ...spec.env }`.
- Dispatcher loads resolved def by task.agent name.
**Do not port** Paseo `GenericACPAgentClient` class — keep procedural dispatch + existing `acp-stream.ts`.
---
## 6. HTTP API
File: `apps/coder/src/routes/providers.ts`
| Method | Path | Body | Response |
|--------|------|------|----------|
| GET | `/api/providers/snapshot?cwd=` | — | `ProviderSnapshotEntry[]` (unchanged path) |
| POST | `/api/providers/refresh` | `{ providers?: string[] }` optional | `{ refreshed: number }` — if `providers` set, refresh subset only (Paseo pattern) |
| GET | `/api/providers/config` | — | `{ providers: Record<string, ProviderOverride> }` |
| PATCH | `/api/providers/config` | partial providers map | merged file written, registry reload, `{ ok: true }` |
| GET | `/api/providers/:id/diagnostic` | — | `{ diagnostic: string }` Tier 2 |
**PATCH semantics:** shallow merge at top level per provider id (same as Paseo `patchConfig`). Writing `enabled: false` triggers registry reload + snapshot reconcile (mark unavailable without probe).
**Proxy:** BooChat server may proxy `/api/coder/providers/*` — check `apps/server/src/index.ts` coder proxy prefix; add config routes if missing.
**Web client:** `apps/web/src/api/client.ts`
```typescript
coder: {
snapshot: ...
refreshProviders: (providers?: string[]) => ...
getProviderConfig: () => ...
patchProviderConfig: (patch) => ...
getProviderDiagnostic: (id) => ...
}
```
---
## 7. Web UI
### 7.1 Settings: Provider management drawer
New: `apps/web/src/components/coder/ProviderSettingsDrawer.tsx` (or section under existing settings)
**Behavior lifted from Paseo `providers-section.tsx`:**
| UI element | Action |
|------------|--------|
| Row per registered provider | Label, status dot, model count |
| Switch | `PATCH config { [id]: { enabled } }` |
| Refresh icon | `POST /api/providers/refresh` |
| Add provider | Opens catalog modal |
| Row click | Diagnostic sheet (optional phase 2) |
**Status labels:** Disabled · Loading · Available · Not installed · Error
Entry point: link from `AgentComposerBar` (gear icon) or CoderPane header.
### 7.2 AgentComposerBar filter
File: `apps/web/src/components/AgentComposerBar.tsx`
```typescript
const selectable = entries.filter(
(e) => e.enabled && e.status === 'ready' && e.models.length > 0
);
// boocode: allow ready with empty models if llama-swap down? keep current fallback
```
Show subtitle when current provider becomes unavailable (toast + reset to boocode).
### 7.3 Add provider modal
New: `apps/web/src/data/acp-provider-catalog.ts`
Curated entries (start with 510 you might install):
| id | command | installLink |
|----|---------|-------------|
| amp-acp | `["amp-acp"]` | github amp-acp |
| cline | `["npx","-y","cline@…","--acp"]` | cline.bot |
| pi-acp | from fork | … |
Copy **structure** from Paseo `acp-provider-catalog.ts` + `buildAcpProviderConfigPatch` — trim versions aggressively; pin only when youve verified on homelab.
Modal: search, Install → `patchProviderConfig(buildPatch(entry))``refreshProviders([entry.id])`.
**Do not port:** React Native components, remote SVG icon pipeline — use lucide fallback icon.
### 7.4 Loading UX
While any entry `status === 'loading'`, show spinner in composer provider dropdown; optional 2s poll until terminal state (reuse CoderPane poll pattern).
---
## 8. Diagnostics (Tier 2 in batch — lightweight)
Paseo `getDiagnostic()` runs version probe + short ACP initialize. For solo debugging:
File: `apps/coder/src/services/provider-diagnostic.ts`
```typescript
export async function getProviderDiagnostic(
resolved: ResolvedProviderDef,
agentRow: AgentRow | undefined,
cwd: string,
): Promise<string> {
// Plaintext report:
// - enabled, installed, binary path
// - last_probed_at, model count from DB
// - optional: 8s ACP initialize probe (reuse acp-probe with shorter timeout)
}
```
No need for Paseo `diagnostic-utils.ts` formatting library — a template string is fine.
---
## 9. Testing strategy
| Test | File |
|------|------|
| Config load + merge | `provider-config-registry.test.ts` |
| Snapshot: disabled → unavailable, no probe mock call | extend `provider-snapshot.test.ts` |
| Snapshot: uninstalled → unavailable, installed true/false | same |
| Tier-2 skip when fresh DB models | same |
| force refresh calls probe | same |
| PATCH config writes file | `routes/providers.test.ts` (optional integration) |
| resolveLaunchSpec custom command | `acp-spawn.test.ts` |
Run: `pnpm -C apps/coder test`, `npx tsc -p apps/web/tsconfig.app.json --noEmit`.
Smoke:
```bash
curl http://100.114.205.53:9502/api/providers/snapshot
curl -X PATCH http://100.114.205.53:9502/api/providers/config -d '{"providers":{"goose":{"enabled":false}}}'
curl -X POST http://100.114.205.53:9502/api/providers/refresh
```
---
## 10. Implementation phases
### Phase 1 — Config + registry (backend only)
- `provider-config.ts`, `provider-config-registry.ts`
- `data/coder-providers.json` + `CODER_PROVIDERS_PATH`
- Wire `agent-probe` to resolved ids
- Unit tests
**Exit:** custom entry in JSON → row in `available_agents` after restart.
### Phase 2 — Snapshot lifecycle
- Types: `loading`, `unavailable`, `enabled`
- Rewrite `buildProviderEntry` (never omit)
- Tier-1 fast availability
- Tier-2 skip when DB fresh
- Restore warm-cache + force refresh semantics
**Exit:** disabled goose visible in API as unavailable; picker filters it out.
### Phase 3 — Generic dispatch
- `resolveLaunchSpec`
- Dispatcher passes resolved def
- Smoke: dispatch task for config-only provider (amp-acp if installed)
### Phase 4 — HTTP config API
- GET/PATCH config
- Reload registry on PATCH
- Subset refresh
### Phase 5 — Web UI
- Provider settings drawer + toggle
- AgentComposerBar filter
- Catalog modal (minimal list)
### Phase 6 — Docs + deploy
- `BOOCODER.md` section: Provider config
- `CHANGELOG.md` entry
- `docs/DEFERRED-WORK.md` — mark cold-probe item resolved
- `pnpm -C apps/coder build && sudo systemctl restart boocoder`
---
## 11. Tier 2 follow-ups (document, dont build in v2.3)
| Item | Paseo source | When |
|------|--------------|------|
| WS `provider_snapshot_updated` | `ProviderSnapshotManager` EventEmitter | When loading poll feels hacky |
| MCP `list_providers` / `inspect_provider` | `mcp-server.ts` | When BooCoder MCP orchestration matures |
| Profile overrides (`extends: "claude"`) | `provider-registry.ts` derived providers | When you run Z.AI / multi-endpoint |
| `order` field UI sort | config schema | When catalog >10 entries |
| Per-workspace snapshot in picker | cwd param | Already partial — verify project path passed from CoderPane |
---
## 12. Tier 3 reference — what Paseo has and why we dont port it
This section is **reference only**. These are large subsystems in `/opt/forks/paseo` that solve problems BooCode doesnt have at solo scale, or that BooCode already solved differently.
### 12.1 `ACPAgentClient` base class (~2,800 lines)
**Path:** `packages/server/src/server/agent/providers/acp-agent.ts`
**What it does:** Full ACP lifecycle — spawn, initialize, session/new, streaming, permissions, tool calls, MCP injection, revert, persisted agent import, probe sessions.
**Why Paseo needs it:** Paseo is the primary runtime for dozens of providers; one abstraction reduces duplication across copilot, cursor, generic ACP, etc.
**Why BooCode skips it:** `acp-dispatch.ts` + `acp-stream.ts` + `acp-probe.ts` already cover dispatch and probe as **scripts** (~400 lines total). Replacing with the class hierarchy is a multi-week rewrite with high regression risk on v2.2 dispatch that works on homelab.
**What we take instead:** Patterns only — `isAvailable()` = resolve binary; permission waiter (already shipped); derive models/modes (already shipped).
---
### 12.2 Per-provider client classes (claude, codex, opencode, pi, copilot, cursor…)
**Paths:** `packages/server/src/server/agent/providers/*/agent.ts`, `codex-app-server-agent.ts` (5,000+ lines)
**What they do:** Native SDK/RPC integration — not just CLI spawn. Codex uses app-server RPC; Claude uses Claude Agent SDK; OpenCode manages a sidecar server.
**Why Paseo needs it:** Deep integration — voice, revert, persisted sessions, feature toggles, OAuth diagnostics.
**Why BooCode skips it:** BooCode **delegates** to existing CLIs in worktrees. No embedded SDKs. PTY path for claude/qwen is stdin pipe; ACP path uses `@agentclientprotocol/sdk` at dispatch boundary only.
**Lift risk:** Importing codex-app-server-agent would drag thousands of lines + unknown deps.
---
### 12.3 `ProviderSnapshotManager` class (full port)
**Path:** `packages/server/src/server/agent/provider-snapshot-manager.ts`
**What it does:** Per-cwd Maps, loading states, singleflight, event emitter, reconcile on registry replace, settings vs workspace refresh split.
**Why not full port:** BooCodes `provider-snapshot.ts` is ~250 lines and already has cache + inflight. **Selective lift:** loading status, reconcile on config reload, subset refresh — not a class-for-class rewrite.
---
### 12.4 React Native settings app (`packages/app`)
**Paths:** `providers-section.tsx`, `add-provider-modal.tsx`, `use-providers-snapshot.ts`
**What it does:** Mobile/desktop cross-platform provider UI with Unistyles, native Switch, adaptive sheets.
**Why BooCode skips it:** BooChat is React web + Tailwind. Port **interaction design** (toggle, status dots, add flow), not components.
---
### 12.5 Daemon config system (`patchConfig`, migrations, Zod wire messages)
**Path:** `packages/server/src/shared/messages.ts` (4000+ lines), daemon config patch RPC
**What it does:** Every settings change is a typed WS/HTTP patch to daemon with validation, persistence, broadcast.
**Why BooCode simplifies:** Single-user — PATCH writes JSON file + reloads in-process Map. No multi-client sync requirement. If BooChat and CLI both edit, last-write-wins on file is acceptable.
---
### 12.6 Full ACP catalog (30+ providers, version-pinned npx)
**Path:** `packages/app/src/data/acp-provider-catalog.ts` (~400 lines)
**Why trim:** Maintenance burden — every upstream version bump is a PR in Paseo. Solo homelab: 510 entries you actually install, update when you install.
---
### 12.7 Voice provider stack
**Path:** `packages/server/src/server/speech/*`
**Why skip:** BooCode has no voice surface; unrelated to coder provider lifecycle.
---
### 12.8 Workspace git service inside agents
**Path:** Codex client integration with `WorkspaceGitService`
**Why skip:** BooCode worktrees (`worktrees.ts`) are explicit per-task; agents run in worktree cwd. Different architecture.
---
### 12.9 OpenCode server manager sidecar
**Path:** `packages/server/src/server/agent/providers/opencode/server-manager.ts`
**What it does:** Manages long-lived OpenCode server process.
**Why skip:** BooCode spawns `opencode acp` per dispatch — stateless, simpler, good enough for single user.
---
### 12.10 Pi RPC agent + session import from JSONL
**Paths:** `packages/server/src/server/agent/providers/pi/agent.ts` (1,500+ lines)
**Why skip until needed:** Only lift if you add `pi` as a built-in with import/revert requirements. Otherwise generic ACP + `extends: "acp"` + pi-acp catalog entry suffices.
---
### 12.11 Summary table
| Paseo subsystem | Lines (approx) | BooCode v2.3 approach |
|-----------------|----------------|------------------------|
| ACPAgentClient | 2,800 | Keep acp-dispatch |
| Codex app server agent | 5,500 | Don't import |
| Provider registry merge | 700 | New 200-line module |
| Snapshot manager | 490 | Extend existing snapshot |
| Generic ACP agent | 300 | resolveLaunchSpec only |
| RN providers UI | 400 | Web drawer ~200 lines |
| MCP list_providers | 200 | Defer |
| Config wire protocol | 4,000+ | JSON file PATCH |
**Rule of thumb for solo project:** Lift **data models and lifecycle rules**, not **class hierarchies**.
---
## 13. Risk register
| Risk | Mitigation |
|------|------------|
| Custom npx provider slow cold start | Show loading; subset refresh; dont block picker on whole snapshot |
| Config file edit while coder running | PATCH API primary; manual edit requires restart (document) |
| `enabled: false` but task in flight | Allow running task to finish; block new sends (picker filter) |
| Type drift web/coder | Update both `provider-types.ts` and `api/types.ts`; optional zod parity test |
| Security: arbitrary command in config | Single-user trusted path; same trust as `AGENTS.md` — no app-layer auth |
| Re-enabling cold probe slowness on refresh | Expected; refresh is explicit user action |
---
## 14. File map (new + touched)
| Action | Path |
|--------|------|
| **New** | `apps/coder/src/services/provider-config.ts` |
| **New** | `apps/coder/src/services/provider-config-registry.ts` |
| **New** | `apps/coder/src/services/command-availability.ts` |
| **New** | `apps/coder/src/services/provider-diagnostic.ts` |
| **New** | `apps/coder/src/services/__tests__/provider-config-registry.test.ts` |
| **New** | `data/coder-providers.json` |
| **New** | `apps/web/src/data/acp-provider-catalog.ts` |
| **New** | `apps/web/src/components/coder/ProviderSettingsDrawer.tsx` |
| **New** | `apps/web/src/components/coder/AddProviderModal.tsx` |
| **Edit** | `apps/coder/src/services/provider-snapshot.ts` |
| **Edit** | `apps/coder/src/services/agent-probe.ts` |
| **Edit** | `apps/coder/src/services/acp-spawn.ts` |
| **Edit** | `apps/coder/src/services/acp-dispatch.ts` |
| **Edit** | `apps/coder/src/services/dispatcher.ts` |
| **Edit** | `apps/coder/src/routes/providers.ts` |
| **Edit** | `apps/coder/src/config.ts``CODER_PROVIDERS_PATH` |
| **Edit** | `apps/coder/.env.host` |
| **Edit** | `apps/coder/src/services/provider-types.ts` |
| **Edit** | `apps/web/src/api/types.ts` |
| **Edit** | `apps/web/src/api/client.ts` |
| **Edit** | `apps/web/src/components/AgentComposerBar.tsx` |
| **Edit** | `BOOCODER.md` |
| **Edit** | `docs/DEFERRED-WORK.md` |
---
## 15. Attribution
Design patterns from [Paseo](https://github.com/getpaseo/paseo) (`/opt/forks/paseo`), especially:
- `provider-registry.ts` — merge built-ins + config + `enabled`
- `provider-snapshot-manager.ts` — loading/unavailable/ready lifecycle
- `provider-launch-config.ts` — override schema
- `providers-section.tsx` — settings UX
- `public-docs/custom-providers.md` — config file semantics
BooCode implementation remains original code — no copy-paste of Paseo sources required; licensing treated as irrelevant per project owner directive.