sync from monorepo @ 2452e92e

This commit is contained in:
2026-05-08 01:59:04 +02:00
commit b03dc15371
459 changed files with 129586 additions and 0 deletions
@@ -0,0 +1,620 @@
# Migration Guide: Dirigent Protocol 0.1.x → 0.2.0
## Overview
Version 0.2.0 introduces a **new ACP-style streaming model** while maintaining backward compatibility with most existing code. This guide will help you migrate to the new `SessionUpdate` event system.
## Breaking Changes Summary
### Removed: Event::MessagePartAdded
The `MessagePartAdded` event variant has been **removed** from the `Event` enum. This was the primary breaking change in 0.2.0.
**What was removed:**
```rust
// This no longer exists in 0.2.0
Event::MessagePartAdded {
session_id: String,
message_id: String,
part: MessagePart,
}
```
**Replaced with:**
```rust
// New in 0.2.0
Event::SessionUpdate {
session_id: String,
update: SessionUpdate,
}
```
## Migration Patterns
### Pattern 1: Basic Text Streaming
#### Before (0.1.x)
```rust
use dirigent_protocol::{Event, MessagePart};
match event {
Event::MessagePartAdded { session_id, message_id, part } => {
match part {
MessagePart::Text { content, .. } => {
println!("Text from {}: {}", message_id, content);
}
_ => {}
}
}
_ => {}
}
```
#### After (0.2.0)
```rust
use dirigent_protocol::{Event, SessionUpdate, ContentBlock};
match event {
Event::SessionUpdate { session_id, update } => {
match update {
SessionUpdate::UserMessageChunk { message_id, content, .. } |
SessionUpdate::AgentMessageChunk { message_id, content, .. } => {
match content {
ContentBlock::Text { text } => {
println!("Text from {}: {}", message_id, text);
}
_ => {}
}
}
_ => {}
}
}
_ => {}
}
```
**Key Changes:**
- Use `SessionUpdate` instead of `MessagePartAdded`
- Separate `UserMessageChunk` and `AgentMessageChunk` variants
- `ContentBlock::Text { text }` instead of `MessagePart::Text { content }`
- Field renamed: `content``text`
### Pattern 2: Thinking/Reasoning Content
#### Before (0.1.x)
```rust
match event {
Event::MessagePartAdded { session_id, message_id, part } => {
match part {
MessagePart::Thinking { content, .. } => {
println!("Agent thinking: {}", content);
}
_ => {}
}
}
_ => {}
}
```
#### After (0.2.0)
```rust
match event {
Event::SessionUpdate { session_id, update } => {
match update {
SessionUpdate::AgentThoughtChunk { message_id, content, .. } => {
match content {
ContentBlock::Text { text } => {
println!("Agent thinking: {}", text);
}
_ => {}
}
}
_ => {}
}
}
_ => {}
}
```
**Key Changes:**
- `MessagePart::Thinking``SessionUpdate::AgentThoughtChunk`
- Content wrapped in `ContentBlock::Text`
### Pattern 3: Tool Calls
#### Before (0.1.x)
```rust
match event {
Event::MessagePartAdded { session_id, message_id, part } => {
match part {
MessagePart::Tool {
tool_name,
tool_call_id,
status,
input,
output,
..
} => {
println!("Tool {}: {:?}", tool_name, status);
if let Some(out) = output {
println!("Output: {}", out);
}
}
_ => {}
}
}
_ => {}
}
```
#### After (0.2.0)
```rust
use dirigent_protocol::ToolCallStatus;
match event {
Event::SessionUpdate { session_id, update } => {
match update {
// Initial tool call
SessionUpdate::ToolCall { message_id, tool_call, .. } => {
println!("Tool {}: {:?}", tool_call.tool_name, tool_call.status);
}
// Tool call updates (progress, completion, errors)
SessionUpdate::ToolCallUpdate { message_id, tool_call_id, tool_call, .. } => {
println!("Tool {} updated: {:?}", tool_call.tool_name, tool_call.status);
// Output is now in content blocks
for content in &tool_call.content {
match content {
ContentBlock::Text { text } => {
println!("Output: {}", text);
}
_ => {}
}
}
// Check for errors
if tool_call.status == ToolCallStatus::Error {
if let Some(error) = &tool_call.error {
println!("Error: {}", error);
}
}
}
_ => {}
}
}
_ => {}
}
```
**Key Changes:**
- Tool lifecycle split into `ToolCall` (initial) and `ToolCallUpdate` (updates)
- Tool output now in `tool_call.content: Vec<ContentBlock>`
- Structured `ToolCall` type with multiple fields
- Explicit `ToolCallStatus` enum (Pending/Running/Completed/Error)
- Error information in dedicated `error` field
### Pattern 4: File References
#### Before (0.1.x)
```rust
match event {
Event::MessagePartAdded { session_id, message_id, part } => {
match part {
MessagePart::File { path, name, mime_type, .. } => {
println!("File reference: {} ({})", name.unwrap_or_default(), path);
}
_ => {}
}
}
_ => {}
}
```
#### After (0.2.0)
```rust
match event {
Event::SessionUpdate { session_id, update } => {
match update {
SessionUpdate::AgentMessageChunk { message_id, content, .. } => {
match content {
ContentBlock::ResourceLink { uri, name, mime_type } => {
println!("Resource: {} ({})",
name.as_deref().unwrap_or("unnamed"),
uri
);
}
_ => {}
}
}
_ => {}
}
}
_ => {}
}
```
**Key Changes:**
- `MessagePart::File``ContentBlock::ResourceLink`
- Field renamed: `path``uri` (more generic)
- Can appear in any message chunk type
## Complete Migration Example
Here's a complete before/after example showing a typical event handler:
### Before (0.1.x)
```rust
use dirigent_protocol::{Event, MessagePart};
fn handle_event(event: Event) {
match event {
Event::MessagePartAdded { session_id, message_id, part } => {
match part {
MessagePart::Text { content, .. } => {
append_text(&message_id, &content);
}
MessagePart::Thinking { content, .. } => {
show_thinking(&message_id, &content);
}
MessagePart::Tool { tool_name, tool_call_id, status, output, .. } => {
update_tool_display(&tool_call_id, &tool_name, status, output.as_deref());
}
MessagePart::File { path, name, .. } => {
add_file_reference(&message_id, &path, name.as_deref());
}
_ => {}
}
}
Event::MessageStarted { session_id, message_id, .. } => {
create_message_container(&message_id);
}
Event::MessageCompleted { session_id, message_id, .. } => {
finalize_message(&message_id);
}
_ => {}
}
}
```
### After (0.2.0)
```rust
use dirigent_protocol::{Event, SessionUpdate, ContentBlock, ToolCallStatus};
fn handle_event(event: Event) {
match event {
Event::SessionUpdate { session_id, update } => {
match update {
SessionUpdate::UserMessageChunk { message_id, content, .. } |
SessionUpdate::AgentMessageChunk { message_id, content, .. } => {
match content {
ContentBlock::Text { text } => {
append_text(&message_id, &text);
}
ContentBlock::ResourceLink { uri, name, .. } => {
add_file_reference(&message_id, &uri, name.as_deref());
}
}
}
SessionUpdate::AgentThoughtChunk { message_id, content, .. } => {
if let ContentBlock::Text { text } = content {
show_thinking(&message_id, &text);
}
}
SessionUpdate::ToolCall { message_id, tool_call, .. } => {
create_tool_display(
&tool_call.id,
&tool_call.tool_name,
tool_call.status,
);
}
SessionUpdate::ToolCallUpdate { tool_call_id, tool_call, .. } => {
// Extract output text from content blocks
let output_text = tool_call.content.iter()
.filter_map(|c| match c {
ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
update_tool_display(
&tool_call_id,
&tool_call.tool_name,
tool_call.status,
if output_text.is_empty() { None } else { Some(&output_text) },
);
// Handle errors
if tool_call.status == ToolCallStatus::Error {
if let Some(error) = &tool_call.error {
show_tool_error(&tool_call_id, error);
}
}
}
}
}
// These events remain unchanged
Event::MessageStarted { session_id, message_id, .. } => {
create_message_container(&message_id);
}
Event::MessageCompleted { session_id, message_id, .. } => {
finalize_message(&message_id);
}
_ => {}
}
}
```
## UI State Management Changes
### Before: Simple Append Model
```rust
struct MessageState {
id: String,
text: String,
thinking: String,
tools: Vec<ToolDisplay>,
}
// On MessagePartAdded with Text:
message.text.push_str(&content);
```
### After: ContentBlock Streaming
```rust
use std::collections::HashMap;
struct MessageState {
id: String,
content_blocks: Vec<ContentBlock>,
thoughts: Vec<ContentBlock>,
tools: HashMap<String, ToolCall>, // Keyed by tool_call_id
}
// On AgentMessageChunk:
message.content_blocks.push(content);
// On AgentThoughtChunk:
message.thoughts.push(content);
// On ToolCall:
message.tools.insert(tool_call.id.clone(), tool_call);
// On ToolCallUpdate:
message.tools.insert(tool_call_id.clone(), tool_call); // Replace, not merge
```
**Key Insight:** ToolCallUpdate sends the **complete tool state**, not a delta. Always replace the existing tool call entry.
## What Stays the Same
The following events and types are **unchanged** and require no migration:
### Session Events
- `Event::SessionCreated`
- `Event::SessionUpdated`
- `Event::SessionDeleted`
- `Event::SessionsListed`
### Message Lifecycle Events
- `Event::MessageStarted`
- `Event::MessageCompleted`
- `Event::MessageDeleted`
### Connector Events
- `Event::ConnectorStateChanged`
### Types
- `Session`
- `SessionMetadata` (extended with optional fields, but backward compatible)
- `Message`
- `MessageMetadata`
- `MessageRole`
- `MessageStatus`
- `MessagePart` (still used for completed messages)
## Common Migration Issues
### Issue 1: Pattern Matching Exhaustiveness
**Problem:** Compiler errors about non-exhaustive patterns after removing `MessagePartAdded`.
**Solution:** Remove any match arms for `MessagePartAdded` and add `SessionUpdate` handling.
### Issue 2: Field Name Mismatches
**Problem:** `MessagePart::Text { content }` vs `ContentBlock::Text { text }`
**Solution:** Update field access from `content` to `text`.
### Issue 3: Tool Call State Management
**Problem:** Treating `ToolCallUpdate` as a delta instead of complete state.
**Solution:** Replace the entire tool call entry when receiving `ToolCallUpdate`, don't try to merge.
**Incorrect:**
```rust
// DON'T do this
if let Some(existing) = tools.get_mut(&tool_call_id) {
existing.content.extend(tool_call.content); // Wrong!
existing.status = tool_call.status;
}
```
**Correct:**
```rust
// DO this
tools.insert(tool_call_id.clone(), tool_call); // Replace completely
```
### Issue 4: Missing _meta Fields
**Problem:** Trying to access `_meta` that might be `None`.
**Solution:** Always use `Option` handling or provide defaults.
```rust
match update {
SessionUpdate::AgentMessageChunk { _meta, .. } => {
if let Some(meta) = _meta {
if let Some(provider) = &meta.provider {
println!("Provider: {}", provider.name);
}
}
}
_ => {}
}
```
## Testing Your Migration
### Test Checklist
- [ ] Text streaming displays correctly
- [ ] Agent thoughts appear in designated section
- [ ] User messages are distinguished from agent messages
- [ ] Tool calls show initial pending state
- [ ] Tool calls update with running status
- [ ] Tool calls complete successfully
- [ ] Tool errors display with error messages
- [ ] File references render as links
- [ ] Multiple content chunks accumulate properly
- [ ] Tool call state replaces (not merges) on update
### Migration Test Example
```rust
#[test]
fn test_migration_text_streaming() {
let event = Event::SessionUpdate {
session_id: "test_session".to_string(),
update: SessionUpdate::AgentMessageChunk {
message_id: "msg_1".to_string(),
content: ContentBlock::Text {
text: "Hello".to_string(),
},
_meta: None,
},
};
// Your handler should process this correctly
handle_event(event);
// Assert expected state changes
assert_eq!(get_message_text("msg_1"), "Hello");
}
#[test]
fn test_migration_tool_lifecycle() {
use dirigent_protocol::ToolCallStatus;
// 1. Initial tool call
handle_event(Event::SessionUpdate {
session_id: "test".to_string(),
update: SessionUpdate::ToolCall {
message_id: "msg_1".to_string(),
tool_call: ToolCall {
id: "call_1".to_string(),
tool_name: "bash".to_string(),
status: ToolCallStatus::Pending,
content: vec![],
raw_input: None,
raw_output: None,
title: None,
error: None,
metadata: None,
},
_meta: None,
},
});
assert_eq!(get_tool_status("call_1"), ToolCallStatus::Pending);
// 2. Tool starts running
handle_event(Event::SessionUpdate {
session_id: "test".to_string(),
update: SessionUpdate::ToolCallUpdate {
message_id: "msg_1".to_string(),
tool_call_id: "call_1".to_string(),
tool_call: ToolCall {
id: "call_1".to_string(),
tool_name: "bash".to_string(),
status: ToolCallStatus::Running,
content: vec![],
raw_input: None,
raw_output: None,
title: None,
error: None,
metadata: None,
},
_meta: None,
},
});
assert_eq!(get_tool_status("call_1"), ToolCallStatus::Running);
// 3. Tool completes with output
handle_event(Event::SessionUpdate {
session_id: "test".to_string(),
update: SessionUpdate::ToolCallUpdate {
message_id: "msg_1".to_string(),
tool_call_id: "call_1".to_string(),
tool_call: ToolCall {
id: "call_1".to_string(),
tool_name: "bash".to_string(),
status: ToolCallStatus::Completed,
content: vec![
ContentBlock::Text {
text: "Done!".to_string(),
},
],
raw_input: None,
raw_output: Some(serde_json::json!({"exit_code": 0})),
title: None,
error: None,
metadata: None,
},
_meta: None,
},
});
assert_eq!(get_tool_status("call_1"), ToolCallStatus::Completed);
assert_eq!(get_tool_output("call_1"), "Done!");
}
```
## Getting Help
If you encounter issues during migration:
1. Check the [streaming_model.md](streaming_model.md) documentation for detailed API information
2. Review the [examples/](../examples/) directory for working code samples
3. Examine the [CHANGELOG.md](../CHANGELOG.md) for version-specific details
4. Look at the protocol source code tests for reference implementations
## Quick Reference: Event Type Mapping
| 0.1.x Event | 0.2.0 Event | Notes |
|-------------|-------------|-------|
| `MessagePartAdded` with `Text` | `SessionUpdate::AgentMessageChunk` or `UserMessageChunk` | Split by role |
| `MessagePartAdded` with `Thinking` | `SessionUpdate::AgentThoughtChunk` | Dedicated type |
| `MessagePartAdded` with `Tool` | `SessionUpdate::ToolCall` + `ToolCallUpdate` | Split lifecycle |
| `MessagePartAdded` with `File` | `ContentBlock::ResourceLink` in chunks | Different structure |
| All other events | Unchanged | No migration needed |
## Summary
The 0.2.0 migration primarily involves:
1. Replacing `Event::MessagePartAdded` pattern matching with `Event::SessionUpdate`
2. Using `SessionUpdate` variants instead of `MessagePart` variants
3. Accessing `ContentBlock::Text { text }` instead of `MessagePart::Text { content }`
4. Handling tool lifecycle with separate `ToolCall` and `ToolCallUpdate` events
5. Managing tool state as complete snapshots, not deltas
The new model provides better structure, clearer semantics, and improved alignment with ACP standards while maintaining most of your existing code.
@@ -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