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.