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:
2026-04-30 21:58:57 +02:00
commit c62d8daea8
34 changed files with 12268 additions and 0 deletions
+386
View File
@@ -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