🛰️ export from upstream (56c5a2dd)

This commit is contained in:
2026-05-25 17:51:04 +02:00
parent 5829546671
commit 3bcd2fa759
7 changed files with 2 additions and 1229 deletions
+2
View File
@@ -6,6 +6,7 @@
<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.
---
@@ -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.
> [!CAUTION]
> **Expect breakage.** These are internal library crates under active development. Pin to a specific commit if you depend on stability.
---
-124
View File
@@ -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`
-22
View File
@@ -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
-616
View File
@@ -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
-268
View File
@@ -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/)
-169
View File
@@ -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`
-30
View File
@@ -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