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>
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
Per-batch documentation convention adopted v1.13.15-openspec.
|
||||
|
||||
**Agent entry point:** `AGENTS.md` at repo root. **Architecture diagram:** `docs/ARCHITECTURE.md`.
|
||||
|
||||
Lift source: Fission-AI/OpenSpec directory layout. **No CLI dependency** — just
|
||||
the folder shape. Full OpenSpec lifecycle adoption is a future v1.14+ batch.
|
||||
|
||||
|
||||
5
openspec/changes/archived/v2.2-paseo-providers.md
Normal file
5
openspec/changes/archived/v2.2-paseo-providers.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# v2.2-paseo-providers
|
||||
|
||||
**Status:** Shipped (`v2.2-paseo-providers`, `v2.2.1-pane-scoped-chats`). Archived.
|
||||
|
||||
Follow-up fixes shipped as `v2.2.1-pane-scoped-chats` (pane-scoped chats, tool UI, WS delta, inference payload).
|
||||
726
openspec/changes/v2-3-provider-lifecycle/design.md
Normal file
726
openspec/changes/v2-3-provider-lifecycle/design.md
Normal file
@@ -0,0 +1,726 @@
|
||||
# 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 what’s *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):** don’t 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 5–10 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 you’ve 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, don’t 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 don’t port it
|
||||
|
||||
This section is **reference only**. These are large subsystems in `/opt/forks/paseo` that solve problems BooCode doesn’t 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:** BooCode’s `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: 5–10 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; don’t 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.
|
||||
61
openspec/changes/v2-3-provider-lifecycle/proposal.md
Normal file
61
openspec/changes/v2-3-provider-lifecycle/proposal.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# v2.3 Provider lifecycle (Paseo-style registry)
|
||||
|
||||
**Status:** Planned
|
||||
**Depends on:** v2.2 Paseo providers (snapshot, modes, commands, ACP dispatch)
|
||||
**Reference fork:** `/opt/forks/paseo`
|
||||
**Related deferred work:** [`docs/DEFERRED-WORK.md`](../../../docs/DEFERRED-WORK.md) §2 (cold-probe skip)
|
||||
|
||||
## Why
|
||||
|
||||
BooCode v2.2 copied Paseo’s **snapshot wire shape** (modes, thinking, commands) but not Paseo’s **provider lifecycle**:
|
||||
|
||||
- Providers are hardcoded in `provider-registry.ts`; adding one requires a code change and redeploy.
|
||||
- Uninstalled agents **disappear** from the picker instead of showing “not installed.”
|
||||
- There is no **enable/disable** toggle — every probed binary appears.
|
||||
- Every snapshot cache miss runs a **full cold ACP probe** for all installed agents (5–30s).
|
||||
|
||||
Paseo’s model (see `/opt/forks/paseo/public-docs/providers.md`) treats providers as **registered entries** in a config-backed registry, then probes the machine for readiness, then lets the user toggle visibility. That fits a one-person homelab: edit JSON, refresh, flip a switch — no TypeScript deploy for each new ACP CLI.
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
1. **Config file** `/data/coder-providers.json` — add/disable/custom ACP providers without code changes
|
||||
2. **Merged registry** — built-ins + config overrides at runtime
|
||||
3. **Snapshot lifecycle** — `loading` | `ready` | `unavailable` | `error`; always list registered providers; `enabled` flag
|
||||
4. **Two-tier probe** — fast binary check vs slow ACP session (DB `last_probed_at` gate)
|
||||
5. **Generic ACP dispatch** — config entries spawn via `{ command, env }` without new `acp-spawn` cases
|
||||
6. **HTTP API** — read/patch config, per-provider refresh, optional diagnostic
|
||||
7. **Web UI** — settings drawer: provider list, enable toggle, refresh, add-from-catalog (curated ~5–10 entries)
|
||||
8. **Tests + docs** — snapshot unit tests, `BOOCODER.md` refresh contract
|
||||
|
||||
### Out of scope (this batch)
|
||||
|
||||
- Full Paseo ACP catalog (30+ agents) — curate a small local catalog only
|
||||
- React Native settings app port
|
||||
- Replacing `acp-dispatch.ts` with Paseo’s `ACPAgentClient` hierarchy
|
||||
- Voice provider stack
|
||||
- MCP `list_providers` / `inspect_provider` tools (Tier 2 follow-up)
|
||||
- WS push of snapshot updates (Tier 2 follow-up)
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Multi-user provider prefs (single-user homelab)
|
||||
- Installing CLIs from the UI (link to install instructions only, like Paseo)
|
||||
- Removing `available_agents` table — keep it as probe cache, extend with `enabled` or mirror config
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Add `amp-acp` via catalog → appears in picker after refresh without coder redeploy
|
||||
- Disable goose in settings → gone from picker, still visible as “Disabled” in settings
|
||||
- opencode not on PATH → shows “Not installed” in settings, hidden from picker
|
||||
- Second snapshot open within warm window completes in <500ms (no ACP spawns)
|
||||
- `POST /api/providers/refresh` still runs full cold probe
|
||||
- Existing v2.2 dispatch (cursor, opencode, claude, qwen) unchanged for built-ins
|
||||
|
||||
## Deliverables
|
||||
|
||||
| Doc | Purpose |
|
||||
|-----|---------|
|
||||
| [`design.md`](./design.md) | Full architecture, schemas, file map, Tier 3 reference |
|
||||
| [`tasks.md`](./tasks.md) | Numbered implementation checklist |
|
||||
75
openspec/changes/v2-3-provider-lifecycle/tasks.md
Normal file
75
openspec/changes/v2-3-provider-lifecycle/tasks.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# v2.3 Provider lifecycle — tasks
|
||||
|
||||
Implement in phase order from [`design.md`](./design.md). Do not commit unless Sam asks.
|
||||
|
||||
## Phase 1 — Config + registry
|
||||
|
||||
- [ ] 1.1 Add `CODER_PROVIDERS_PATH` to `apps/coder/src/config.ts` (default `/data/coder-providers.json`)
|
||||
- [ ] 1.2 Add `data/coder-providers.json` example + wire in `apps/coder/.env.host`
|
||||
- [ ] 1.3 Implement `provider-config.ts` (Zod schema + load/merge/save)
|
||||
- [ ] 1.4 Implement `provider-config-registry.ts` (`buildResolvedRegistry`, module singleton + reload)
|
||||
- [ ] 1.5 Unit tests: built-in override, custom ACP add, enabled false, invalid entry skipped
|
||||
- [ ] 1.6 Update `agent-probe.ts` to iterate resolved registry (include custom ids, respect enabled)
|
||||
|
||||
## Phase 2 — Snapshot lifecycle
|
||||
|
||||
- [ ] 2.1 Extend `ProviderSnapshotEntry` / status union in coder + web types (`loading`, `unavailable`, `enabled`)
|
||||
- [ ] 2.2 Add `command-availability.ts` (`isCommandAvailable`)
|
||||
- [ ] 2.3 Rewrite `buildProviderEntry`: never return null; handle disabled/uninstalled/loading
|
||||
- [ ] 2.4 Implement tier-2 skip using `available_agents.last_probed_at` + `PROVIDER_PROBE_TTL_MS`
|
||||
- [ ] 2.5 Return `loading` entries synchronously on cache miss; complete via inflight promise
|
||||
- [ ] 2.6 Extend `provider-snapshot.test.ts` for disabled, uninstalled, fresh DB skip, force refresh
|
||||
- [ ] 2.7 Verify warm cache: second snapshot call does not invoke `probeAcpProvider` (mock assert)
|
||||
|
||||
## Phase 3 — Generic dispatch
|
||||
|
||||
- [ ] 3.1 Add `resolveLaunchSpec()` to `acp-spawn.ts`
|
||||
- [ ] 3.2 Wire `acp-dispatch.ts` to use launch spec + env merge
|
||||
- [ ] 3.3 Wire `dispatcher.ts` to load resolved def by agent name
|
||||
- [ ] 3.4 Unit test: custom command argv reaches spawn
|
||||
- [ ] 3.5 Smoke: task dispatch for one custom catalog provider (if installed on host)
|
||||
|
||||
## Phase 4 — HTTP API
|
||||
|
||||
- [ ] 4.1 `GET /api/providers/config`
|
||||
- [ ] 4.2 `PATCH /api/providers/config` (merge + write file + reload registry + clear snapshot cache)
|
||||
- [ ] 4.3 `POST /api/providers/refresh` optional body `{ providers?: string[] }`
|
||||
- [ ] 4.4 `GET /api/providers/:id/diagnostic` (plaintext report)
|
||||
- [ ] 4.5 Extend `apps/web/src/api/client.ts` coder namespace
|
||||
- [ ] 4.6 Confirm BooChat proxy forwards new routes (or document direct :9502)
|
||||
|
||||
## Phase 5 — Web UI
|
||||
|
||||
- [ ] 5.1 Create `apps/web/src/data/acp-provider-catalog.ts` (5–10 curated entries)
|
||||
- [ ] 5.2 `AddProviderModal.tsx` — search, install → patch + refresh subset
|
||||
- [ ] 5.3 `ProviderSettingsDrawer.tsx` — list, status, toggle, refresh, link to add
|
||||
- [ ] 5.4 Entry point from CoderPane / AgentComposerBar (gear or settings link)
|
||||
- [ ] 5.5 Filter `AgentComposerBar` selectable providers (`enabled && ready`)
|
||||
- [ ] 5.6 Loading state while snapshot entries `loading` (poll or one-shot refetch)
|
||||
- [ ] 5.7 `npx tsc -p apps/web/tsconfig.app.json --noEmit`
|
||||
|
||||
## Phase 6 — Docs, deploy, closeout
|
||||
|
||||
- [ ] 6.1 `BOOCODER.md` — config file, refresh contract, enable/disable
|
||||
- [ ] 6.2 Update `docs/DEFERRED-WORK.md` — mark tier-2 cold-probe item addressed
|
||||
- [ ] 6.3 `CHANGELOG.md` entry when tagged
|
||||
- [ ] 6.4 `pnpm -C apps/coder test && pnpm -C apps/coder build`
|
||||
- [ ] 6.5 `sudo systemctl restart boocoder`
|
||||
- [ ] 6.6 Smoke via Tailscale:
|
||||
- `curl http://100.114.205.53:9502/api/providers/snapshot`
|
||||
- PATCH disable goose → absent from composer, visible in settings
|
||||
- POST refresh → models repopulate
|
||||
- Add catalog entry → appears after refresh
|
||||
|
||||
## Optional (same batch if time)
|
||||
|
||||
- [ ] O.1 WS frame `provider_snapshot_updated` (skip polling)
|
||||
- [ ] O.2 `available_agents.enabled` column mirror
|
||||
- [ ] O.3 Diagnostic sheet UI (row click → modal)
|
||||
|
||||
## Explicitly out of scope
|
||||
|
||||
- Port Paseo `ACPAgentClient` / per-provider SDK clients (see design §12)
|
||||
- Full 30+ ACP catalog
|
||||
- MCP `list_providers` tools
|
||||
- Voice providers
|
||||
Reference in New Issue
Block a user