c62d8daea8
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.
387 lines
12 KiB
Markdown
387 lines
12 KiB
Markdown
# 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<Mutex<FixtureRegistry>>
|
|
|
|
### 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
|