sync from monorepo @ 2452e92e
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
# 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/)
|
||||
Reference in New Issue
Block a user