🛰️ export standalone-repo assets (c86caab7)
This commit is contained in:
@@ -1,386 +0,0 @@
|
|||||||
# 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
|
|
||||||
+4
-7
@@ -57,10 +57,10 @@ tabled = "0.16"
|
|||||||
shell-words = "1.1"
|
shell-words = "1.1"
|
||||||
|
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
dirigent_protocol = { workspace = true }
|
dirigent_protocol = { git = "https://git.g4b.org/dirigence/dirigent", path = "crates/dirigent_protocol" }
|
||||||
dirigent_core = { workspace = true, features = ["server"] }
|
dirigent_core = { git = "https://git.g4b.org/dirigence/dirigent", path = "crates/dirigent_core", features = ["server"] }
|
||||||
dirigent_tools = { workspace = true }
|
dirigent_tools = { git = "https://git.g4b.org/dirigence/dirigent", path = "crates/dirigent_tools" }
|
||||||
opencode_client = { workspace = true, optional = true }
|
opencode_client = { git = "https://git.g4b.org/dirigence/dirigent", path = "crates/opencode_client", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dirigent_acp_api = { workspace = true }
|
dirigent_acp_api = { workspace = true }
|
||||||
@@ -68,6 +68,3 @@ dirigent_acp_api = { workspace = true }
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
ingest = ["dep:opencode_client"]
|
ingest = ["dep:opencode_client"]
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|||||||
@@ -1,175 +1,148 @@
|
|||||||
# Dirigate
|
# 𝄑 dirigate
|
||||||
|
|
||||||
ACP (Agent-Client Protocol) bridge and mock server for testing and proxying ACP connections.
|
**ACP bridge and mock server for AI agent orchestration.**
|
||||||
|
|
||||||
## Overview
|
Connect stdio-based ACP agents (Claude Code, Zed) to a remote Dirigent server — or run a fixture-backed mock for local development and testing.
|
||||||
|
|
||||||
`dirigate` provides tools for working with ACP clients and servers:
|
> [!CAUTION]
|
||||||
|
> **Alpha software.** Dirigate is under active development and not fully battle-tested. The ACP specification itself is still evolving, and dirigate tracks it on a best-effort basis. APIs and CLI flags may change between releases.
|
||||||
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
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Bridge Mode**: Connect external ACP clients (like Claude Code) to a Dirigent ACP Server
|
|
||||||
- **Fixture-based responses**: Define sessions and message flows in YAML for testing
|
|
||||||
- **Multiple response modes**: Static, random, sequential, and pattern-based
|
|
||||||
- **Session ingestion**: Import sessions from OpenCode.ai or other sources (feature-gated)
|
|
||||||
- **ACP compliance**: Full protocol implementation for testing clients
|
|
||||||
- **CLI tool**: Easy-to-use command-line interface
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### As a CLI Tool
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo install --path packages/conductor
|
# Bridge Claude Code to a running Dirigent instance
|
||||||
|
dirigate bridge --server-url http://your-server:3001/acp
|
||||||
```
|
```
|
||||||
|
|
||||||
### As a Library
|
---
|
||||||
|
|
||||||
Add to your `Cargo.toml`:
|
## Dirigent Protocol and ACP
|
||||||
|
|
||||||
```toml
|
Dirigate speaks the **Dirigent Protocol**, which is a superset of the [Agent-Client Protocol (ACP)](https://github.com/anthropics/agent-protocol). Dirigent Protocol started as an early attempt at an HTTP-based ACP implementation and carries additional signaling used by the Dirigent orchestration platform (session lifecycle, multi-agent coordination, archival events).
|
||||||
[dependencies]
|
|
||||||
conductor = { path = "../conductor" }
|
|
||||||
```
|
|
||||||
|
|
||||||
For ingestion features:
|
**The goal is full ACP parity at all times.** As the ACP HTTP specification evolves, dirigate tracks it and aims to provide a fully ACP-compliant smart adapter as its primary function. The Dirigent-specific extensions ride alongside as optional protocol signals — an ACP-only client can ignore them entirely and still get a conformant experience.
|
||||||
|
|
||||||
```toml
|
In short: dirigate is an **ACP adapter first**, Dirigent extension carrier second.
|
||||||
[dependencies]
|
|
||||||
conductor = { path = "../conductor", features = ["ingest"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
---
|
||||||
|
|
||||||
### Bridge Mode (NEW)
|
## Why Dirigate
|
||||||
|
|
||||||
Bridge a stdio ACP client to a Dirigent ACP Server via HTTP/SSE:
|
ACP clients speak JSON-RPC over stdio. Dirigent listens over HTTP/SSE. Dirigate bridges the two — reliably, with correct streaming semantics.
|
||||||
|
|
||||||
|
The core challenge: an ACP server returns the JSON-RPC response immediately, but the actual agent output keeps arriving as SSE chunks. If the bridge naively forwards the response right away, the client thinks the turn is done and stops listening — before the content has arrived.
|
||||||
|
|
||||||
|
**Dirigate solves this with RPC response buffering**: the response is held until `turn.complete` arrives via SSE, so all chunks and notifications reach the client first, and the response arrives last. The client gets clean "all content, then done" semantics — exactly what stdio JSON-RPC expects.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Overview
|
||||||
|
|
||||||
|
| Mode | Command | Maturity | Description |
|
||||||
|
|------|---------|----------|-------------|
|
||||||
|
| Bridge | `dirigate bridge` | beta | Relay a stdio ACP client to a Dirigent ACP Server over HTTP/SSE |
|
||||||
|
| Mock server | `dirigate serve` | beta | Fixture-based ACP server for client development and testing |
|
||||||
|
| Interactive client | `dirigate connect` | concept | Debug and explore any ACP agent interactively |
|
||||||
|
| Session ingest | `dirigate ingest` | concept | Import sessions from OpenCode.ai into fixture format (requires `ingest` feature) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Connect to local server (default: http://localhost:3001/acp)
|
cargo install --git https://git.g4b.org/dirigence/dirigate
|
||||||
conductor bridge
|
```
|
||||||
|
|
||||||
# Connect to remote server
|
Requires a working [Rust toolchain](https://rustup.rs).
|
||||||
conductor bridge --server-url http://remote:3001/acp
|
|
||||||
|
|
||||||
# Via environment variable
|
---
|
||||||
|
|
||||||
|
## Quick Start: Bridge Mode
|
||||||
|
|
||||||
|
Bridge mode is the primary use case. It lets any stdio ACP client reach a remote Dirigent server.
|
||||||
|
|
||||||
|
### 1. Start Dirigent
|
||||||
|
|
||||||
|
Start the Dirigent server with the ACP server enabled:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default ACP endpoint: http://localhost:3001/acp
|
||||||
|
DIRIGENT_ACP_ENABLED=true dirigent
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure your ACP client
|
||||||
|
|
||||||
|
**Claude Code** — add to `.claude/settings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"dirigent": {
|
||||||
|
"command": "dirigate",
|
||||||
|
"args": ["bridge"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zed** — add to `settings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agent_servers": {
|
||||||
|
"dirigent": {
|
||||||
|
"command": "dirigate",
|
||||||
|
"args": ["bridge"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Remote server:**
|
||||||
|
|
||||||
|
```bash
|
||||||
DIRIGENT_SERVER_URL=http://remote:3001/acp dirigate bridge
|
DIRIGENT_SERVER_URL=http://remote:3001/acp dirigate bridge
|
||||||
|
|
||||||
# With verbose logging
|
|
||||||
conductor bridge --verbose
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Bridge Architecture:**
|
Or with the flag:
|
||||||
|
|
||||||
```
|
|
||||||
External ACP Client (Claude Code, etc.)
|
|
||||||
|
|
|
||||||
| stdio (stdin/stdout)
|
|
||||||
v
|
|
||||||
+-------------------+
|
|
||||||
| Conductor Bridge |
|
|
||||||
| - stdin parser |
|
|
||||||
| - HTTP client |
|
|
||||||
| - SSE subscriber |
|
|
||||||
+-------------------+
|
|
||||||
|
|
|
||||||
| HTTP/SSE
|
|
||||||
v
|
|
||||||
Dirigent ACP Server
|
|
||||||
```
|
|
||||||
|
|
||||||
**Options:**
|
|
||||||
- `-s, --server-url <URL>` - ACP Server URL (env: `DIRIGENT_SERVER_URL`, default: `http://localhost:3001/acp`)
|
|
||||||
- `-v, --verbose` - Enable verbose logging of JSON-RPC messages
|
|
||||||
- `--timeout <SECS>` - Timeout for HTTP requests (default: 30)
|
|
||||||
- `--auto-reconnect` - Automatically reconnect SSE stream on disconnect (default: true)
|
|
||||||
|
|
||||||
**Use Case: Connecting Claude Code to Dirigent**
|
|
||||||
|
|
||||||
To use Claude Code with Dirigent:
|
|
||||||
|
|
||||||
1. Start the Dirigent web application (which includes the ACP Server)
|
|
||||||
2. Configure Claude Code to use a command-based ACP agent:
|
|
||||||
```
|
|
||||||
dirigate bridge
|
|
||||||
```
|
|
||||||
3. Claude Code will now communicate through Conductor to Dirigent
|
|
||||||
|
|
||||||
### Mock Server
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
conductor serve --fixtures ./fixtures --port 8080
|
dirigate bridge --server-url http://remote:3001/acp
|
||||||
```
|
```
|
||||||
|
|
||||||
Options:
|
### Bridge options
|
||||||
- `-f, --fixtures <DIR>` - Directory containing fixture YAML files (default: `./fixtures`)
|
|
||||||
- `-p, --port <PORT>` - Port to bind the server to (default: `8080`)
|
|
||||||
- `--host <HOST>` - Host address to bind to (default: `127.0.0.1`)
|
|
||||||
- `--stdio` - Use stdin/stdout for JSON-RPC transport
|
|
||||||
- `-l, --log-level <LEVEL>` - Log level: trace, debug, info, warn, error (default: `info`)
|
|
||||||
- `--log-format <FORMAT>` - Log format: pretty, json, compact (default: `pretty`)
|
|
||||||
|
|
||||||
### Validate Fixtures
|
| Flag | Env | Default | Description |
|
||||||
|
|------|-----|---------|-------------|
|
||||||
|
| `-s, --server-url` | `DIRIGENT_SERVER_URL` | `http://localhost:3001/acp` | ACP server URL |
|
||||||
|
| `-v, --verbose` | | false | Log all JSON-RPC messages |
|
||||||
|
| `--timeout` | | 30 | HTTP request timeout in seconds |
|
||||||
|
| `--auto-reconnect` | | true | Reconnect SSE stream on disconnect |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture: RPC Response Buffering
|
||||||
|
|
||||||
|
The bridge implements a minimal buffering mechanism that preserves real-time streaming while maintaining correct JSON-RPC request/response pairing over stdio.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="rpc-buffering.svg" alt="RPC response buffering sequence diagram" width="760">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Without buffering, the client would receive the `"id":1` response at the moment the HTTP POST returns — before any chunks arrive — and immediately consider the turn complete. Dirigate holds that response until `turn.complete` signals that all content has been delivered.
|
||||||
|
|
||||||
|
What is buffered: only the final RPC response (approximately 50 bytes).
|
||||||
|
What is not buffered: all chunks and notifications, forwarded immediately as they arrive.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mock Server
|
||||||
|
|
||||||
|
Start a fixture-backed ACP server for testing ACP clients without a running agent:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
conductor validate ./fixtures
|
dirigate serve --fixtures ./fixtures --port 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Validates fixture files without starting the server.
|
Fixtures are YAML files defining sessions and response behavior:
|
||||||
|
|
||||||
### Interactive Client
|
|
||||||
|
|
||||||
Connect to an ACP agent for debugging and exploration:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Connect via stdio (spawns a process)
|
|
||||||
conductor connect --command "claude --acp" --auto-session
|
|
||||||
|
|
||||||
# Connect via HTTP
|
|
||||||
conductor connect --url http://localhost:8080 --auto-session
|
|
||||||
|
|
||||||
# Custom protocol version
|
|
||||||
conductor connect --command "claude --acp" --protocol-version 1 --auto-session
|
|
||||||
```
|
|
||||||
|
|
||||||
**Interactive Commands:**
|
|
||||||
- `/help` - Display available commands
|
|
||||||
- `/quit` - Exit the client
|
|
||||||
- `/session list` - List available sessions
|
|
||||||
- `/session new` - Create a new session
|
|
||||||
- `/session <id>` - Switch to a session
|
|
||||||
- `/cancel` - Cancel generation in current session
|
|
||||||
- Any other text - Send as message to current session
|
|
||||||
|
|
||||||
**Debugging:**
|
|
||||||
|
|
||||||
Enable detailed logging to see JSON-RPC packets:
|
|
||||||
```bash
|
|
||||||
# See all debug logs including packets
|
|
||||||
RUST_LOG=debug dirigate connect --command "claude --acp" --auto-session
|
|
||||||
|
|
||||||
# See event details
|
|
||||||
DIRIGENT_VERBOSE=1 dirigate connect --command "claude --acp" --auto-session
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ingest Sessions (requires `ingest` feature)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
conductor ingest \
|
|
||||||
-u http://localhost:12225 \
|
|
||||||
--session-id my-session-123 \
|
|
||||||
--output ./fixtures/my-session.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
Imports a session from an external source and converts it to fixture format.
|
|
||||||
|
|
||||||
## Fixture Format
|
|
||||||
|
|
||||||
Fixtures are defined in YAML files:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: "1"
|
version: "1"
|
||||||
@@ -182,106 +155,111 @@ streaming:
|
|||||||
sessions:
|
sessions:
|
||||||
- id: "session-123"
|
- id: "session-123"
|
||||||
title: "Test Session"
|
title: "Test Session"
|
||||||
created_at: "2025-01-01T00:00:00Z"
|
|
||||||
participants:
|
|
||||||
- id: "assistant"
|
|
||||||
name: "Assistant"
|
|
||||||
role: "agent"
|
|
||||||
messages:
|
messages:
|
||||||
- role: "agent"
|
- role: "agent"
|
||||||
content: "Hello! How can I help you?"
|
content: "Hello! How can I help you?"
|
||||||
|
|
||||||
responders:
|
responders:
|
||||||
default_strategy: Echo
|
default_strategy: Echo
|
||||||
keyword_map: {}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Response Modes
|
**Response modes:** `sequential`, `random`, `pattern`, `static`, `echo`
|
||||||
|
|
||||||
- **sequential**: Return messages in order (default)
|
**Server options:**
|
||||||
- **random**: Return a random message from the fixture
|
|
||||||
- **pattern**: Match user input against patterns and return corresponding response
|
|
||||||
- **static**: Always return the same message
|
|
||||||
- **echo**: Echo back the user's input
|
|
||||||
|
|
||||||
## Project Structure
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `-f, --fixtures` | `./fixtures` | Fixture directory |
|
||||||
|
| `-p, --port` | `8080` | Bind port |
|
||||||
|
| `--host` | `127.0.0.1` | Bind address |
|
||||||
|
| `--stdio` | false | Use stdio transport instead of HTTP |
|
||||||
|
|
||||||
```
|
---
|
||||||
conductor/
|
|
||||||
|-- src/
|
|
||||||
| |-- lib.rs # Library entry point
|
|
||||||
| |-- error.rs # Error types
|
|
||||||
| |-- logging.rs # Logging infrastructure
|
|
||||||
| |-- acp/ # ACP protocol implementation
|
|
||||||
| | |-- mod.rs
|
|
||||||
| | |-- bridge.rs # Bridge mode (stdio <-> HTTP/SSE)
|
|
||||||
| | |-- server.rs # HTTP server
|
|
||||||
| | |-- model.rs # Protocol types
|
|
||||||
| | |-- stdio.rs # Stdio transport
|
|
||||||
| | |-- stream.rs # SSE streaming
|
|
||||||
| |-- fixture/ # Fixture system
|
|
||||||
| | |-- mod.rs
|
|
||||||
| | |-- types.rs # Fixture definitions
|
|
||||||
| | |-- loader.rs # YAML loading
|
|
||||||
| | |-- responders.rs # Response logic
|
|
||||||
| |-- ingest/ # Session ingestion (feature-gated)
|
|
||||||
| | |-- mod.rs
|
|
||||||
| | |-- opencode.rs # OpenCode.ai ingestion
|
|
||||||
| |-- cli/ # CLI interface
|
|
||||||
| | |-- mod.rs
|
|
||||||
| | |-- args.rs # Argument parsing
|
|
||||||
| | |-- commands.rs # Command execution
|
|
||||||
| |-- bin/
|
|
||||||
| |-- conductor.rs # Binary entry point
|
|
||||||
|-- Cargo.toml
|
|
||||||
|-- README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
## Interactive Client
|
||||||
|
|
||||||
### Environment Variables
|
Connect to any ACP agent for debugging and exploration:
|
||||||
|
|
||||||
| Variable | Description | Default |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `DIRIGENT_SERVER_URL` | ACP Server URL for bridge mode | `http://localhost:3001/acp` |
|
|
||||||
| `CONDUCTOR_BUFFER_SIZE` | Broadcast channel buffer size for event distribution. Increase for high-volume event scenarios or when experiencing event drops during bursts. Valid values: Any positive integer (usize). Example: `CONDUCTOR_BUFFER_SIZE=500` | `100` |
|
|
||||||
| `RUST_LOG` | Log level | `info` |
|
|
||||||
| `DIRIGENT_VERBOSE` | Enable verbose event logging | (unset) |
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Run tests:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test -p dirigate
|
# Spawn a local agent over stdio
|
||||||
|
dirigate connect --command "my-agent --acp" --auto-session
|
||||||
|
|
||||||
|
# Connect to an HTTP ACP server
|
||||||
|
dirigate connect --url http://localhost:8080 --auto-session
|
||||||
```
|
```
|
||||||
|
|
||||||
Run with all features:
|
Interactive commands:
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/session list` | List available sessions |
|
||||||
|
| `/session new` | Create a new session |
|
||||||
|
| `/session <id>` | Switch to a session |
|
||||||
|
| `/cancel` | Cancel current generation |
|
||||||
|
| `/quit` | Exit |
|
||||||
|
| (any text) | Send as a message to the current session |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session Ingest (feature-gated)
|
||||||
|
|
||||||
|
Import sessions from OpenCode.ai into fixture format:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test -p dirigate --all-features
|
cargo install --git https://git.g4b.org/dirigence/dirigate --features ingest
|
||||||
|
|
||||||
|
dirigate ingest \
|
||||||
|
-u http://localhost:12225 \
|
||||||
|
--session-id my-session-123 \
|
||||||
|
--output ./fixtures/my-session.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dependencies
|
---
|
||||||
|
|
||||||
### Core
|
## Library Usage
|
||||||
- `clap` - CLI argument parsing
|
|
||||||
- `serde`, `serde_yaml`, `serde_json` - Serialization
|
|
||||||
- `tokio` - Async runtime
|
|
||||||
- `axum` - Web server
|
|
||||||
- `reqwest` - HTTP client (for bridge mode)
|
|
||||||
- `reqwest-eventsource` - SSE client (for bridge mode)
|
|
||||||
- `anyhow`, `thiserror` - Error handling
|
|
||||||
- `tracing`, `tracing-subscriber` - Logging
|
|
||||||
|
|
||||||
### Optional (ingest feature)
|
Dirigate ships as both a CLI tool and a library. To use it as a dependency:
|
||||||
- `dirigent_protocol` - Protocol types
|
|
||||||
- `opencode_client` - OpenCode.ai client
|
```toml
|
||||||
|
# Cargo.toml
|
||||||
|
[dependencies]
|
||||||
|
dirigate = { git = "https://git.g4b.org/dirigence/dirigate" }
|
||||||
|
|
||||||
|
# With session ingestion support
|
||||||
|
dirigate = { git = "https://git.g4b.org/dirigence/dirigate", features = ["ingest"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verbose JSON-RPC logging
|
||||||
|
dirigate bridge --verbose
|
||||||
|
|
||||||
|
# Full debug output via RUST_LOG
|
||||||
|
RUST_LOG=debug dirigate bridge --verbose
|
||||||
|
|
||||||
|
# Trace level for maximum detail
|
||||||
|
RUST_LOG=trace dirigate bridge --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## About This Repository
|
||||||
|
|
||||||
|
This is a downstream mirror. Dirigate is developed inside the upstream
|
||||||
|
[Dirigent](https://git.g4b.org/dirigence/dirigent) monorepo and exported here
|
||||||
|
for standalone distribution. Issues and pull requests are accepted on the
|
||||||
|
`develop` branch, but canonical development happens upstream.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Same as the Dirigent project.
|
Licensed under either of
|
||||||
|
|
||||||
## Contributing
|
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
|
||||||
|
- MIT License ([LICENSE-MIT](LICENSE-MIT))
|
||||||
|
|
||||||
See the main project README for contribution guidelines.
|
at your option.
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 820 470" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13">
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||||
|
<path d="M0,0 L10,5 L0,10 z" fill="#374151"/>
|
||||||
|
</marker>
|
||||||
|
<marker id="arrowR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||||
|
<path d="M0,0 L10,5 L0,10 z" fill="#1d4ed8"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- participant boxes -->
|
||||||
|
<rect x="40" y="20" width="220" height="40" rx="6" fill="#f3f4f6" stroke="#9ca3af"/>
|
||||||
|
<text x="150" y="45" text-anchor="middle" fill="#111827">Zed / Claude Code</text>
|
||||||
|
|
||||||
|
<rect x="560" y="20" width="220" height="40" rx="6" fill="#f3f4f6" stroke="#9ca3af"/>
|
||||||
|
<text x="670" y="45" text-anchor="middle" fill="#111827">Dirigent ACP Server</text>
|
||||||
|
|
||||||
|
<!-- lifelines -->
|
||||||
|
<line x1="150" y1="60" x2="150" y2="440" stroke="#9ca3af" stroke-dasharray="3,3"/>
|
||||||
|
<line x1="670" y1="60" x2="670" y2="440" stroke="#9ca3af" stroke-dasharray="3,3"/>
|
||||||
|
|
||||||
|
<!-- buffered region highlight on server side -->
|
||||||
|
<rect x="660" y="115" width="20" height="245" fill="#dbeafe" stroke="#1d4ed8" stroke-width="1"/>
|
||||||
|
<text x="690" y="125" fill="#1d4ed8" font-style="italic">response held</text>
|
||||||
|
<text x="690" y="143" fill="#1d4ed8" font-style="italic">in buffer</text>
|
||||||
|
|
||||||
|
<!-- POST request -->
|
||||||
|
<line x1="150" y1="100" x2="666" y2="100" stroke="#374151" marker-end="url(#arrow)"/>
|
||||||
|
<text x="408" y="93" text-anchor="middle" fill="#111827">POST /rpc {"method":"session/prompt","id":1}</text>
|
||||||
|
|
||||||
|
<!-- chunks coming back -->
|
||||||
|
<g stroke="#374151" stroke-width="1" fill="none">
|
||||||
|
<line x1="670" y1="160" x2="156" y2="160" marker-end="url(#arrow)"/>
|
||||||
|
<line x1="670" y1="190" x2="156" y2="190" marker-end="url(#arrow)"/>
|
||||||
|
<line x1="670" y1="220" x2="156" y2="220" marker-end="url(#arrow)"/>
|
||||||
|
<line x1="670" y1="250" x2="156" y2="250" marker-end="url(#arrow)"/>
|
||||||
|
<line x1="670" y1="280" x2="156" y2="280" marker-end="url(#arrow)"/>
|
||||||
|
<line x1="670" y1="310" x2="156" y2="310" marker-end="url(#arrow)"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#111827" text-anchor="start">
|
||||||
|
<text x="170" y="155">SSE: agent_message_chunk</text>
|
||||||
|
<text x="170" y="185">SSE: agent_message_chunk</text>
|
||||||
|
<text x="170" y="215">SSE: tool_call_started</text>
|
||||||
|
<text x="170" y="245">SSE: tool_call_completed</text>
|
||||||
|
<text x="170" y="275">SSE: agent_message_chunk</text>
|
||||||
|
<text x="170" y="305" font-weight="bold">SSE: turn.complete</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- final flushed response (highlighted blue) -->
|
||||||
|
<line x1="670" y1="355" x2="156" y2="355" stroke="#1d4ed8" stroke-width="2" marker-end="url(#arrowR)"/>
|
||||||
|
<text x="170" y="350" fill="#1d4ed8" font-weight="bold">{"result":{"stopReason":"end_turn"},"id":1} ◂ flushed</text>
|
||||||
|
|
||||||
|
<!-- final state -->
|
||||||
|
<text x="150" y="400" text-anchor="middle" fill="#6b7280" font-style="italic">(client accepts new input)</text>
|
||||||
|
|
||||||
|
<!-- caption -->
|
||||||
|
<text x="410" y="445" text-anchor="middle" fill="#374151" font-size="12">
|
||||||
|
The JSON-RPC response is buffered until turn.complete arrives, so chunks reach the client first and the response arrives last.
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
Reference in New Issue
Block a user