18 KiB
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:
// This no longer exists in 0.2.0
Event::MessagePartAdded {
session_id: String,
message_id: String,
part: MessagePart,
}
Replaced with:
// New in 0.2.0
Event::SessionUpdate {
session_id: String,
update: SessionUpdate,
}
Migration Patterns
Pattern 1: Basic Text Streaming
Before (0.1.x)
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)
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
SessionUpdateinstead ofMessagePartAdded - Separate
UserMessageChunkandAgentMessageChunkvariants ContentBlock::Text { text }instead ofMessagePart::Text { content }- Field renamed:
content→text
Pattern 2: Thinking/Reasoning Content
Before (0.1.x)
match event {
Event::MessagePartAdded { session_id, message_id, part } => {
match part {
MessagePart::Thinking { content, .. } => {
println!("Agent thinking: {}", content);
}
_ => {}
}
}
_ => {}
}
After (0.2.0)
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)
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)
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) andToolCallUpdate(updates) - Tool output now in
tool_call.content: Vec<ContentBlock> - Structured
ToolCalltype with multiple fields - Explicit
ToolCallStatusenum (Pending/Running/Completed/Error) - Error information in dedicated
errorfield
Pattern 4: File References
Before (0.1.x)
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)
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)
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)
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
struct MessageState {
id: String,
text: String,
thinking: String,
tools: Vec<ToolDisplay>,
}
// On MessagePartAdded with Text:
message.text.push_str(&content);
After: ContentBlock Streaming
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::SessionCreatedEvent::SessionUpdatedEvent::SessionDeletedEvent::SessionsListed
Message Lifecycle Events
Event::MessageStartedEvent::MessageCompletedEvent::MessageDeleted
Connector Events
Event::ConnectorStateChanged
Types
SessionSessionMetadata(extended with optional fields, but backward compatible)MessageMessageMetadataMessageRoleMessageStatusMessagePart(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:
// 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:
// 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.
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
#[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:
- Check the streaming_model.md documentation for detailed API information
- Review the examples/ directory for working code samples
- Examine the CHANGELOG.md for version-specific details
- 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:
- Replacing
Event::MessagePartAddedpattern matching withEvent::SessionUpdate - Using
SessionUpdatevariants instead ofMessagePartvariants - Accessing
ContentBlock::Text { text }instead ofMessagePart::Text { content } - Handling tool lifecycle with separate
ToolCallandToolCallUpdateevents - 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.