diff --git a/docs/provider-adapter/findings.md b/docs/provider-adapter/findings.md new file mode 100644 index 0000000..de4c480 --- /dev/null +++ b/docs/provider-adapter/findings.md @@ -0,0 +1,62 @@ +# Agent Provider Adapter — Findings + +## Architecture Exploration (2026-03-11) + +### Claude-Specific Coupling Severity Map + +Full codebase exploration of 13+ files revealed coupling at 4 severity levels: + +#### CRITICAL (hardcoded SDK, must abstract) + +| File | Coupling | Impact | +|------|----------|--------| +| `sidecar/agent-runner.ts` | Imports `@anthropic-ai/claude-agent-sdk`, calls `query()`, hardcoded `findClaudeCli()` | Entire sidecar is Claude-only. Must become `claude-runner.ts`. Other providers get own runners. | +| `bterminal-core/src/sidecar.rs` | `AgentQueryOptions` struct has no `provider` field. `SidecarCommand` hardcodes `agent-runner.mjs` path. | Must add `provider: String` field. Runner selection must be provider-based. | +| `src/lib/adapters/sdk-messages.ts` | `parseMessage()` assumes Claude SDK JSON format (assistant/user/result types, subagent tool names like `dispatch_agent`) | Must become `claude-messages.ts`. Other providers get own parsers. Registry selects by provider. | + +#### HIGH (TS mirror types, provider-specific commands) + +| File | Coupling | Impact | +|------|----------|--------| +| `src/lib/adapters/agent-bridge.ts` | `AgentQueryOptions` interface mirrors Rust struct — no provider field. `queryAgent()` passes options directly. | Add `provider` field. Options shape stays generic (provider_config blob). | +| `src-tauri/src/lib.rs` | `claude_list_profiles`, `claude_list_skills`, `claude_read_skill` commands are Claude-specific. | Keep as-is — they're provider-specific commands, not generic agent commands. UI gates by capability. | +| `src/lib/adapters/claude-bridge.ts` | `listClaudeProfiles()`, `listClaudeSkills()` — provider-specific adapter. | Stays as `claude-bridge.ts`. Other providers get own bridges. Provider-bridge.ts for generic routing. | + +#### MEDIUM (provider-aware routing, UI rendering) + +| File | Coupling | Impact | +|------|----------|--------| +| `src/lib/agent-dispatcher.ts` | `handleAgentMessage()` calls `parseMessage()` (Claude-specific). Subagent tool names hardcoded (`dispatch_agent`). | Route through message adapter registry. Subagent detection becomes provider-capability. | +| `src/lib/components/Agent/AgentPane.svelte` | Profile selector, skill autocomplete, Claude-specific tool names in rendering logic. | Gate by `ProviderCapabilities`. No `if(provider==='claude')` — use `capabilities.hasProfiles`. | +| `src/lib/components/Workspace/ClaudeSession.svelte` | Name says "Claude" but logic is mostly generic (session management, prompt, AgentPane). | Rename to `AgentSession.svelte`. Add provider prop. | + +#### LOW (mostly generic already) + +| File | Coupling | Impact | +|------|----------|--------| +| `src/lib/stores/agents.svelte.ts` | AgentMessage type is already generic (text, tool_call, tool_result). No Claude-specific logic. | No changes needed. Common AgentMessage type stays. | +| `src/lib/stores/health.svelte.ts` | Tracks activity/cost/context per project. Provider-agnostic. | No changes needed. | +| `src/lib/stores/conflicts.svelte.ts` | File overlap detection. Provider-agnostic (operates on tool_call file paths). | No changes needed. | +| `bterminal-relay/` | Forwards AgentQueryOptions as-is. No provider logic. | No changes needed (will forward `provider` field transparently). | + +### Key Design Insights + +1. **Sidecar is the natural boundary**: Each provider needs its own JS runner because SDKs are incompatible (Claude Agent SDK vs Codex CLI vs Ollama REST). The Rust sidecar manager selects which runner to spawn based on `provider` field. + +2. **Message format is the main divergence**: Claude SDK emits structured JSON (assistant/user/result with specific fields). Codex CLI has different output format. Ollama uses OpenAI-compatible streaming. Per-provider message adapters normalize to common AgentMessage. + +3. **Settings are per-provider + per-project**: Global defaults (API keys, model preferences) are per-provider. Project-level setting is just "which provider to use" (with override for model). Current SettingsTab has room for a collapsible Providers section without needing tabs. + +4. **Capability flags eliminate provider switches**: Instead of `if (provider === 'claude') showProfiles()`, use `if (capabilities.hasProfiles) showProfiles()`. This means adding a new provider only requires registering its capabilities — no UI code changes. + +5. **env var stripping is provider-specific**: Claude needs CLAUDE* vars stripped (nesting detection). Codex may need CODEX* stripped. Ollama needs nothing stripped. This is part of provider config, not generic logic. + +### Risk Assessment + +| Risk | Likelihood | Mitigation | +|------|-----------|------------| +| Rename breaks imports across 20+ files | High | Do renames one-at-a-time with full grep verification. Run tests after each. | +| AgentQueryOptions Rust/TS mismatch | Medium | Add provider field to both simultaneously. Default to 'claude'. | +| Message parser regression | Medium | sdk-messages.ts has 25 tests. Copy tests to claude-messages.ts test file. All must pass. | +| Settings persistence migration | Low | New settings keys (provider defaults) — no migration needed, just new keys. | +| UI regression from capability gating | Medium | Start with Claude capabilities = all true. Verify AgentPane renders identically. | diff --git a/docs/provider-adapter/progress.md b/docs/provider-adapter/progress.md new file mode 100644 index 0000000..2ceffb5 --- /dev/null +++ b/docs/provider-adapter/progress.md @@ -0,0 +1,25 @@ +# Agent Provider Adapter — Progress + +## Session Log + +### 2026-03-11 — Planning Phase + +**Duration:** ~30 min + +**What happened:** +1. Explored 13+ files across Rust backend, TypeScript bridges, Svelte UI, and JS sidecar to map Claude-specific coupling +2. Classified coupling into 4 severity levels (CRITICAL/HIGH/MEDIUM/LOW) +3. Ran /ultra-think for deep architectural analysis — evaluated 3 design options for sidecar routing, message adapters, and settings UI +4. Made 6 architecture decisions (PA-1 through PA-6) +5. Created 3-phase implementation plan (16 + 5 + 3 tasks) +6. Created planning files: task_plan.md, findings.md, progress.md + +**Architecture decisions made:** +- PA-1: Per-provider sidecar binaries (not single multi-SDK bundle) +- PA-2: Generic provider_config blob in AgentQueryOptions +- PA-3: Per-provider message adapter files → common AgentMessage type +- PA-4: Provider selection per-project with global default +- PA-5: Capability flags drive UI rendering (not provider ID checks) +- PA-6: Providers section in SettingsTab scroll (not inner tabs) + +**Status:** Planning complete. Ready for Phase 1 implementation. diff --git a/docs/provider-adapter/task_plan.md b/docs/provider-adapter/task_plan.md new file mode 100644 index 0000000..476d1f8 --- /dev/null +++ b/docs/provider-adapter/task_plan.md @@ -0,0 +1,134 @@ +# Agent Provider Adapter — Task Plan + +## Goal + +Multi-provider agent support (Claude Code, Codex CLI, Ollama) via adapter pattern. Claude Code remains primary and fully functional. Zero regression. + +## Architecture Decisions + +| # | Date | Decision | Rationale | +|---|------|----------|-----------| +| PA-1 | 2026-03-11 | Per-provider sidecar binaries (not single multi-SDK bundle) | Independent testing, no bloat, clean separation. SidecarCommand already abstracts binary path. | +| PA-2 | 2026-03-11 | Generic provider_config blob in AgentQueryOptions (not discriminated union) | Rust passes through without parsing. TypeScript uses discriminated unions for compile-time safety. Minimal Rust changes. | +| PA-3 | 2026-03-11 | Per-provider message adapter files → common AgentMessage type | sdk-messages.ts becomes claude-messages.ts. Registry selects parser by provider. Store/UI unchanged. | +| PA-4 | 2026-03-11 | Provider selection per-project with global default | ProjectConfig.provider field (default: 'claude'). Matches real workflow. | +| PA-5 | 2026-03-11 | Capability flags drive UI rendering (not provider ID checks) | ProviderCapabilities interface. AgentPane checks hasProfiles/hasSkills/etc. No hardcoded if(provider==='claude'). | +| PA-6 | 2026-03-11 | Providers section in SettingsTab scroll (not inner tabs) | Current sections aren't long enough for tabs. Collapsible per-provider config panels. | + +## Phases + +### Phase 1: Core Abstraction Layer (no functional change) + +**Goal:** Insert abstraction boundary. Claude remains the only registered provider. Zero user-visible change. + +| # | Task | Files | Status | +|---|------|-------|--------| +| 1.1 | Create provider types | NEW: `src/lib/providers/types.ts` | pending | +| 1.2 | Create provider registry | NEW: `src/lib/providers/registry.svelte.ts` | pending | +| 1.3 | Create Claude provider meta | NEW: `src/lib/providers/claude.ts` | pending | +| 1.4 | Rename sdk-messages.ts → claude-messages.ts | RENAME + update imports | pending | +| 1.5 | Create message adapter registry | NEW: `src/lib/adapters/message-adapters.ts` | pending | +| 1.6 | Update Rust AgentQueryOptions | MOD: `bterminal-core/src/sidecar.rs` | pending | +| 1.7 | Update agent-bridge.ts options shape | MOD: `src/lib/adapters/agent-bridge.ts` | pending | +| 1.8 | Rename agent-runner.ts → claude-runner.ts | RENAME + update build script | pending | +| 1.9 | Add provider field to ProjectConfig | MOD: `src/lib/types/groups.ts` | pending | +| 1.10 | Rename ClaudeSession.svelte → AgentSession.svelte | RENAME + update imports | pending | +| 1.11 | Update agent-dispatcher provider routing | MOD: `src/lib/agent-dispatcher.ts` | pending | +| 1.12 | Update AgentPane for capability-driven rendering | MOD: `src/lib/components/Agent/AgentPane.svelte` | pending | +| 1.13 | Rename claude-bridge.ts → provider-bridge.ts | RENAME + genericize | pending | +| 1.14 | Update Rust lib.rs commands | MOD: `src-tauri/src/lib.rs` | pending | +| 1.15 | Update all tests | MOD: test files | pending | +| 1.16 | Verify: 202 vitest + 42 cargo tests pass | — | pending | + +### Phase 2: Settings UI + +| # | Task | Files | Status | +|---|------|-------|--------| +| 2.1 | Add Providers section to SettingsTab | MOD: `SettingsTab.svelte` | pending | +| 2.2 | Per-provider collapsible config panels | MOD: `SettingsTab.svelte` | pending | +| 2.3 | Per-project provider dropdown | MOD: `SettingsTab.svelte` | pending | +| 2.4 | Persist provider settings | MOD: `settings-bridge.ts` | pending | +| 2.5 | Provider-aware AgentPane | MOD: `AgentPane.svelte` | pending | + +### Phase 3: Sidecar Routing + +| # | Task | Files | Status | +|---|------|-------|--------| +| 3.1 | SidecarManager provider-based runner selection | MOD: `bterminal-core/src/sidecar.rs` | pending | +| 3.2 | Per-provider runner discovery | MOD: `bterminal-core/src/sidecar.rs` | pending | +| 3.3 | Provider-specific env var stripping | MOD: `bterminal-core/src/sidecar.rs` | pending | + +## Type System + +### ProviderQueryOptions (TypeScript → Rust → Sidecar) + +``` +Frontend (typed): + AgentQueryOptions { + provider: ProviderId // 'claude' | 'codex' | 'ollama' + session_id: string + prompt: string + model?: string + max_turns?: number + provider_config: Record // provider-specific + } + ↓ (Tauri invoke) +Rust (generic): + AgentQueryOptions { + provider: String + session_id: String + prompt: String + model: Option + max_turns: Option + provider_config: serde_json::Value + } + ↓ (stdin NDJSON) +Sidecar (provider-specific): + claude-runner.ts parses provider_config as ClaudeProviderConfig + codex-runner.ts parses provider_config as CodexProviderConfig + ollama-runner.ts parses provider_config as OllamaProviderConfig +``` + +### Message Flow (Sidecar → Frontend) + +``` +Sidecar stdout (NDJSON, provider-specific format) + ↓ +Rust SidecarManager (pass-through, adds sessionId) + ↓ +agent-dispatcher.ts + → message-adapters.ts registry + → claude-messages.ts (if provider=claude) + → codex-messages.ts (if provider=codex, future) + → ollama-messages.ts (if provider=ollama, future) + → AgentMessage (common type) + ↓ +agents.svelte.ts store (unchanged) + ↓ +AgentPane.svelte (renders AgentMessage, capability-driven) +``` + +## File Inventory + +### New Files (Phase 1) +- `v2/src/lib/providers/types.ts` +- `v2/src/lib/providers/registry.svelte.ts` +- `v2/src/lib/providers/claude.ts` +- `v2/src/lib/adapters/message-adapters.ts` + +### Renamed Files (Phase 1) +- `sdk-messages.ts` → `claude-messages.ts` +- `agent-runner.ts` → `claude-runner.ts` +- `ClaudeSession.svelte` → `AgentSession.svelte` +- `claude-bridge.ts` → `provider-bridge.ts` (genericized) + +### Modified Files (Phase 1) +- `bterminal-core/src/sidecar.rs` — AgentQueryOptions struct +- `src-tauri/src/lib.rs` — command handlers +- `src/lib/adapters/agent-bridge.ts` — options interface +- `src/lib/agent-dispatcher.ts` — provider routing +- `src/lib/components/Agent/AgentPane.svelte` — capability checks +- `src/lib/components/Workspace/ProjectBox.svelte` — import rename +- `src/lib/types/groups.ts` — ProjectConfig.provider field +- `package.json` — build:sidecar script path +- Test files — import path updates