# 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, timeout: Duration, ) -> Option { 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, Option), 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/)