# 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 }, AgentMessageChunk { message_id: String, content: ContentBlock, _meta: Option }, AgentThoughtChunk { message_id: String, content: ContentBlock, _meta: Option }, ToolCall { message_id: String, tool_call: ToolCall, _meta: Option }, ToolCallUpdate { message_id: String, tool_call_id: String, tool_call: ToolCall, _meta: Option }, } ``` ### ContentBlock Types Content is represented using structured `ContentBlock` variants: ```rust pub enum ContentBlock { Text { text: String }, ResourceLink { uri: String, name: Option, mime_type: Option, }, // 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, pub extra: HashMap, // Arbitrary additional fields } pub struct ProviderMeta { pub name: String, // e.g., "opencode", "anthropic" pub original_ids: Option>, // Original provider IDs pub raw_excerpt: Option, // 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