Files
dirigent/crates/dirigent_protocol/CLAUDE.md
T
2026-05-08 01:59:04 +02:00

9.0 KiB

dirigent_protocol

Version: 0.2.0 Status: Active Development

ACP/MCP-aligned protocol library for agent-client interactions.

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

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

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

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 for migration guide.

Development

Running Tests

cargo test --package dirigent_protocol

Checking Code

cargo check --package dirigent_protocol

Running Examples

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 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.

Anti-Patterns

Timeout-Based Event Waiting (FORBIDDEN)

Never use timeout-based waiting to receive events that should be available immediately.

// ❌ 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:

// ✅ 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