Files
dirigent/crates/dirigent_protocol/docs/migration_from_0.1.md
T
2026-05-08 01:59:04 +02:00

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 SessionUpdate instead of MessagePartAdded
  • Separate UserMessageChunk and AgentMessageChunk variants
  • ContentBlock::Text { text } instead of MessagePart::Text { content }
  • Field renamed: contenttext

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::ThinkingSessionUpdate::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) 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)

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::FileContentBlock::ResourceLink
  • Field renamed: pathuri (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::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:

// 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:

  1. Check the streaming_model.md documentation for detailed API information
  2. Review the examples/ directory for working code samples
  3. Examine the 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.