# Package: dirigate Dirigate - ACP bridge and mock server for testing and proxying ACP connections. ## Quick Facts - **Type**: Library + Binary - **Main Entry**: src/lib.rs - **Binary**: src/bin/dirigate.rs - **Status**: Bridge mode implemented - **Dependencies**: clap, serde, tokio, axum, reqwest, thiserror, tracing ## Purpose This package provides tools for working with ACP (Agent-Client Protocol) clients and servers: 1. **Bridge Mode**: Relay stdio ACP clients to a Dirigent ACP Server via HTTP/SSE 2. **Mock Server**: Serve fixture-based responses for testing ACP clients 3. **Interactive Client**: Connect to ACP agents for debugging and exploration 4. **Session Ingestion**: Import sessions from OpenCode.ai (feature-gated) ## Architecture ### Bridge Mode: RPC Response Buffering The bridge implements a **minimal buffering mechanism** to maintain proper JSON-RPC request/response semantics over stdio while preserving real-time streaming. #### The Problem: Stdio JSON-RPC Semantics When an external ACP client (like Zed/Claude Code) sends a JSON-RPC **request** with an `id`: ```json {"jsonrpc":"2.0","method":"session/prompt","params":{...},"id":1} ``` It expects a JSON-RPC **response** with the same `id`: ```json {"jsonrpc":"2.0","result":{"stopReason":"end_turn"},"id":1} ``` However, the response must not arrive until the turn is **complete**. Meanwhile, chunks and events stream as JSON-RPC **notifications** (no `id` field). #### The Solution: Buffer RPC Response Until TurnComplete **What gets buffered**: Only the RPC response (the `{"id":1}` response) - approximately 50 bytes **What does NOT get buffered**: Chunks, events, notifications - all forwarded immediately as they arrive via SSE #### Timeline Example ``` Time Zed → Conductor Conductor → Zed ──────────────────────────────────────────────────────────── T0 session/prompt (id:1) → T1 ← HTTP POST to server T2 (server returns end_turn immediately) T3 [BUFFERED: response id:1] T4 ← notification: agent_message_chunk T5 ← notification: agent_message_chunk T6 ← notification: tool_call_started T7 ← notification: tool_call_chunk T8 ← notification: tool_call_completed T9 ← notification: agent_message_chunk T10 ← notification: turn.complete T11 ← [FLUSH] response (id:1) ✓ Zed gets control back T12 ← notification: session_idle ``` #### Why This Design If Conductor sent the RPC response at T3 (right after HTTP returns), Zed would: 1. Receive `{"result":{"stopReason":"end_turn"},"id":1}` 2. Think "turn is done, I can accept new input now" 3. Still be receiving chunks for the previous turn 4. Enter an inconsistent state By **buffering the RPC response until turn.complete**, Conductor ensures: - All chunks/events are delivered **first** (these flow immediately via SSE → stdout) - The RPC response arrives **last**, signaling "now you can accept new input" - Clean request/response pairing semantics for stdio JSON-RPC clients - **Backward compatibility**: If `turn.complete` is not received, `session_idle` acts as fallback #### Implementation Details See `src/acp/bridge.rs`: - Lines 87-95: `buffered_responses` hashmap (session_id → response_json) - Lines 409-428: Buffer session/prompt responses instead of sending immediately - Lines 681-714: Flush buffered response when turn.complete notification arrives (primary) - Lines 716-761: Flush buffered response when session_idle notification arrives (fallback) This buffering is **minimal and necessary** for stdio transport to work correctly with the async nature of agent responses. **Related Pattern**: The Dirigent ACP Server (which the bridge connects to) implements a complementary bidirectional request handling pattern. While the bridge buffers responses for stdio semantics, the server's ACP connector polls command channels during `send_request()` to avoid deadlock when the agent sends permission requests mid-turn. See `crates/dirigent_core/CLAUDE.md` section "Bidirectional Request Handling Pattern" for details. ### Module Structure ``` conductor/ ├── error.rs # Error types (MockerError) ├── logging.rs # Tracing/logging setup ├── acp/ # ACP protocol implementation │ ├── bridge.rs # Bridge mode (stdio <-> HTTP/SSE) │ ├── server.rs # Axum HTTP server │ ├── model.rs # Protocol types (Session, Message, etc.) │ ├── stdio.rs # Stdio transport for editors │ └── stream.rs # SSE streaming ├── fixture/ # Fixture system │ ├── types.rs # Fixture definitions (YAML structure) │ ├── loader.rs # Load fixtures from disk │ └── responders.rs # Response behavior logic ├── ingest/ # Session ingestion (feature-gated) │ └── opencode.rs # Import from OpenCode.ai └── cli/ # CLI interface ├── args.rs # Clap argument definitions └── commands.rs # Command execution ``` ### Key Types #### Fixture System - `Fixture` - Top-level fixture definition with metadata and sessions - `SessionFixture` - Mock session with messages and behavior mode - `MessageFixture` - Individual message with role, content, tool calls - `ResponseMode` - Enum: Sequential, Random, Pattern, Static - `FixtureRegistry` - Runtime registry of loaded fixtures - `FixtureResponder` - Generates responses based on mode #### ACP Protocol - `Session` - Session metadata (id, title, timestamps) - `Message` - Message in a session (role, content, etc.) - `CreateSessionRequest` - Request to create new session - `SendMessageRequest` - Request to send message - `StreamEvent` - SSE event for real-time updates #### Errors - `MockerError` - Error enum with variants: - `FixtureLoad` - Failed to load fixture file - `FixtureValidation` - Invalid fixture structure - `AcpProtocol` - ACP protocol error - `SessionNotFound` - Session not found in fixtures - `Transport` - IO/network error - `Ingest` - Session ingestion error (feature-gated) ### CLI Commands #### serve Start the mock server: ```bash dirigent-acp-mocker serve --fixtures ./fixtures --port 8080 ``` #### validate Validate fixture files without starting server: ```bash dirigent-acp-mocker validate --path ./fixtures ``` #### ingest (feature: ingest) Import sessions from external sources: ```bash dirigent-acp-mocker ingest \ --source opencode \ --url http://localhost:12225 \ --session-id my-session \ --output ./fixtures/my-session.yaml ``` ## Fixture Format Fixtures are YAML files defining mock sessions: ```yaml metadata: name: "Basic Chat" description: "A simple chat session" version: "1.0" sessions: - id: "session-123" title: "Test Session" mode: sequential # sequential, random, pattern, static messages: - role: "agent" content: "Hello! How can I help you?" delay_ms: 100 - role: "agent" content: "I can assist with various tasks." tool_calls: - name: "search" input: { "query": "example" } output: { "results": ["item1", "item2"] } ``` ### Response Modes 1. **Sequential**: Return messages in order (default) 2. **Random**: Pick a random message 3. **Pattern**: Match user input patterns 4. **Static**: Always return the same message ## Features ### Default Features - Basic fixture loading and validation - ACP server implementation - CLI commands: serve, validate ### Optional Features #### `ingest` Enables session ingestion from external sources: - Adds `ingest` CLI command - Adds `dirigent_protocol` dependency - Adds `opencode_client` dependency - Provides `ingest_from_opencode()` function Enable with: ```toml dirigent_acp_mocker = { features = ["ingest"] } ``` Or build with: ```bash cargo build --package dirigent_acp_mocker --features ingest ``` ## Implementation Status ### Phase 0: Scaffold ✓ COMPLETE - [x] Crate structure (lib + bin targets) - [x] Core dependencies in Cargo.toml - [x] Error handling with thiserror - [x] Logging infrastructure with tracing - [x] Complete module structure - [x] CLI argument parsing with clap - [x] All code compiles (cargo check passes) ### Phase 1: Fixture System (TODO) - [ ] YAML fixture loading from disk - [ ] Fixture validation logic - [ ] FixtureRegistry implementation - [ ] Sequential responder implementation - [ ] Static responder implementation - [ ] Random responder (with rand crate) - [ ] Pattern responder (with regex) - [ ] Unit tests for fixture loading - [ ] Example fixtures ### Phase 2: ACP Server (TODO) - [ ] Axum server setup with routing - [ ] List sessions endpoint - [ ] Create session endpoint - [ ] Send message endpoint - [ ] SSE streaming endpoint - [ ] Error response handling - [ ] CORS configuration - [ ] Integration tests ### Phase 3: Advanced Features (TODO) - [ ] OpenCode session ingestion - [ ] Message conversion logic - [ ] Tool call simulation - [ ] Delay support for message timing - [ ] Session state persistence - [ ] WebSocket transport (alternative to SSE) ## Usage Examples ### As a Library ```rust use dirigent_acp_mocker::{ fixture::{load_fixture, FixtureRegistry, FixtureResponder}, acp::{start_server, ServerConfig}, Result, }; #[tokio::main] async fn main() -> Result<()> { // Load fixtures let fixture = load_fixture("fixtures/chat.yaml").await?; // Create registry let mut registry = FixtureRegistry::new(); registry.add(fixture); // Start server let config = ServerConfig { port: 8080, host: "127.0.0.1".to_string(), }; start_server(config).await?; Ok(()) } ``` ### As a CLI Tool ```bash # Start server dirigent-acp-mocker serve -f ./fixtures -p 8080 # Validate fixtures dirigent-acp-mocker validate -p ./fixtures/chat.yaml # Ingest session (requires 'ingest' feature) dirigent-acp-mocker ingest \ -s opencode \ -u http://localhost:12225 \ --session-id abc-123 \ -o ./fixtures/imported.yaml ``` ## Development ### Running Tests ```bash cargo test --package dirigent_acp_mocker ``` ### Building ```bash # Without features cargo build --package dirigent_acp_mocker # With all features cargo build --package dirigent_acp_mocker --all-features ``` ### Checking ```bash cargo check --package dirigent_acp_mocker ``` ## Related Packages - **dirigent_protocol** - Protocol types (optional, with `ingest` feature) - **opencode_client** - OpenCode.ai client (optional, with `ingest` feature) - **dirigent_core** - Uses this for testing connector implementations ## Documentation - README: ./README.md - Main project: ../../CLAUDE.md ## Notes for Future Implementation ### Fixture Loading (Phase 1) - Use `tokio::fs` for async file I/O - Support both .yaml and .yml extensions - Validate fixture structure on load - Provide helpful error messages for malformed YAML ### Response Behavior (Phase 1) - Sequential mode: Track state per session - Random mode: Add `rand` crate dependency - Pattern mode: Add `regex` crate for pattern matching - Consider thread-safety for responder state ### Server Implementation (Phase 2) - Use Axum's Router for endpoint setup - Add tower-http middleware for CORS - Implement SSE with axum::response::Sse - Consider shared state with Arc> ### Ingestion (Phase 3) - Use opencode_client to fetch sessions - Convert OpenCode message types to fixture format - Handle tool calls and reasoning blocks - Preserve timestamps for realistic playback ## Testing Strategy 1. **Unit Tests**: Test individual components (responders, loaders) 2. **Integration Tests**: Test full server with fixture loading 3. **Doc Tests**: Keep examples in documentation up-to-date 4. **Example Fixtures**: Provide realistic test fixtures 5. **CLI Tests**: Test command-line interface ## Performance Considerations - Lazy-load fixtures on demand vs. pre-load all - Consider caching parsed YAML - Stream large responses instead of buffering - Use connection pooling for ingestion - Benchmark response latency for different modes