chore: rename packages/ to crates/
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.
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user