🛰️ export from upstream (56c5a2dd)
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
<p align="center">Core libraries for the Dirigent agent orchestration platform.</p>
|
<p align="center">Core libraries for the Dirigent agent orchestration platform.</p>
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
> **Alpha software.** Dirigent is in early active development and not fully battle-tested. Most crates are experimental — APIs will change without notice. There is nothing to install from this repository yet. The standalone tools listed below have their own repositories and maturity levels.
|
> **Alpha software.** Dirigent is in early active development and not fully battle-tested. Most crates are experimental — APIs will change without notice. There is nothing to install from this repository yet. The standalone tools listed below have their own repositories and maturity levels.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -79,6 +80,7 @@ dirigent_core = { git = "https://git.g4b.org/dirigence/dirigent", path = "cr
|
|||||||
|
|
||||||
Replace the crate name and path with the one you need. All crates follow the same pattern.
|
Replace the crate name and path with the one you need. All crates follow the same pattern.
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
> **Expect breakage.** These are internal library crates under active development. Pin to a specific commit if you depend on stability.
|
> **Expect breakage.** These are internal library crates under active development. Pin to a specific commit if you depend on stability.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
# Package: dirigent_acp_api
|
|
||||||
|
|
||||||
ACP Server implementation for accepting incoming ACP connections from external agents.
|
|
||||||
|
|
||||||
## Quick Facts
|
|
||||||
- **Type**: Library
|
|
||||||
- **Main Entry**: src/lib.rs
|
|
||||||
- **Dependencies**: axum, tokio, serde, tracing, uuid, async-trait, dirigent_protocol
|
|
||||||
- **Status**: Core structure complete, integration with CoreRuntime pending
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The `dirigent_acp_api` package implements an ACP (Agent-Client Protocol) server that allows Dirigent to accept incoming connections from external ACP clients like Claude Code or custom agents. This enables session sharing, remote orchestration, and multi-client collaboration.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
- **config.rs** - Server configuration types (`AcpServerConfig`)
|
|
||||||
- **error.rs** - Error types (`AcpServerError`, `JsonRpcErrorObject`)
|
|
||||||
- **jsonrpc.rs** - JSON-RPC 2.0 types and parsing
|
|
||||||
- **rpc.rs** - RPC handler and method dispatch
|
|
||||||
- **session_manager.rs** - Session/client tracking (TODO)
|
|
||||||
- **sse.rs** - SSE notification system (TODO)
|
|
||||||
- **event_bridge.rs** - Event translation (TODO)
|
|
||||||
|
|
||||||
### Key Types
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct AcpServerConfig {
|
|
||||||
pub enabled: bool, // Enable/disable server
|
|
||||||
pub port: u16, // Listen port (default: 3001)
|
|
||||||
pub allowed_origins: Option<Vec<String>>, // CORS origins
|
|
||||||
pub max_connections: usize, // Connection limit (default: 100)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ConnectorOperations Trait
|
|
||||||
|
|
||||||
The RPC handler uses a trait abstraction to avoid circular dependencies with dirigent_core:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
pub trait ConnectorOperations: Send + Sync {
|
|
||||||
async fn create_session(&self, connector_id: &str) -> Result<String>;
|
|
||||||
async fn load_session(&self, connector_id: &str, session_id: &str) -> Result<Session>;
|
|
||||||
async fn send_prompt(&self, connector_id: &str, session_id: &str, prompt: &str) -> Result<String>;
|
|
||||||
// ... more methods
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### POST `/rpc`
|
|
||||||
|
|
||||||
JSON-RPC 2.0 endpoint supporting:
|
|
||||||
- `initialize` - Client handshake
|
|
||||||
- `session/new` - Create session
|
|
||||||
- `session/load` - Load existing session
|
|
||||||
- `session/prompt` - Send prompt
|
|
||||||
- `session/cancel` - Cancel generation
|
|
||||||
- `session/close` - Close session
|
|
||||||
|
|
||||||
### GET `/events`
|
|
||||||
|
|
||||||
Server-Sent Events for streaming notifications:
|
|
||||||
- `acp/messageChunk` - Streaming content
|
|
||||||
- `acp/messageComplete` - Generation complete
|
|
||||||
- `acp/sessionIdle` - Ready for input
|
|
||||||
|
|
||||||
### GET `/health`
|
|
||||||
|
|
||||||
Health check endpoint.
|
|
||||||
|
|
||||||
## Configuration UI
|
|
||||||
|
|
||||||
The ACP Server is configured via the web UI at **Configuration > ACP Server**:
|
|
||||||
|
|
||||||
- Enable/disable toggle
|
|
||||||
- Port configuration
|
|
||||||
- Max connections limit
|
|
||||||
- Allowed origins (CORS)
|
|
||||||
- Default connector selection
|
|
||||||
- Connected clients management
|
|
||||||
|
|
||||||
Server functions in `crates/api/src/acp_server.rs` bridge the UI and this package.
|
|
||||||
|
|
||||||
## Implementation Status
|
|
||||||
|
|
||||||
**Completed:**
|
|
||||||
- Configuration types (`AcpServerConfig`)
|
|
||||||
- Error types (`AcpServerError`)
|
|
||||||
- JSON-RPC types and parsing
|
|
||||||
- RPC handler structure with ConnectorOperations trait
|
|
||||||
|
|
||||||
**Pending:**
|
|
||||||
- Session Manager implementation
|
|
||||||
- SSE Notifier implementation
|
|
||||||
- Event Bridge implementation
|
|
||||||
- Axum router integration
|
|
||||||
- Web server integration
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
| File | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `src/lib.rs` | Module exports and router creation |
|
|
||||||
| `src/config.rs` | AcpServerConfig with validation |
|
|
||||||
| `src/error.rs` | Error types and codes |
|
|
||||||
| `src/jsonrpc.rs` | JSON-RPC 2.0 implementation |
|
|
||||||
| `src/rpc.rs` | RPC handler and method dispatch |
|
|
||||||
|
|
||||||
## Related Packages
|
|
||||||
|
|
||||||
- **dirigent_core** - Provides CoreHandle implementation of ConnectorOperations
|
|
||||||
- **dirigent_protocol** - Shared event and message types
|
|
||||||
- **api** - Server functions for UI configuration
|
|
||||||
- **web** - Configuration UI components
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- **Architecture**: `docs/architecture/acp_server.md`
|
|
||||||
- **Configuration**: `docs/configuration/acp-connectors.md`
|
|
||||||
- **Tasks**: `docs/building/07_acp_serve/02_acp_server_tasks.md`
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# dirigent_config
|
|
||||||
|
|
||||||
Platform-native configuration and data path resolution.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
Provides `DirigentPaths` for resolving config/data directories across Linux, macOS, Windows.
|
|
||||||
Creates a symlink on Linux/macOS from config_dir/data -> data_dir for discoverability.
|
|
||||||
|
|
||||||
## Key Types
|
|
||||||
- `DirigentPaths` -- resolved config_dir + data_dir with convenience methods
|
|
||||||
- `ConfigPathError` -- error enum for path resolution failures
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
```rust
|
|
||||||
let paths = DirigentPaths::resolve()?;
|
|
||||||
paths.ensure_dirs()?; // creates dirs + symlink
|
|
||||||
let config = paths.config_file(); // ~/.config/dirigent/dirigent.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
- `dirs` -- cross-platform directory resolution
|
|
||||||
- Zero UI dependency -- used by core, archivist, zed crates
|
|
||||||
@@ -1,616 +0,0 @@
|
|||||||
# Package: dirigent_core
|
|
||||||
|
|
||||||
Core orchestration engine for multi-connector agent system management.
|
|
||||||
|
|
||||||
## Quick Facts
|
|
||||||
- **Type**: Library
|
|
||||||
- **Main Entry**: src/lib.rs
|
|
||||||
- **Dependencies**: dirigent_protocol, tokio, axum, serde, uuid
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
The dirigent_core package provides a **runtime-based architecture** for managing long-lived connections to external agent systems (OpenCode.ai, ACP agents, etc.). The core abstraction is the **Connector**, which represents a bidirectional communication channel to an agent system.
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
#### CoreRuntime
|
|
||||||
The central orchestrator for managing connectors. It maintains:
|
|
||||||
- Registry of active connectors (keyed by ConnectorId)
|
|
||||||
- Global event broadcast channel for system-wide events
|
|
||||||
- User registry for ownership and authorization
|
|
||||||
- Configuration state (with persistence support)
|
|
||||||
|
|
||||||
#### CoreHandle
|
|
||||||
Lightweight, cloneable wrapper around CoreRuntime. Uses Arc internally for cheap cloning across async tasks and server functions.
|
|
||||||
|
|
||||||
#### Connector Trait
|
|
||||||
Defines the interface for connector implementations:
|
|
||||||
- Command channel (mpsc) for control operations
|
|
||||||
- Event broadcast channel for publishing events
|
|
||||||
- State tracking (Initializing, Connecting, Ready, Error, Stopped)
|
|
||||||
- User ownership for authorization
|
|
||||||
|
|
||||||
#### ConnectorHandle
|
|
||||||
Concrete implementation of the Connector trait that wraps:
|
|
||||||
- Metadata (id, kind, owner, title)
|
|
||||||
- Shared state (protected by RwLock)
|
|
||||||
- Command and event channels
|
|
||||||
- Optional task handle for lifecycle management
|
|
||||||
|
|
||||||
### Connector Implementations
|
|
||||||
|
|
||||||
#### OpenCodeConnector
|
|
||||||
Connector for OpenCode.ai REST + SSE API:
|
|
||||||
- HTTP client for session/message operations
|
|
||||||
- SSE event stream for real-time updates
|
|
||||||
- Background task loop for command processing
|
|
||||||
- State machine for connection lifecycle
|
|
||||||
- **TurnComplete emission**: Uses `TurnCompleteTrigger::ExplicitSignal` after `MessageCompleted` events (based on upstream session.idle signals)
|
|
||||||
|
|
||||||
#### AcpConnector (Future)
|
|
||||||
Connector for Agent-Client Protocol:
|
|
||||||
- WebSocket or HTTP/2 transport
|
|
||||||
- ACP message protocol handling
|
|
||||||
- Tool execution and streaming support
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
### Core Runtime
|
|
||||||
- `src/runtime.rs` - CoreRuntime and CoreHandle implementation
|
|
||||||
- `src/types.rs` - Core types (ConnectorId, ConnectorState, User, etc.)
|
|
||||||
- `src/error.rs` - Error types for the runtime
|
|
||||||
- `src/config.rs` - Configuration types and template system
|
|
||||||
|
|
||||||
### Connectors
|
|
||||||
- `src/connectors/mod.rs` - Connector trait and ConnectorHandle
|
|
||||||
- `src/connectors/opencode/mod.rs` - OpenCode connector implementation
|
|
||||||
- `src/connectors/opencode/config.rs` - OpenCode-specific configuration
|
|
||||||
- `src/connectors/acp/mod.rs` - ACP connector implementation (in progress)
|
|
||||||
|
|
||||||
### ACP Protocol Implementation
|
|
||||||
- `src/acp/protocol/initialize.rs` - Protocol initialization and capability negotiation
|
|
||||||
- `src/acp/protocol/authenticate.rs` - Optional authentication flow
|
|
||||||
- `src/acp/protocol/session.rs` - Session lifecycle (new, load, set_mode, cancel)
|
|
||||||
- `src/acp/protocol/prompt.rs` - Prompt requests with content blocks (Phases 3-7 complete)
|
|
||||||
- `src/acp/protocol/streaming.rs` - Session update notifications and handlers
|
|
||||||
- `src/acp/protocol/stop_reason.rs` - Stop reason interpretation and actions
|
|
||||||
- `src/acp/protocol/cancellation.rs` - Cancellation and disconnect handling
|
|
||||||
- `src/acp/protocol/error.rs` - Error classification and retry logic
|
|
||||||
- `src/acp/connector_state.rs` - Connection and session state management
|
|
||||||
- `src/acp/transport/mod.rs` - Transport abstraction layer
|
|
||||||
- `src/acp/transport/stdio.rs` - Stdio transport (process spawning)
|
|
||||||
- `src/acp/transport/http.rs` - HTTP+SSE transport
|
|
||||||
|
|
||||||
### Bidirectional Request Handling Pattern
|
|
||||||
|
|
||||||
The ACP connector implements **true bidirectional communication** where both client and agent can send JSON-RPC requests at any time. This creates an architectural challenge: how to maintain synchronous request/response semantics (async/await) while handling incoming agent requests during outgoing client requests.
|
|
||||||
|
|
||||||
**The Challenge:**
|
|
||||||
|
|
||||||
When the connector sends `session/prompt` to the agent:
|
|
||||||
1. It calls `send_request()` which awaits the JSON-RPC response
|
|
||||||
2. The agent may send permission requests (e.g., `tools/write`) before responding
|
|
||||||
3. These agent requests arrive as `ConnectorCommand::AgentResponse` via the command channel
|
|
||||||
4. If `send_request()` only polls transport and response channels, the command channel isn't polled
|
|
||||||
5. **Result**: Deadlock - agent waits for permission → permission stuck in channel → client waits for prompt response
|
|
||||||
|
|
||||||
**The Solution (src/connectors/acp/connector.rs:1253-1523):**
|
|
||||||
|
|
||||||
The `send_request()` method uses `tokio::select!` to poll **three** sources simultaneously:
|
|
||||||
- **Response channel** (`response_rx`) - Waiting for the correlated JSON-RPC response
|
|
||||||
- **Transport channel** - Receiving messages/notifications from agent (may trigger response)
|
|
||||||
- **Command channel** (`cmd_rx`) - Receiving commands from event bridge (e.g., `AgentResponse`)
|
|
||||||
|
|
||||||
When an `AgentResponse` command arrives during `send_request()`:
|
|
||||||
1. Extract the response payload from the command
|
|
||||||
2. Send it to the agent via transport immediately
|
|
||||||
3. Remove from pending requests map
|
|
||||||
4. Continue waiting for the original prompt response
|
|
||||||
|
|
||||||
This pattern is **idiomatic async Rust** for implementing synchronous abstractions over bidirectional transports. It's similar to:
|
|
||||||
- gRPC bidirectional streaming with request/response correlation
|
|
||||||
- WebSocket clients with RPC-style method calls
|
|
||||||
- HTTP/2 multiplexing with concurrent streams
|
|
||||||
|
|
||||||
**Why Not Separate Tasks?**
|
|
||||||
|
|
||||||
Alternative architectures (separate command processing task, full actor model) introduce:
|
|
||||||
- Complex synchronization between tasks
|
|
||||||
- Race conditions on shared state
|
|
||||||
- Message ordering guarantees across channels
|
|
||||||
- Significantly more code and cognitive overhead
|
|
||||||
|
|
||||||
The single-task, multi-channel select pattern keeps all state local and eliminates these issues.
|
|
||||||
|
|
||||||
**Key Invariant:**
|
|
||||||
|
|
||||||
Any method that blocks waiting for a response MUST also poll the command channel to process `AgentResponse` commands, otherwise bidirectional flows deadlock.
|
|
||||||
|
|
||||||
## Main Exports
|
|
||||||
|
|
||||||
### Runtime
|
|
||||||
- `CoreRuntime` - Main orchestrator
|
|
||||||
- `CoreHandle` - Cloneable runtime handle
|
|
||||||
- `CoreConfig` - Runtime configuration
|
|
||||||
|
|
||||||
### Types
|
|
||||||
- `ConnectorId`, `UserId` - Type aliases for IDs
|
|
||||||
- `ConnectorKind` - Enum of connector types (OpenCode, Acp, Mock)
|
|
||||||
- `ConnectorState` - Lifecycle state enum
|
|
||||||
- `ConnectorSummary` - Lightweight connector view
|
|
||||||
- `User` - User information
|
|
||||||
|
|
||||||
### Connectors
|
|
||||||
- `Connector` - Trait for connector implementations
|
|
||||||
- `ConnectorHandle` - Handle to a running connector
|
|
||||||
- `ConnectorCommand` - Commands sent to connectors
|
|
||||||
- `OpenCodeConnector` - OpenCode.ai integration
|
|
||||||
- `OpenCodeConfig` - OpenCode connector configuration
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
- `ConnectorConfig` - Configuration for creating connectors
|
|
||||||
- `apply_template()` - Apply connector templates with patches
|
|
||||||
|
|
||||||
### ACP Protocol Types (Phases 3-7)
|
|
||||||
- **Prompt Turn**:
|
|
||||||
- `SessionPromptRequest`, `SessionPromptResponse` - Prompt requests/responses
|
|
||||||
- `ContentBlock` - Text, Image, Audio, Resource, ResourceLink content
|
|
||||||
- `EmbeddedResource` - Text or Blob embedded resources
|
|
||||||
- `StopReason` - EndTurn, MaxTokens, MaxTurnRequests, Refusal, Cancelled
|
|
||||||
- `PromptError` - Timeout, JsonRpcError, TransportError, Cancelled, ValidationError
|
|
||||||
|
|
||||||
- **Streaming Updates**:
|
|
||||||
- `SessionUpdate` - All update types (agent_message_chunk, tool_call, plan, etc.)
|
|
||||||
- `SessionUpdateNotification` - Notification wrapper with session_id
|
|
||||||
- `ToolCallInfo` - Tool call tracking with status and content
|
|
||||||
- `ToolKind` - Read, Edit, Search, Execute, Think, Other
|
|
||||||
- `ToolCallStatus` - Pending, InProgress, Completed, Failed, Cancelled
|
|
||||||
- `ToolCallContent` - Content, Diff, Terminal output
|
|
||||||
- `MessageAccumulator` - Message chunk accumulation helper
|
|
||||||
- `PlanEntry`, `Command` - Plan and command structures
|
|
||||||
|
|
||||||
- **Stop Reason Handling**:
|
|
||||||
- `StopReasonAction` - Complete, ShowWarning, ShowError, ShowInfo
|
|
||||||
- `handle_stop_reason()` - Interpret stop reasons
|
|
||||||
- `is_continuable()`, `is_error()` - Stop reason classification
|
|
||||||
|
|
||||||
- **Cancellation**:
|
|
||||||
- `handle_cancellation()` - Cancel pending operations
|
|
||||||
- `cancel_pending_tool_calls()` - Mark tool calls as cancelled
|
|
||||||
- `handle_disconnect()` - Handle transport disconnect
|
|
||||||
|
|
||||||
- **Error Classification**:
|
|
||||||
- `ClassifiedError` - Error with class, message, details, retry_after
|
|
||||||
- `ErrorClass` - Transient, Terminal, User
|
|
||||||
- `ErrorSeverity` - Info, Warning, Error, Fatal
|
|
||||||
- `ErrorAction` - Retry, Reconnect, CheckConfig, ContactSupport, Dismiss
|
|
||||||
- `classify_jsonrpc_error()`, `classify_transport_error()` - Classify errors
|
|
||||||
- `exponential_backoff()` - Calculate retry delays
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Creating a Runtime
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use dirigent_core::{CoreRuntime, CoreConfig, CoreHandle};
|
|
||||||
|
|
||||||
// Load config from file or use default
|
|
||||||
let config = CoreConfig::load_config(None)?;
|
|
||||||
|
|
||||||
// Create runtime
|
|
||||||
let runtime = CoreRuntime::new(config);
|
|
||||||
|
|
||||||
// Wrap in handle for cheap cloning
|
|
||||||
let handle = CoreHandle::new(runtime);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Creating a Connector
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use dirigent_core::{ConnectorConfig, ConnectorKind, OpenCodeConfig};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
// Build OpenCode connector config
|
|
||||||
let config = OpenCodeConfig {
|
|
||||||
base_url: "http://localhost:12225".to_string(),
|
|
||||||
title: "My OpenCode".to_string(),
|
|
||||||
initial_session: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Serialize to JSON for ConnectorConfig
|
|
||||||
let params = serde_json::to_value(&config)?;
|
|
||||||
|
|
||||||
let connector_config = ConnectorConfig {
|
|
||||||
id: None, // Runtime generates ID
|
|
||||||
kind: ConnectorKind::OpenCode,
|
|
||||||
owner: Some("user-123".to_string()),
|
|
||||||
title: Some("My OpenCode".to_string()),
|
|
||||||
params,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create connector via runtime
|
|
||||||
let connector_id = handle.create_connector(
|
|
||||||
"user-123".to_string(),
|
|
||||||
connector_config
|
|
||||||
).await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Templates
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use dirigent_core::{apply_template, ConnectorKind};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
// Use default template with custom URL
|
|
||||||
let connector_config = apply_template(
|
|
||||||
ConnectorKind::OpenCode,
|
|
||||||
"default",
|
|
||||||
json!({
|
|
||||||
"base_url": "http://localhost:8080",
|
|
||||||
"title": "Custom OpenCode"
|
|
||||||
})
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let connector_id = handle.create_connector(
|
|
||||||
"user-123".to_string(),
|
|
||||||
connector_config
|
|
||||||
).await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Managing Connectors
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// List all connectors
|
|
||||||
let all_connectors = handle.list_connectors(None).await;
|
|
||||||
|
|
||||||
// List connectors for a specific user
|
|
||||||
let user_connectors = handle.list_connectors(Some("user-123".to_string())).await;
|
|
||||||
|
|
||||||
// Get a specific connector
|
|
||||||
let connector = handle.get_connector(&connector_id).await;
|
|
||||||
|
|
||||||
// Stop a connector
|
|
||||||
handle.stop_connector(&connector_id).await?;
|
|
||||||
|
|
||||||
// Restart a stopped connector
|
|
||||||
handle.restart_connector(&connector_id).await?;
|
|
||||||
|
|
||||||
// Remove a connector
|
|
||||||
handle.remove_connector(&connector_id).await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Connector Lifecycle with Restart
|
|
||||||
|
|
||||||
Connectors can be restarted after being stopped or entering an error state:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Create and start a connector
|
|
||||||
let connector_id = handle.create_connector(
|
|
||||||
"user-123".to_string(),
|
|
||||||
connector_config
|
|
||||||
).await?;
|
|
||||||
// Connector is now in Ready state
|
|
||||||
|
|
||||||
// Stop the connector
|
|
||||||
handle.stop_connector(&connector_id).await?;
|
|
||||||
// Connector is now in Stopped state
|
|
||||||
|
|
||||||
// Restart the connector (recreates background task with fresh channels)
|
|
||||||
handle.restart_connector(&connector_id).await?;
|
|
||||||
// Connector transitions: Stopped → Initializing → Connecting → Ready
|
|
||||||
|
|
||||||
// Restart preserves:
|
|
||||||
// - Connector ID (same instance)
|
|
||||||
// - Configuration (base_url, title, etc.)
|
|
||||||
// - Event broadcast channel (subscribers continue receiving)
|
|
||||||
// - State Arc (observers see real-time updates)
|
|
||||||
|
|
||||||
// Restart recreates:
|
|
||||||
// - Command channel (new sender/receiver pair)
|
|
||||||
// - Connector instance (fresh OpenCodeConnector)
|
|
||||||
// - Background task (new spawn)
|
|
||||||
// - Task handle (new JoinHandle)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sending Commands
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use dirigent_core::connectors::ConnectorCommand;
|
|
||||||
|
|
||||||
// Get connector handle
|
|
||||||
let connector = handle.get_connector(&connector_id).await.unwrap();
|
|
||||||
|
|
||||||
// Subscribe to events
|
|
||||||
let mut events = connector.subscribe();
|
|
||||||
|
|
||||||
// Send a command
|
|
||||||
let cmd_tx = connector.command_tx();
|
|
||||||
cmd_tx.send(ConnectorCommand::ListSessions).await?;
|
|
||||||
|
|
||||||
// Receive events
|
|
||||||
while let Ok(event) = events.recv().await {
|
|
||||||
match event {
|
|
||||||
Event::SessionsListed { sessions } => {
|
|
||||||
println!("Got {} sessions", sessions.len());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Event::Error { message } => {
|
|
||||||
eprintln!("Error: {}", message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Global Event Stream
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Subscribe to every event on the SharingBus. Callers can also pick an
|
|
||||||
// `EventFilter` via `subscribe_filtered()` to receive only the events
|
|
||||||
// they care about.
|
|
||||||
let mut bus_rx = handle.sharing_bus().subscribe_all().await;
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
while let Some(bus_event) = bus_rx.rx.recv().await {
|
|
||||||
println!("Bus event: {:?}", bus_event.event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Runtime Configuration (dirigent.toml or dirigent.json)
|
|
||||||
|
|
||||||
```toml
|
|
||||||
port = 3000
|
|
||||||
project_dir = "."
|
|
||||||
project_name = "my_project"
|
|
||||||
templates_enabled = true
|
|
||||||
|
|
||||||
[[connectors]]
|
|
||||||
id = "opencode-1"
|
|
||||||
kind = "OpenCode"
|
|
||||||
owner = "user-123"
|
|
||||||
title = "OpenCode Local"
|
|
||||||
|
|
||||||
[connectors.params]
|
|
||||||
base_url = "http://localhost:12225"
|
|
||||||
title = "OpenCode Local"
|
|
||||||
initial_session = null
|
|
||||||
```
|
|
||||||
|
|
||||||
### Templates
|
|
||||||
|
|
||||||
Available templates:
|
|
||||||
- `opencode/default` - Standard localhost OpenCode connector
|
|
||||||
- `acp/claude-default` - Claude API connector (stub for future)
|
|
||||||
|
|
||||||
## Connector Event Emission Patterns
|
|
||||||
|
|
||||||
### TurnComplete Event Semantics
|
|
||||||
|
|
||||||
All connectors emit `Event::TurnComplete` to signal that a turn/message is finalized. This is the **primary signal** for:
|
|
||||||
- Archivist to finalize and write message to disk
|
|
||||||
- UI cache to lock message state as immutable
|
|
||||||
- Conductor bridge to flush response to upstream
|
|
||||||
|
|
||||||
**Event ordering guarantee**:
|
|
||||||
```text
|
|
||||||
MessageCompleted → TurnComplete → SessionIdle
|
|
||||||
```
|
|
||||||
|
|
||||||
### Connector-Specific TurnComplete Strategies
|
|
||||||
|
|
||||||
Different connectors use different strategies to determine when a turn is complete:
|
|
||||||
|
|
||||||
#### OpenCode Connector
|
|
||||||
- **Trigger**: `TurnCompleteTrigger::ExplicitSignal`
|
|
||||||
- **Strategy**: Relies on upstream `session.idle` events from OpenCode.ai
|
|
||||||
- **Implementation**: After translating `MessageCompleted`, emits `TurnComplete` then `SessionIdle`
|
|
||||||
- **Code location**: `crates/dirigent_core/src/connectors/opencode.rs:550-575`
|
|
||||||
|
|
||||||
#### ACP Connector (stdio transport)
|
|
||||||
- **Trigger**: `TurnCompleteTrigger::ResponseReceived`
|
|
||||||
- **Strategy**: JSON-RPC response message is the final message in a turn
|
|
||||||
- **Implementation**: Emits `TurnComplete` after receiving the JSON-RPC response to `session/prompt`
|
|
||||||
- **Code location**: `crates/dirigent_core/src/connectors/acp/connector.rs:328`
|
|
||||||
|
|
||||||
#### Gateway Connector
|
|
||||||
- **Trigger**: `TurnCompleteTrigger::OperationsComplete`
|
|
||||||
- **Strategy**: Tracks pending tool calls and emits when all operations resolve
|
|
||||||
- **Implementation**: Monitors tool call status changes and emits when last pending call completes
|
|
||||||
- **Code location**: `crates/dirigent_core/src/connectors/gateway/mod.rs:464,556,626,678`
|
|
||||||
|
|
||||||
**Important**: Connectors MUST emit `TurnComplete` exactly once per turn. Duplicate emissions can cause archiving issues and UI state corruption.
|
|
||||||
|
|
||||||
### Gateway Session Transfer Mechanics
|
|
||||||
|
|
||||||
The Gateway connector serves as an **entry point** for incoming ACP connections. Sessions can be transferred to real agent connectors (Claude, etc.) via `/select-connector` commands.
|
|
||||||
|
|
||||||
#### Key Principle: New Connector Is Authority
|
|
||||||
|
|
||||||
**After transfer, the target connector becomes the sole authority for session configuration.**
|
|
||||||
|
|
||||||
```text
|
|
||||||
Before transfer:
|
|
||||||
Gateway has placeholder modes: "ask", "write", "yolo"
|
|
||||||
Gateway has placeholder models: "simple", "default", "high"
|
|
||||||
|
|
||||||
After transfer to Claude:
|
|
||||||
Claude's actual modes/models become authoritative
|
|
||||||
Gateway's placeholders are irrelevant
|
|
||||||
Editor receives config_option_update with Claude's real options
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Transfer Flow
|
|
||||||
|
|
||||||
1. User sends `/select-connector claude` in Gateway session
|
|
||||||
2. Gateway emits `SessionTransferRequest` to CoreRuntime
|
|
||||||
3. CoreRuntime creates/loads session in target connector
|
|
||||||
4. Target connector emits `SessionCreated` with its modes/models
|
|
||||||
5. CoreRuntime extracts modes/models from `SessionCreated` event
|
|
||||||
6. CoreRuntime emits `SessionTransferred` event with modes/models
|
|
||||||
7. Event bridge sends `config_option_update` to editor with target's modes/models
|
|
||||||
|
|
||||||
#### What Does NOT Happen
|
|
||||||
|
|
||||||
- Gateway does NOT adjust or map its values to the target connector
|
|
||||||
- Gateway does NOT remain involved after transfer completes
|
|
||||||
- Target connector does NOT inherit Gateway's mode/model selections
|
|
||||||
- No "mapping" between Gateway placeholders and real connector values
|
|
||||||
|
|
||||||
#### Code Locations
|
|
||||||
|
|
||||||
- Transfer request handling: `src/runtime.rs:execute_transfer()`
|
|
||||||
- Gateway commands: `src/connectors/gateway/commands.rs`
|
|
||||||
- SessionTransferred event: `dirigent_protocol/src/events/mod.rs`
|
|
||||||
- config_option_update emission: `dirigent_acp_api/src/event_bridge.rs:handle_session_transferred_internal()`
|
|
||||||
|
|
||||||
## Architecture Patterns
|
|
||||||
|
|
||||||
### Request-Response Pattern
|
|
||||||
For operations that need results (list_sessions, list_messages):
|
|
||||||
1. Subscribe to connector events
|
|
||||||
2. Send command via command channel
|
|
||||||
3. Wait for corresponding response event (with timeout)
|
|
||||||
4. Return result or error
|
|
||||||
|
|
||||||
### Fire-and-Forget Pattern
|
|
||||||
For operations that stream results (send_message):
|
|
||||||
1. Send command via command channel
|
|
||||||
2. Return immediately
|
|
||||||
3. Clients subscribe to event stream for updates
|
|
||||||
|
|
||||||
### Lifecycle Management
|
|
||||||
Connectors progress through states:
|
|
||||||
1. **Initializing** - Created but not connecting
|
|
||||||
2. **Connecting** - Attempting to establish connection
|
|
||||||
3. **Ready** - Connected and operational
|
|
||||||
4. **Error** - Encountered failure (with error message)
|
|
||||||
5. **Stopped** - Shutdown or unrecoverable error
|
|
||||||
|
|
||||||
#### Restart Support
|
|
||||||
Connectors in `Stopped` or `Error` state can be restarted:
|
|
||||||
- **restart_connector()** recreates the connector's background task with fresh channels
|
|
||||||
- Preserves connector identity (ID, owner, configuration)
|
|
||||||
- Preserves event broadcast channel (existing subscribers continue receiving)
|
|
||||||
- Recreates command channel and background task
|
|
||||||
- State transitions: `Stopped`/`Error` → `Initializing` → `Connecting` → `Ready`
|
|
||||||
|
|
||||||
### Configuration Persistence
|
|
||||||
The runtime automatically saves configuration to disk when:
|
|
||||||
- A connector is created
|
|
||||||
- A connector is removed
|
|
||||||
- Configuration is explicitly saved
|
|
||||||
|
|
||||||
This ensures connectors are restored on server restart.
|
|
||||||
|
|
||||||
## Session Tracking Responsibilities
|
|
||||||
|
|
||||||
**IMPORTANT**: CoreRuntime is a **stateless orchestrator** for session operations. It does NOT maintain session state or cache message history.
|
|
||||||
|
|
||||||
### What CoreRuntime Does
|
|
||||||
|
|
||||||
- **Route Commands**: Forward session/message commands to appropriate connectors
|
|
||||||
- **Broadcast Events**: Relay connector events to global event stream
|
|
||||||
- **Manage Connectors**: Track which connectors are active and available
|
|
||||||
- **Persist Configuration**: Save/load connector configuration (not session data)
|
|
||||||
|
|
||||||
### What CoreRuntime Does NOT Do
|
|
||||||
|
|
||||||
- **Cache Sessions**: Does not maintain lists of sessions or session metadata
|
|
||||||
- **Store Messages**: Does not retain message content or history
|
|
||||||
- **Buffer Events**: Does not cache events for replay or historical access
|
|
||||||
- **Track Session State**: Does not know which sessions exist or their current state
|
|
||||||
|
|
||||||
### Stateless Design Rationale
|
|
||||||
|
|
||||||
1. **Multi-Connector Support**: With multiple connectors (OpenCode, ACP, etc.), caching sessions would require complex invalidation
|
|
||||||
2. **Memory Efficiency**: Long-running server should not accumulate unbounded session history
|
|
||||||
3. **Single Source of Truth**: External APIs (OpenCode.ai, ACP agents) are authoritative for session state
|
|
||||||
4. **Scalability**: Stateless design supports future horizontal scaling
|
|
||||||
|
|
||||||
### Data Flow for Session Operations
|
|
||||||
|
|
||||||
**List Sessions**:
|
|
||||||
```
|
|
||||||
Server Function → CoreRuntime.get_connector() → Send Command → Connector queries API
|
|
||||||
↓ ↓
|
|
||||||
Returns handle Broadcasts SessionsListed
|
|
||||||
↓
|
|
||||||
Server function returns to UI
|
|
||||||
(CoreRuntime does NOT cache list)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Send Message**:
|
|
||||||
```
|
|
||||||
Server Function → CoreRuntime.get_connector() → Send Command → Connector sends to API
|
|
||||||
↓ ↓
|
|
||||||
Returns handle Broadcasts MessageSent
|
|
||||||
↓
|
|
||||||
SSE pushes to UI in real-time
|
|
||||||
(CoreRuntime does NOT store message)
|
|
||||||
```
|
|
||||||
|
|
||||||
All session data passes through CoreRuntime but is **never cached**. See `docs/architecture/session_tracking.md` for the complete three-layer architecture (CoreRuntime, UI Cache, Archivist).
|
|
||||||
|
|
||||||
## Phase 4: `SharingBus` + `StreamRegistry` (2026-04-21)
|
|
||||||
|
|
||||||
Every `Event` emitted by a connector or the runtime is published onto a
|
|
||||||
single `SharingBus` that owns:
|
|
||||||
|
|
||||||
- A `broadcast::Sender<BusEvent>` as the internal multicast.
|
|
||||||
- A worker task that receives from that broadcast and dispatches per-
|
|
||||||
subscriber `mpsc::Sender<BusEvent>` pipes with filter matching.
|
|
||||||
- A `HashMap<(connector_id, native_session_id), scroll_id>` cache that
|
|
||||||
late-binds `routing.scroll_id` for events emitted before their
|
|
||||||
`SessionRegistered` event arrived.
|
|
||||||
|
|
||||||
### Subscriber model
|
|
||||||
|
|
||||||
`SharingBus::subscribe_all()` and `subscribe_filtered(EventFilter, cap)`
|
|
||||||
return a `BusReceiver { id, rx, lagged }`. Filters are applied by the
|
|
||||||
worker — subscribers never allocate a closure for skipped events.
|
|
||||||
|
|
||||||
`EventFilter` variants: `All | ScrollId | ConnectorUid | Kinds | AnyOf
|
|
||||||
| AllOf`.
|
|
||||||
|
|
||||||
### Stream registry
|
|
||||||
|
|
||||||
The runtime owns a `StreamRegistry` and a `StreamFactoryRegistry`.
|
|
||||||
Streams are attached via `CoreRuntime::attach_stream(StreamConfig)`:
|
|
||||||
|
|
||||||
- Factory registry resolves the `kind` string → concrete
|
|
||||||
`Arc<dyn SessionStream>`.
|
|
||||||
- The new stream gets its own `BusReceiver` scoped via `scope_to_filter`
|
|
||||||
(Session → ScrollId, Connector → ConnectorUid, ArchiveWide → All).
|
|
||||||
- A worker task pumps events into `stream.on_event(&bus_event).await`.
|
|
||||||
|
|
||||||
Health drift: `record_failure` / `record_success` in `sharing/health.rs`
|
|
||||||
transitions `HealthStatus` on 5 consecutive failures
|
|
||||||
(`Healthy → Degraded → Unavailable`).
|
|
||||||
|
|
||||||
### Replay
|
|
||||||
|
|
||||||
`CoreRuntime::replay_session_to_stream(scroll_id, stream_id, opts)`
|
|
||||||
loads archived messages via the archivist's read API and calls
|
|
||||||
`stream.on_event` directly, bypassing the bus. Each replayed event has
|
|
||||||
`origin = EventOrigin::Replay { replay_id }` and
|
|
||||||
`routing.scroll_id = Some(scroll_id)`.
|
|
||||||
|
|
||||||
### Config
|
|
||||||
|
|
||||||
`[[streams]]` blocks in `dirigent.toml` are parsed into `StreamsConfig`
|
|
||||||
and applied at boot (best-effort: failures log + continue).
|
|
||||||
|
|
||||||
## Related Packages
|
|
||||||
- **api** - Server functions that wrap CoreRuntime operations
|
|
||||||
- **web** - Dioxus UI that calls server functions
|
|
||||||
- **dirigent_protocol** - Shared event and message types
|
|
||||||
- **opencode_client** - Low-level OpenCode.ai HTTP client (used by OpenCodeConnector)
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
- README: ./README.md
|
|
||||||
- Architecture: ../../docs/architecture/overview.md
|
|
||||||
- Migration Guide: ../../docs/migration/singleton_to_runtime.md
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
# dirigent_protocol
|
|
||||||
|
|
||||||
**Version:** 0.2.0
|
|
||||||
**Status:** Active Development
|
|
||||||
|
|
||||||
ACP/MCP-aligned protocol library for agent-client interactions.
|
|
||||||
|
|
||||||
## Quick Links
|
|
||||||
|
|
||||||
- **Package README**: [README.md](README.md) - Main documentation
|
|
||||||
- **Streaming Model**: [docs/streaming_model.md](docs/streaming_model.md) - Detailed SessionUpdate guide
|
|
||||||
- **Migration Guide**: [docs/migration_from_0.1.md](docs/migration_from_0.1.md) - Upgrading from 0.1.x
|
|
||||||
- **Architecture Doc**: [../../docs/architecture/protocol.md](../../docs/architecture/protocol.md) - System design
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
This package provides the core event protocol for Dirigent, enabling:
|
|
||||||
- Real-time streaming of agent interactions
|
|
||||||
- Provider-agnostic event representation
|
|
||||||
- Tool lifecycle management
|
|
||||||
- Structured content representation
|
|
||||||
|
|
||||||
## Key Types
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use dirigent_protocol::{
|
|
||||||
Event, // Top-level event enum
|
|
||||||
SessionUpdate, // Streaming content updates
|
|
||||||
ContentBlock, // Structured content (Text, ResourceLink)
|
|
||||||
ToolCall, // Tool execution state
|
|
||||||
ToolCallStatus, // Pending → Running → Completed/Error
|
|
||||||
TurnCompleteTrigger, // How turn completion was detected
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Event Semantics: MessageCompleted vs TurnComplete vs SessionIdle
|
|
||||||
|
|
||||||
Understanding the distinction between these three events is critical for correct system behavior:
|
|
||||||
|
|
||||||
### MessageCompleted - "Metadata is ready"
|
|
||||||
- **Purpose**: Informational - signals that message metadata exists
|
|
||||||
- **Timing**: Emitted when message record is created, content may still be streaming
|
|
||||||
- **Consumer action**: Update UI status indicators ("Assistant is typing" → "Complete")
|
|
||||||
- **Example**: Show message timestamp, update message count
|
|
||||||
|
|
||||||
### TurnComplete - "All content received" (ACTIONABLE)
|
|
||||||
- **Purpose**: **Primary finalization signal** - all content for this turn is complete
|
|
||||||
- **Timing**: Emitted AFTER all content chunks, tool calls, and metadata updates
|
|
||||||
- **Consumer action**: **Finalize storage, lock state, trigger post-processing**
|
|
||||||
- **Example**: Write message to disk, mark as immutable, generate summaries
|
|
||||||
|
|
||||||
### SessionIdle - "No recent activity"
|
|
||||||
- **Purpose**: Informational - indicates session is quiet
|
|
||||||
- **Timing**: Emitted AFTER TurnComplete as final safety signal
|
|
||||||
- **Consumer action**: Hide spinners, update activity indicators
|
|
||||||
- **Example**: Remove "typing" animation, update last activity timestamp
|
|
||||||
|
|
||||||
### Event Ordering Guarantee
|
|
||||||
|
|
||||||
```text
|
|
||||||
1. MessageStarted (message created)
|
|
||||||
2. SessionUpdate::*Chunk (content streaming)
|
|
||||||
3. SessionUpdate::ToolCall* (tool execution)
|
|
||||||
4. MessageCompleted (metadata ready) ← UI: "Complete"
|
|
||||||
5. TurnComplete ← FINALIZE HERE!
|
|
||||||
6. SessionIdle ← UI: hide spinner
|
|
||||||
```
|
|
||||||
|
|
||||||
### Consumer Behavior Table
|
|
||||||
|
|
||||||
| Consumer | MessageCompleted | TurnComplete | SessionIdle |
|
|
||||||
|----------|------------------|--------------|-------------|
|
|
||||||
| **Archivist** | Ignore | **Finalize and write** | Safety net |
|
|
||||||
| **UI Cache** | Update status | **Lock state** | Hide spinner |
|
|
||||||
| **Conductor Bridge** | - | **Flush response** | Fallback flush |
|
|
||||||
|
|
||||||
### TurnCompleteTrigger Variants
|
|
||||||
|
|
||||||
The `TurnCompleteTrigger` enum indicates **how** the system determined completion:
|
|
||||||
|
|
||||||
- **`ExplicitSignal`**: Upstream provider sent explicit completion (e.g., OpenCode session.idle)
|
|
||||||
- **`ResponseReceived`**: JSON-RPC response received (ACP stdio - response is last message)
|
|
||||||
- **`OperationsComplete`**: All tracked operations finished (e.g., pending tool calls resolved)
|
|
||||||
- **`IdleTimeout { duration_ms }`**: Timeout-based detection (fallback mechanism)
|
|
||||||
|
|
||||||
**For most consumers**, treat all triggers the same - the turn is complete. The trigger type is primarily for debugging and observability.
|
|
||||||
|
|
||||||
## Usage Pattern
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use dirigent_protocol::{Event, SessionUpdate, ContentBlock};
|
|
||||||
|
|
||||||
fn handle_event(event: Event) {
|
|
||||||
match event {
|
|
||||||
Event::SessionUpdate { session_id, update } => {
|
|
||||||
match update {
|
|
||||||
SessionUpdate::AgentMessageChunk { message_id, content, .. } => {
|
|
||||||
if let ContentBlock::Text { text } = content {
|
|
||||||
println!("Agent: {}", text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SessionUpdate::ToolCall { tool_call, .. } => {
|
|
||||||
println!("Tool: {}", tool_call.tool_name);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
dirigent_protocol/
|
|
||||||
├── src/
|
|
||||||
│ ├── types/ # Core types
|
|
||||||
│ │ ├── content.rs # ContentBlock definitions
|
|
||||||
│ │ ├── updates.rs # SessionUpdate variants
|
|
||||||
│ │ ├── tool.rs # ToolCall, ToolCallStatus
|
|
||||||
│ │ └── meta.rs # Provider metadata
|
|
||||||
│ ├── session.rs # Session types
|
|
||||||
│ ├── conversation.rs # Message types
|
|
||||||
│ ├── events.rs # Event enum
|
|
||||||
│ └── adapters/ # Provider adapters
|
|
||||||
│ ├── opencode.rs # OpenCode translation
|
|
||||||
│ └── rest.rs # REST translation
|
|
||||||
├── docs/ # Detailed documentation
|
|
||||||
├── examples/ # Usage examples
|
|
||||||
└── tests/ # Integration tests
|
|
||||||
```
|
|
||||||
|
|
||||||
## Version 0.2.0 Changes
|
|
||||||
|
|
||||||
**Breaking:**
|
|
||||||
- Removed `Event::MessagePartAdded`
|
|
||||||
|
|
||||||
**New:**
|
|
||||||
- `SessionUpdate` event system (ACP-style)
|
|
||||||
- `ContentBlock` types (MCP-compatible)
|
|
||||||
- `ToolCall` with lifecycle tracking
|
|
||||||
- Provider metadata via `_meta`
|
|
||||||
|
|
||||||
See [docs/migration_from_0.1.md](docs/migration_from_0.1.md) for migration guide.
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
```bash
|
|
||||||
cargo test --package dirigent_protocol
|
|
||||||
```
|
|
||||||
|
|
||||||
### Checking Code
|
|
||||||
```bash
|
|
||||||
cargo check --package dirigent_protocol
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running Examples
|
|
||||||
```bash
|
|
||||||
cargo run --package dirigent_protocol --example session_metadata_demo
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration
|
|
||||||
|
|
||||||
This package is used by:
|
|
||||||
- **api** package: Server functions consume protocol events
|
|
||||||
- **web** package: UI renders protocol events
|
|
||||||
- **dirigent_core** (future): Runtime emits protocol events
|
|
||||||
|
|
||||||
## Adapters
|
|
||||||
|
|
||||||
The adapter system translates provider-specific events to Dirigent Protocol:
|
|
||||||
|
|
||||||
- **OpenCodeAdapter**: Translates OpenCode.ai events
|
|
||||||
- **RESTAdapter**: Converts REST API responses
|
|
||||||
|
|
||||||
Adapters preserve provider metadata in the `_meta` field for debugging and traceability.
|
|
||||||
|
|
||||||
## Current Scope
|
|
||||||
|
|
||||||
**Phase 1 (Implemented):**
|
|
||||||
- User/Agent/Thought message streaming
|
|
||||||
- Tool lifecycle (Pending → Running → Completed/Error)
|
|
||||||
- Text and ResourceLink content types
|
|
||||||
- Provider metadata support
|
|
||||||
|
|
||||||
**Deferred to Future Phases:**
|
|
||||||
- Plans and mode switching
|
|
||||||
- Permission system
|
|
||||||
- Embedded resources (full content)
|
|
||||||
- Rich media (images, audio)
|
|
||||||
- Multi-agent communication
|
|
||||||
|
|
||||||
See [../../docs/building/03_acp_prep/04_first_order_refactor.md](../../docs/building/03_acp_prep/04_first_order_refactor.md) for the full plan.
|
|
||||||
|
|
||||||
## Standards Alignment
|
|
||||||
|
|
||||||
**Agent-Client Protocol (ACP):**
|
|
||||||
- Session-centric streaming
|
|
||||||
- Separate content types (user/agent/thought)
|
|
||||||
- Tool status tracking
|
|
||||||
|
|
||||||
**Model Context Protocol (MCP):**
|
|
||||||
- ContentBlock structure
|
|
||||||
- Resource links
|
|
||||||
- Extensible content types
|
|
||||||
|
|
||||||
Differences from standards are documented in [../../docs/architecture/protocol.md](../../docs/architecture/protocol.md).
|
|
||||||
|
|
||||||
## Anti-Patterns
|
|
||||||
|
|
||||||
### Timeout-Based Event Waiting (FORBIDDEN)
|
|
||||||
|
|
||||||
**Never use timeout-based waiting to receive events that should be available immediately.**
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// ❌ BAD - Race condition waiting for event
|
|
||||||
async fn wait_for_metadata_event(
|
|
||||||
events: &mut broadcast::Receiver<Event>,
|
|
||||||
timeout: Duration,
|
|
||||||
) -> Option<SessionMetadataReceived> {
|
|
||||||
let start = Instant::now();
|
|
||||||
while start.elapsed() < timeout {
|
|
||||||
match tokio::time::timeout(Duration::from_millis(100), events.recv()).await {
|
|
||||||
Ok(Ok(Event::SessionMetadataReceived { .. })) => return Some(...),
|
|
||||||
_ => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why this is wrong:**
|
|
||||||
1. **Race condition**: The event may have been emitted before the receiver subscribed
|
|
||||||
2. **Arbitrary delays**: 500ms waits add latency for no good reason
|
|
||||||
3. **Silent failures**: Timeout expiring doesn't indicate the real problem
|
|
||||||
4. **Fragile**: Works "most of the time" but fails under load or timing variations
|
|
||||||
|
|
||||||
**Instead, pass data directly:**
|
|
||||||
```rust
|
|
||||||
// ✅ GOOD - Extract data from existing events
|
|
||||||
async fn create_session_in_connector(...) -> Result<(String, Option<Models>, Option<Modes>), String> {
|
|
||||||
// The SessionCreated event already contains models/modes
|
|
||||||
match event {
|
|
||||||
Event::SessionCreated { session, .. } => {
|
|
||||||
Ok((session.id, session.models, session.modes))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rule**: If you find yourself writing `timeout(Duration::from_millis(N), events.recv())` to wait for an event that "should" arrive, the architecture is wrong. Refactor to pass data directly through return values or existing event payloads.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
When adding features:
|
|
||||||
1. Update type definitions in `src/types/`
|
|
||||||
2. Add comprehensive tests
|
|
||||||
3. Update documentation in `docs/`
|
|
||||||
4. Add examples if applicable
|
|
||||||
5. Update CHANGELOG.md
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- Main project: [../../CLAUDE.md](../../CLAUDE.md)
|
|
||||||
- Architecture docs: [../../docs/architecture/](../../docs/architecture/)
|
|
||||||
- Building docs: [../../docs/building/](../../docs/building/)
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
# Package: dirigent_tools
|
|
||||||
|
|
||||||
Tool implementations for ACP client operations with sandboxing and security.
|
|
||||||
|
|
||||||
## Quick Facts
|
|
||||||
- **Type**: Library
|
|
||||||
- **Main Entry**: src/lib.rs
|
|
||||||
- **Dependencies**: tokio, serde, anyhow, thiserror, tracing, regex, globset, similar, dunce
|
|
||||||
- **Status**: Scaffolding phase - structure complete, implementation pending
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
This package provides the foundational tool operations for the Dirigent ACP client, implementing file operations, terminal execution, and search capabilities with strong security guarantees. It is designed to support ACP-compliant agents by providing safe, sandboxed tool handlers.
|
|
||||||
|
|
||||||
## Module Structure
|
|
||||||
|
|
||||||
### Core Modules
|
|
||||||
- **error** (`src/error.rs`) - Error types for tool operations
|
|
||||||
- **config** (`src/config.rs`) - Configuration types (sandbox, permissions, terminal, search, embedding)
|
|
||||||
- **path** (`src/path.rs`) - Cross-platform path normalization and containment checking
|
|
||||||
|
|
||||||
### Tool Modules
|
|
||||||
- **fs** (`src/fs.rs`) - File read/write/edit operations
|
|
||||||
- **search** (`src/search.rs`) - Glob/grep/ls search operations
|
|
||||||
- **terminal** (`src/terminal.rs`) - Command execution and output capture
|
|
||||||
|
|
||||||
### Security Modules
|
|
||||||
- **permission** (`src/permission.rs`) - Permission prompt system and decision caching
|
|
||||||
- **audit** (`src/audit.rs`) - Structured audit logging
|
|
||||||
|
|
||||||
### Integration Modules
|
|
||||||
- **tool_call** (`src/tool_call.rs`) - ACP tool call metadata and helpers
|
|
||||||
|
|
||||||
## Security Model
|
|
||||||
|
|
||||||
All operations are subject to:
|
|
||||||
|
|
||||||
1. **Sandbox Containment**: Operations restricted to configured allowed roots
|
|
||||||
2. **Blocklist Enforcement**: Sensitive paths explicitly denied
|
|
||||||
3. **Permission Prompts**: Write/execute operations require user approval (configurable)
|
|
||||||
4. **Resource Limits**: Bounded file sizes, search results, terminal output
|
|
||||||
5. **Audit Logging**: All operations logged with structured context
|
|
||||||
|
|
||||||
## Platform Support
|
|
||||||
|
|
||||||
Windows is a first-class platform with explicit support for:
|
|
||||||
- Backslash and forward slash separators
|
|
||||||
- Drive letters (C:\, D:\)
|
|
||||||
- UNC paths (\\server\share\...)
|
|
||||||
- Long path prefixes (\\?\...)
|
|
||||||
- MINGW-style paths (/c/Users/...)
|
|
||||||
- Junctions and symlinks
|
|
||||||
- cmd.exe and PowerShell
|
|
||||||
|
|
||||||
All path normalization and containment logic handles these cases.
|
|
||||||
|
|
||||||
## Configuration (Future)
|
|
||||||
|
|
||||||
Configuration types will be implemented in SCAFF-05:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct SandboxConfig {
|
|
||||||
pub allowed_roots: Vec<PathBuf>,
|
|
||||||
pub blocked_paths: Vec<String>, // glob patterns
|
|
||||||
pub allow_symlink_escape: bool,
|
|
||||||
pub follow_symlinks_within_roots: bool,
|
|
||||||
pub read_enabled: bool,
|
|
||||||
pub write_enabled: bool,
|
|
||||||
pub max_read_bytes: u64,
|
|
||||||
pub max_write_bytes: u64,
|
|
||||||
// ... more fields
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PermissionConfig {
|
|
||||||
pub mode: PermissionMode, // Ask | Whitelist | Yolo
|
|
||||||
pub remember_decisions: bool,
|
|
||||||
pub remember_ttl_secs: u64,
|
|
||||||
pub scope: DecisionScope, // PerConnector | PerSession
|
|
||||||
pub whitelist: WhitelistConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TerminalConfig {
|
|
||||||
pub enabled: bool,
|
|
||||||
pub default_cwd: PathBuf,
|
|
||||||
pub env_allowlist: Vec<String>,
|
|
||||||
pub command_blocklist: Vec<String>,
|
|
||||||
pub output_byte_limit: u64,
|
|
||||||
pub max_runtime_secs: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SearchConfig {
|
|
||||||
pub max_results: u32,
|
|
||||||
pub max_bytes: u64,
|
|
||||||
pub default_include_globs: Vec<String>,
|
|
||||||
pub default_exclude_globs: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EmbeddingConfig {
|
|
||||||
pub max_embed_bytes: u64,
|
|
||||||
pub allow_resource_link: bool,
|
|
||||||
pub redact_patterns: Vec<String>, // regex patterns
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Status
|
|
||||||
|
|
||||||
**Current Phase**: SCAFF-01 (Scaffolding) - Complete
|
|
||||||
|
|
||||||
All modules are stubs with `unimplemented!()`. Implementation will proceed in phases:
|
|
||||||
|
|
||||||
### Phase 1: Path and Sandbox (Protocol tasks)
|
|
||||||
- Path normalization (Windows, Linux, macOS)
|
|
||||||
- Canonical path resolution (symlinks, junctions)
|
|
||||||
- Containment checking
|
|
||||||
- Blocklist matching
|
|
||||||
|
|
||||||
### Phase 2: Tool Operations (Tool tasks)
|
|
||||||
- File read with line ranges
|
|
||||||
- File write with atomic operations
|
|
||||||
- File edit with diff generation
|
|
||||||
- Glob search
|
|
||||||
- Grep search with context
|
|
||||||
- LS directory listing
|
|
||||||
- Terminal creation and lifecycle
|
|
||||||
- Terminal output capture with ring buffer
|
|
||||||
|
|
||||||
### Phase 3: Security and Integration (Integration tasks)
|
|
||||||
- Permission prompt flow
|
|
||||||
- Decision caching with TTL
|
|
||||||
- Audit logging with structured fields
|
|
||||||
- ACP tool call event generation
|
|
||||||
- Tool title formatting
|
|
||||||
|
|
||||||
## Integration Points
|
|
||||||
|
|
||||||
### dirigent_core
|
|
||||||
- Uses `dirigent_tools` for ACP client request handlers
|
|
||||||
- Passes configuration from connector params
|
|
||||||
- Routes tool operations through sandbox
|
|
||||||
|
|
||||||
### api
|
|
||||||
- May use `dirigent_tools` for server function implementations
|
|
||||||
- Exposes configuration endpoints
|
|
||||||
- Handles permission prompts via UI
|
|
||||||
|
|
||||||
### web
|
|
||||||
- Displays tool calls with titles, locations, diffs
|
|
||||||
- Renders permission prompts
|
|
||||||
- Shows audit logs
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
Test infrastructure will be set up in SCAFF-03:
|
|
||||||
- Unit tests for each module
|
|
||||||
- Integration tests with mocker
|
|
||||||
- Cross-platform tests (Windows, Linux, macOS)
|
|
||||||
- Golden transcript fixtures
|
|
||||||
- Test utilities for temp directories, file comparison, sandboxed environments
|
|
||||||
|
|
||||||
## Related Packages
|
|
||||||
- **dirigent_core** - Uses this package for tool operations
|
|
||||||
- **api** - May use this package for server functions
|
|
||||||
- **dirigent_protocol** - Shared event types for tool calls
|
|
||||||
|
|
||||||
## References
|
|
||||||
- Task file: `docs/building/04_acp_client/04_tasks_00_scaffolding_and_finishing.md`
|
|
||||||
- Tools research: `docs/building/04_acp_client/03_tools_research.md`
|
|
||||||
- Sandbox spec: `docs/building/04_acp_client/03_fs_sandboxing_and_permissions_spec.md`
|
|
||||||
- Roadmap: `docs/building/04_acp_client/roadmap.md`
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Package: opencode_client
|
|
||||||
|
|
||||||
Rust client library for interacting with the OpenCode.ai API.
|
|
||||||
|
|
||||||
## Quick Facts
|
|
||||||
- **Type**: Library
|
|
||||||
- **Main Entry**: src/lib.rs
|
|
||||||
- **Dependencies**: reqwest, serde, serde_json, chrono
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
- `src/lib.rs` - Public API exports
|
|
||||||
- `src/types.rs` - OpenCode API type definitions (Session, Message, Part, etc.)
|
|
||||||
- `src/client.rs` - HTTP client implementation with optional logging callbacks
|
|
||||||
|
|
||||||
## Main Exports
|
|
||||||
- `OpenCodeClient` - Main API client with methods: list_sessions, list_messages, send_message
|
|
||||||
- `Session` - Session metadata and configuration
|
|
||||||
- `Message` - User or Assistant message (tagged enum)
|
|
||||||
- `MessageWithParts` - Message info + content parts
|
|
||||||
- `Part` - Text, Reasoning, or workflow parts (StepStart, StepFinish, Tool)
|
|
||||||
- `ClientError` - Error types: Http, Request, Serialization
|
|
||||||
- `LogCallback` - Type alias for logging callbacks
|
|
||||||
|
|
||||||
## Related
|
|
||||||
- Used by: web, mobile (future), desktop (future)
|
|
||||||
- Independent: Can be used in any Rust project
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
- README: ./README.md
|
|
||||||
- API spec: ../../docs/api/opencode.md
|
|
||||||
Reference in New Issue
Block a user