Provider adapter — trait & Claude Code integration #11

Open
opened 2026-04-16 11:30:18 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As a user, I want forge-agent to spawn and communicate with Claude Code CLI via JSON-RPC over stdio, streaming text deltas, tool calls, thinking blocks, and completion signals, so that I can interact with Claude as an AI coding agent.

Acceptance criteria

ProviderAdapter trait (provider/adapter.rs)

  • #[async_trait] trait ProviderAdapter: Send + Sync with methods:
    • fn provider(&self) -> Provider
    • async fn health_check(&self) -> Result<ProviderHealth, ProviderError>
    • async fn start_session(thread_id, config: SessionConfig) -> Result<Pin<Box<dyn Stream<Item = ProviderEvent> + Send>>, ProviderError>
    • async fn send_message(thread_id, message: String) -> Result<(), ProviderError>
    • async fn interrupt(thread_id) -> Result<(), ProviderError>
  • SessionConfig struct: working_dir, model, runtime_mode, system_prompt: Option<String>
  • ProviderHealth struct: is_available, version: Option<String>, authenticated, error: Option<String>
  • ProviderError enum with variants: NotFound, SpawnFailed, SessionNotFound, ProtocolError, Interrupted

ClaudeAdapter (provider/claude.rs)

  • Struct holds cli_path: PathBuf and sessions: Arc<DashMap<Uuid, ClaudeSession>>
  • health_check(): runs claude-code --version, checks exit code and output
  • start_session(): spawns claude-code --app-server --model <model> with stdin(Stdio::piped()), stdout(Stdio::piped()), stderr(Stdio::piped()), working dir from config
  • Reads stdout line-by-line, parses JSON-RPC messages, maps to ProviderEvent variants
  • Returns a tokio_stream::Stream<Item = ProviderEvent>
  • send_message(): writes JSON-RPC request to the session's stdin
  • interrupt(): sends SIGTERM to the child process, cleans up session
  • JSON-RPC request IDs use an AtomicU64 counter per session

ProviderManager

  • Holds HashMap<Provider, Arc<dyn ProviderAdapter>>
  • get(provider: &Provider) -> Option<Arc<dyn ProviderAdapter>>
  • Initialized at startup with health checks; logs warnings for unavailable providers

CLI auto-detection

  • Uses which crate to find claude / claude-code if ProvidersConfig paths are empty
  • Falls back gracefully if CLI is not installed

Tests

  • Test: health_check with a mock script that prints a version
  • Test: start_session with a mock script that emits synthetic JSON-RPC events
  • Test: interrupt kills the child process
  • Test: ProviderEvent deserialization from sample JSON-RPC payloads

Out of scope

  • Codex adapter (can be added later following the same trait; stub is fine for v0.1)
  • Supervised mode approval flow (covered by decider logic in event sourcing story)

References

  • Spec §7 (Provider Adapter)
  • Spec §7.2 (Claude Adapter)

Dependencies

  • Blocked by: #3 (ProvidersConfig paths), #4 (ProviderEvent enum)
  • Blocks: #12
  • Branch off: issue-4-event-sourcing (rebase #3 in once it lands)
  • Full graph: #21
## User story As a **user**, I want forge-agent to spawn and communicate with Claude Code CLI via JSON-RPC over stdio, streaming text deltas, tool calls, thinking blocks, and completion signals, so that I can interact with Claude as an AI coding agent. ## Acceptance criteria ### ProviderAdapter trait (`provider/adapter.rs`) - [ ] `#[async_trait] trait ProviderAdapter: Send + Sync` with methods: - `fn provider(&self) -> Provider` - `async fn health_check(&self) -> Result<ProviderHealth, ProviderError>` - `async fn start_session(thread_id, config: SessionConfig) -> Result<Pin<Box<dyn Stream<Item = ProviderEvent> + Send>>, ProviderError>` - `async fn send_message(thread_id, message: String) -> Result<(), ProviderError>` - `async fn interrupt(thread_id) -> Result<(), ProviderError>` - [ ] `SessionConfig` struct: `working_dir`, `model`, `runtime_mode`, `system_prompt: Option<String>` - [ ] `ProviderHealth` struct: `is_available`, `version: Option<String>`, `authenticated`, `error: Option<String>` - [ ] `ProviderError` enum with variants: `NotFound`, `SpawnFailed`, `SessionNotFound`, `ProtocolError`, `Interrupted` ### ClaudeAdapter (`provider/claude.rs`) - [ ] Struct holds `cli_path: PathBuf` and `sessions: Arc<DashMap<Uuid, ClaudeSession>>` - [ ] `health_check()`: runs `claude-code --version`, checks exit code and output - [ ] `start_session()`: spawns `claude-code --app-server --model <model>` with `stdin(Stdio::piped())`, `stdout(Stdio::piped())`, `stderr(Stdio::piped())`, working dir from config - [ ] Reads stdout line-by-line, parses JSON-RPC messages, maps to `ProviderEvent` variants - [ ] Returns a `tokio_stream::Stream<Item = ProviderEvent>` - [ ] `send_message()`: writes JSON-RPC request to the session's stdin - [ ] `interrupt()`: sends SIGTERM to the child process, cleans up session - [ ] JSON-RPC request IDs use an `AtomicU64` counter per session ### ProviderManager - [ ] Holds `HashMap<Provider, Arc<dyn ProviderAdapter>>` - [ ] `get(provider: &Provider) -> Option<Arc<dyn ProviderAdapter>>` - [ ] Initialized at startup with health checks; logs warnings for unavailable providers ### CLI auto-detection - [ ] Uses `which` crate to find `claude` / `claude-code` if `ProvidersConfig` paths are empty - [ ] Falls back gracefully if CLI is not installed ### Tests - [ ] Test: health_check with a mock script that prints a version - [ ] Test: start_session with a mock script that emits synthetic JSON-RPC events - [ ] Test: interrupt kills the child process - [ ] Test: ProviderEvent deserialization from sample JSON-RPC payloads ## Out of scope - Codex adapter (can be added later following the same trait; stub is fine for v0.1) - Supervised mode approval flow (covered by decider logic in event sourcing story) ## References - Spec §7 (Provider Adapter) - Spec §7.2 (Claude Adapter) ## Dependencies - **Blocked by:** #3 (ProvidersConfig paths), #4 (ProviderEvent enum) - **Blocks:** #12 - **Branch off:** `issue-4-event-sourcing` (rebase #3 in once it lands) - **Full graph:** #21
claude-desktop added this to the v0.1.0 milestone 2026-04-16 11:30:18 +00:00
Sign in to join this conversation.
No description provided.