# 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/)
