Move all 29 workspace members from packages/<name>/ to crates/<name>/. Updates: workspace Cargo.toml (members + path deps), justfile, root CLAUDE.md, scripts/build/CARGO_INSTALL.md, docs/architecture/crates.md (renamed from packages.md), structural references in docs/architecture and docs/configuration, per-crate CLAUDE.md self-references. Historical plans, reports, and building/ docs are left untouched. No behavior change; just check-all stays green and fermata tests pass.
12 KiB
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:
- Bridge Mode: Relay stdio ACP clients to a Dirigent ACP Server via HTTP/SSE
- Mock Server: Serve fixture-based responses for testing ACP clients
- Interactive Client: Connect to ACP agents for debugging and exploration
- 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:
{"jsonrpc":"2.0","method":"session/prompt","params":{...},"id":1}
It expects a JSON-RPC response with the same id:
{"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:
- Receive
{"result":{"stopReason":"end_turn"},"id":1} - Think "turn is done, I can accept new input now"
- Still be receiving chunks for the previous turn
- 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.completeis not received,session_idleacts as fallback
Implementation Details
See src/acp/bridge.rs:
- Lines 87-95:
buffered_responseshashmap (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 sessionsSessionFixture- Mock session with messages and behavior modeMessageFixture- Individual message with role, content, tool callsResponseMode- Enum: Sequential, Random, Pattern, StaticFixtureRegistry- Runtime registry of loaded fixturesFixtureResponder- 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 sessionSendMessageRequest- Request to send messageStreamEvent- SSE event for real-time updates
Errors
MockerError- Error enum with variants:FixtureLoad- Failed to load fixture fileFixtureValidation- Invalid fixture structureAcpProtocol- ACP protocol errorSessionNotFound- Session not found in fixturesTransport- IO/network errorIngest- Session ingestion error (feature-gated)
CLI Commands
serve
Start the mock server:
dirigent-acp-mocker serve --fixtures ./fixtures --port 8080
validate
Validate fixture files without starting server:
dirigent-acp-mocker validate --path ./fixtures
ingest (feature: ingest)
Import sessions from external sources:
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:
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
- Sequential: Return messages in order (default)
- Random: Pick a random message
- Pattern: Match user input patterns
- 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
ingestCLI command - Adds
dirigent_protocoldependency - Adds
opencode_clientdependency - Provides
ingest_from_opencode()function
Enable with:
dirigent_acp_mocker = { features = ["ingest"] }
Or build with:
cargo build --package dirigent_acp_mocker --features ingest
Implementation Status
Phase 0: Scaffold ✓ COMPLETE
- Crate structure (lib + bin targets)
- Core dependencies in Cargo.toml
- Error handling with thiserror
- Logging infrastructure with tracing
- Complete module structure
- CLI argument parsing with clap
- 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
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
# 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
cargo test --package dirigent_acp_mocker
Building
# Without features
cargo build --package dirigent_acp_mocker
# With all features
cargo build --package dirigent_acp_mocker --all-features
Checking
cargo check --package dirigent_acp_mocker
Related Packages
- dirigent_protocol - Protocol types (optional, with
ingestfeature) - opencode_client - OpenCode.ai client (optional, with
ingestfeature) - 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::fsfor 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
randcrate dependency - Pattern mode: Add
regexcrate 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<Mutex>
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
- Unit Tests: Test individual components (responders, loaders)
- Integration Tests: Test full server with fixture loading
- Doc Tests: Keep examples in documentation up-to-date
- Example Fixtures: Provide realistic test fixtures
- 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