# 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` - 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::>() .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, } // On MessagePartAdded with Text: message.text.push_str(&content); ``` ### After: ContentBlock Streaming ```rust use std::collections::HashMap; struct MessageState { id: String, content_blocks: Vec, thoughts: Vec, tools: HashMap, // 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.