sync from monorepo @ 2452e92e
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
# Dirigent Protocol Streaming Model
|
||||
|
||||
## Overview
|
||||
|
||||
The Dirigent Protocol uses an **ACP-style streaming model** built around `SessionUpdate` events. This model provides granular, real-time updates during agent interactions, enabling responsive UIs and structured content representation.
|
||||
|
||||
Version: 0.2.0
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### SessionUpdate Events
|
||||
|
||||
All streaming content is delivered through `SessionUpdate` variants wrapped in `Event::SessionUpdate`:
|
||||
|
||||
```rust
|
||||
pub enum Event {
|
||||
// ... other events
|
||||
SessionUpdate {
|
||||
session_id: String,
|
||||
update: SessionUpdate,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The `SessionUpdate` enum contains five variants for different types of streaming updates:
|
||||
|
||||
```rust
|
||||
pub enum SessionUpdate {
|
||||
UserMessageChunk { message_id: String, content: ContentBlock, _meta: Option<Meta> },
|
||||
AgentMessageChunk { message_id: String, content: ContentBlock, _meta: Option<Meta> },
|
||||
AgentThoughtChunk { message_id: String, content: ContentBlock, _meta: Option<Meta> },
|
||||
ToolCall { message_id: String, tool_call: ToolCall, _meta: Option<Meta> },
|
||||
ToolCallUpdate { message_id: String, tool_call_id: String, tool_call: ToolCall, _meta: Option<Meta> },
|
||||
}
|
||||
```
|
||||
|
||||
### ContentBlock Types
|
||||
|
||||
Content is represented using structured `ContentBlock` variants:
|
||||
|
||||
```rust
|
||||
pub enum ContentBlock {
|
||||
Text { text: String },
|
||||
ResourceLink {
|
||||
uri: String,
|
||||
name: Option<String>,
|
||||
mime_type: Option<String>,
|
||||
},
|
||||
// Future: Resource, Image, Audio (marked as out-of-scope for phase 1)
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- `Text` is the primary content type for all textual output
|
||||
- `ResourceLink` represents file references without embedding full content
|
||||
- Future expansions will include embedded resources, images, and audio
|
||||
|
||||
### Provider Metadata (_meta)
|
||||
|
||||
All `SessionUpdate` variants support optional `_meta` fields for provider-specific information:
|
||||
|
||||
```rust
|
||||
pub struct Meta {
|
||||
pub provider: Option<ProviderMeta>,
|
||||
pub extra: HashMap<String, Value>, // Arbitrary additional fields
|
||||
}
|
||||
|
||||
pub struct ProviderMeta {
|
||||
pub name: String, // e.g., "opencode", "anthropic"
|
||||
pub original_ids: Option<HashMap<String, String>>, // Original provider IDs
|
||||
pub raw_excerpt: Option<Value>, // Minimal raw payload for debugging
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
- Adapters populate `_meta` to preserve provider-specific information
|
||||
- Consumers can use this for debugging, telemetry, or provider-specific features
|
||||
- The `extra` map allows arbitrary fields for forward compatibility
|
||||
|
||||
## SessionUpdate Variants
|
||||
|
||||
### 1. UserMessageChunk
|
||||
|
||||
Represents streaming chunks of user message content.
|
||||
|
||||
```rust
|
||||
SessionUpdate::UserMessageChunk {
|
||||
message_id: "msg_abc123".to_string(),
|
||||
content: ContentBlock::Text {
|
||||
text: "What's the capital of France?".to_string(),
|
||||
},
|
||||
_meta: None,
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Streaming user input being typed
|
||||
- Echo of user input from server
|
||||
- Multi-part user messages being assembled
|
||||
|
||||
### 2. AgentMessageChunk
|
||||
|
||||
Represents streaming chunks of agent response content.
|
||||
|
||||
```rust
|
||||
SessionUpdate::AgentMessageChunk {
|
||||
message_id: "msg_def456".to_string(),
|
||||
content: ContentBlock::Text {
|
||||
text: "The capital of France is ".to_string(),
|
||||
},
|
||||
_meta: Some(Meta {
|
||||
provider: Some(ProviderMeta {
|
||||
name: "opencode".to_string(),
|
||||
original_ids: Some(HashMap::from([
|
||||
("message_id".to_string(), "original_123".to_string()),
|
||||
])),
|
||||
raw_excerpt: None,
|
||||
}),
|
||||
extra: HashMap::new(),
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Agent's response text being generated
|
||||
- Final answer content
|
||||
- Any visible agent output
|
||||
|
||||
**Key distinction from AgentThoughtChunk:**
|
||||
- `AgentMessageChunk`: Visible output intended for the user
|
||||
- `AgentThoughtChunk`: Internal reasoning, typically hidden or collapsible
|
||||
|
||||
### 3. AgentThoughtChunk
|
||||
|
||||
Represents streaming chunks of agent internal reasoning (thinking, planning).
|
||||
|
||||
```rust
|
||||
SessionUpdate::AgentThoughtChunk {
|
||||
message_id: "msg_ghi789".to_string(),
|
||||
content: ContentBlock::Text {
|
||||
text: "I need to look up Paris in my knowledge base...".to_string(),
|
||||
},
|
||||
_meta: None,
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Agent's internal reasoning process
|
||||
- "Chain of thought" content
|
||||
- Planning or decision-making process
|
||||
- Content typically displayed in collapsible sections
|
||||
|
||||
**UI Conventions:**
|
||||
- Often hidden by default or shown in a separate "Thinking" section
|
||||
- May be styled differently (e.g., italics, muted colors)
|
||||
- Can be collapsed to save screen space
|
||||
|
||||
### 4. ToolCall
|
||||
|
||||
Represents the initiation or current state of a tool call.
|
||||
|
||||
```rust
|
||||
SessionUpdate::ToolCall {
|
||||
message_id: "msg_jkl012".to_string(),
|
||||
tool_call: ToolCall {
|
||||
id: "call_xyz789".to_string(),
|
||||
tool_name: "read_file".to_string(),
|
||||
status: ToolCallStatus::Pending,
|
||||
content: vec![],
|
||||
raw_input: Some(json!({
|
||||
"file_path": "/path/to/file.txt"
|
||||
})),
|
||||
raw_output: None,
|
||||
title: Some("Read file.txt".to_string()),
|
||||
error: None,
|
||||
metadata: None,
|
||||
},
|
||||
_meta: None,
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Tool call is first initiated
|
||||
- Sending a snapshot of current tool state
|
||||
- Re-sending full tool state after reconnection
|
||||
|
||||
**ToolCallStatus Lifecycle:**
|
||||
- `Pending` → Tool call created but not yet executing
|
||||
- `Running` → Tool call actively executing
|
||||
- `Completed` → Tool call finished successfully
|
||||
- `Error` → Tool call failed
|
||||
|
||||
### 5. ToolCallUpdate
|
||||
|
||||
Represents an update to an existing tool call (status change, new content).
|
||||
|
||||
```rust
|
||||
SessionUpdate::ToolCallUpdate {
|
||||
message_id: "msg_jkl012".to_string(),
|
||||
tool_call_id: "call_xyz789".to_string(),
|
||||
tool_call: ToolCall {
|
||||
id: "call_xyz789".to_string(),
|
||||
tool_name: "read_file".to_string(),
|
||||
status: ToolCallStatus::Running,
|
||||
content: vec![
|
||||
ContentBlock::Text {
|
||||
text: "Reading file...".to_string(),
|
||||
},
|
||||
],
|
||||
raw_input: Some(json!({
|
||||
"file_path": "/path/to/file.txt"
|
||||
})),
|
||||
raw_output: None,
|
||||
title: Some("Read file.txt".to_string()),
|
||||
error: None,
|
||||
metadata: Some(json!({
|
||||
"bytes_read": 1024
|
||||
})),
|
||||
},
|
||||
_meta: None,
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Tool status changes (Pending → Running → Completed/Error)
|
||||
- New output content available
|
||||
- Progress updates
|
||||
- Error state reached
|
||||
|
||||
**Note:** The full `ToolCall` is sent each time, not a delta. Consumers should replace the previous tool call state with the new one.
|
||||
|
||||
## Tool Call Lifecycle
|
||||
|
||||
Understanding the tool call lifecycle is essential for proper UI implementation:
|
||||
|
||||
```rust
|
||||
// 1. Tool call initiated
|
||||
SessionUpdate::ToolCall {
|
||||
tool_call: ToolCall {
|
||||
id: "call_123",
|
||||
status: ToolCallStatus::Pending,
|
||||
content: vec![],
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Tool starts executing
|
||||
SessionUpdate::ToolCallUpdate {
|
||||
tool_call_id: "call_123",
|
||||
tool_call: ToolCall {
|
||||
id: "call_123",
|
||||
status: ToolCallStatus::Running,
|
||||
content: vec![],
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Tool produces output
|
||||
SessionUpdate::ToolCallUpdate {
|
||||
tool_call_id: "call_123",
|
||||
tool_call: ToolCall {
|
||||
id: "call_123",
|
||||
status: ToolCallStatus::Running,
|
||||
content: vec![
|
||||
ContentBlock::Text { text: "Output line 1" },
|
||||
],
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// 4a. Tool completes successfully
|
||||
SessionUpdate::ToolCallUpdate {
|
||||
tool_call_id: "call_123",
|
||||
tool_call: ToolCall {
|
||||
id: "call_123",
|
||||
status: ToolCallStatus::Completed,
|
||||
content: vec![
|
||||
ContentBlock::Text { text: "Output line 1" },
|
||||
ContentBlock::Text { text: "Done!" },
|
||||
],
|
||||
raw_output: Some(json!({"success": true})),
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// 4b. Or tool fails with error
|
||||
SessionUpdate::ToolCallUpdate {
|
||||
tool_call_id: "call_123",
|
||||
tool_call: ToolCall {
|
||||
id: "call_123",
|
||||
status: ToolCallStatus::Error,
|
||||
content: vec![
|
||||
ContentBlock::Text { text: "Error output" },
|
||||
],
|
||||
error: Some("File not found".to_string()),
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**UI Implementation Guidelines:**
|
||||
- Track tool calls by `id` in a HashMap
|
||||
- On `ToolCall`: create new entry
|
||||
- On `ToolCallUpdate`: replace existing entry (not delta)
|
||||
- Display status with appropriate visual indicators
|
||||
- Show `content` blocks as streaming output
|
||||
- Display `error` when status is `Error`
|
||||
- Use `title` for tool call heading if available
|
||||
|
||||
## Typical Message Flow
|
||||
|
||||
Here's a complete example showing a typical agent interaction:
|
||||
|
||||
```rust
|
||||
// 1. User sends message
|
||||
Event::SessionUpdate {
|
||||
session_id: "session_123",
|
||||
update: SessionUpdate::UserMessageChunk {
|
||||
message_id: "msg_user_1",
|
||||
content: ContentBlock::Text {
|
||||
text: "Read and summarize config.toml",
|
||||
},
|
||||
_meta: None,
|
||||
},
|
||||
}
|
||||
|
||||
// 2. Agent starts thinking
|
||||
Event::SessionUpdate {
|
||||
session_id: "session_123",
|
||||
update: SessionUpdate::AgentThoughtChunk {
|
||||
message_id: "msg_agent_1",
|
||||
content: ContentBlock::Text {
|
||||
text: "I need to read the file first...",
|
||||
},
|
||||
_meta: None,
|
||||
},
|
||||
}
|
||||
|
||||
// 3. Agent initiates tool call
|
||||
Event::SessionUpdate {
|
||||
session_id: "session_123",
|
||||
update: SessionUpdate::ToolCall {
|
||||
message_id: "msg_agent_1",
|
||||
tool_call: ToolCall {
|
||||
id: "call_read_1",
|
||||
tool_name: "read_file",
|
||||
status: ToolCallStatus::Pending,
|
||||
content: vec![],
|
||||
raw_input: Some(json!({"path": "config.toml"})),
|
||||
title: Some("Read config.toml"),
|
||||
// ...
|
||||
},
|
||||
_meta: None,
|
||||
},
|
||||
}
|
||||
|
||||
// 4. Tool starts executing
|
||||
Event::SessionUpdate {
|
||||
session_id: "session_123",
|
||||
update: SessionUpdate::ToolCallUpdate {
|
||||
message_id: "msg_agent_1",
|
||||
tool_call_id: "call_read_1",
|
||||
tool_call: ToolCall {
|
||||
id: "call_read_1",
|
||||
status: ToolCallStatus::Running,
|
||||
// ...
|
||||
},
|
||||
_meta: None,
|
||||
},
|
||||
}
|
||||
|
||||
// 5. Tool completes
|
||||
Event::SessionUpdate {
|
||||
session_id: "session_123",
|
||||
update: SessionUpdate::ToolCallUpdate {
|
||||
message_id: "msg_agent_1",
|
||||
tool_call_id: "call_read_1",
|
||||
tool_call: ToolCall {
|
||||
id: "call_read_1",
|
||||
status: ToolCallStatus::Completed,
|
||||
content: vec![
|
||||
ContentBlock::Text {
|
||||
text: "[port = 3000\n...]",
|
||||
},
|
||||
],
|
||||
raw_output: Some(json!({"bytes_read": 1024})),
|
||||
// ...
|
||||
},
|
||||
_meta: None,
|
||||
},
|
||||
}
|
||||
|
||||
// 6. Agent responds with summary
|
||||
Event::SessionUpdate {
|
||||
session_id: "session_123",
|
||||
update: SessionUpdate::AgentMessageChunk {
|
||||
message_id: "msg_agent_1",
|
||||
content: ContentBlock::Text {
|
||||
text: "The config file sets the server port to 3000",
|
||||
},
|
||||
_meta: None,
|
||||
},
|
||||
}
|
||||
|
||||
// 7. More response chunks...
|
||||
Event::SessionUpdate {
|
||||
session_id: "session_123",
|
||||
update: SessionUpdate::AgentMessageChunk {
|
||||
message_id: "msg_agent_1",
|
||||
content: ContentBlock::Text {
|
||||
text: " and enables debug mode.",
|
||||
},
|
||||
_meta: None,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Content vs MessagePart
|
||||
|
||||
**Important Distinction:**
|
||||
|
||||
- **ContentBlock**: Streaming content representation (used in `SessionUpdate`)
|
||||
- Designed for real-time rendering
|
||||
- Granular updates
|
||||
- MCP-compatible structure
|
||||
|
||||
- **MessagePart**: Completed message content (legacy, still supported)
|
||||
- Used in stored/completed messages
|
||||
- May include additional fields for history
|
||||
- Compatibility with existing code
|
||||
|
||||
**Migration Path:** The protocol supports both models. New code should prefer `SessionUpdate` with `ContentBlock` for streaming, while `MessagePart` remains available for compatibility and completed message storage.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Consumers
|
||||
|
||||
1. **Track by message_id**: Group all chunks/updates for the same message
|
||||
2. **Handle tool calls separately**: Maintain a HashMap of tool calls by `tool_call_id`
|
||||
3. **Replace, don't merge**: `ToolCallUpdate` sends complete state, not deltas
|
||||
4. **Use _meta for debugging**: Provider metadata helps with troubleshooting
|
||||
5. **Distinguish thoughts from messages**: Render `AgentThoughtChunk` differently
|
||||
|
||||
### For Adapters
|
||||
|
||||
1. **Always include message_id**: Every update must reference its message
|
||||
2. **Preserve provider info in _meta**: Store original IDs for debugging
|
||||
3. **Send complete tool state**: Include all tool call fields in updates
|
||||
4. **Use appropriate chunk types**: User/Agent/Thought for correct semantics
|
||||
5. **Keep _meta minimal**: Avoid large raw payloads in production
|
||||
|
||||
### For UI Developers
|
||||
|
||||
1. **Stream incrementally**: Append chunks as they arrive
|
||||
2. **Show tool status visually**: Use icons/colors for Pending/Running/Completed/Error
|
||||
3. **Make thoughts collapsible**: Don't clutter the main conversation
|
||||
4. **Handle reconnection**: Be prepared to receive full state snapshots
|
||||
5. **Display errors prominently**: Show tool errors clearly to users
|
||||
|
||||
## Future Extensions
|
||||
|
||||
The following features are planned but not yet implemented:
|
||||
|
||||
- **ResourceBlock**: Embedded resource content (text/blob)
|
||||
- **Image/Audio blocks**: Rich media content
|
||||
- **Plan updates**: Agent planning and mode switching
|
||||
- **Permissions**: Request/reply for user permissions
|
||||
- **Stop reasons**: Detailed completion reasons
|
||||
|
||||
See the protocol roadmap for timeline and details.
|
||||
|
||||
## See Also
|
||||
|
||||
- [Migration from 0.1.x](migration_from_0.1.md) - Upgrading from older versions
|
||||
- [CHANGELOG.md](../CHANGELOG.md) - Version history and breaking changes
|
||||
- [examples/](../examples/) - Code examples demonstrating usage
|
||||
Reference in New Issue
Block a user