messages.rs (14616B)
1 use crate::tools::{ToolCall, ToolResponse, ToolResponses}; 2 use async_openai::types::*; 3 use md_stream::{MdElement, Partial, StreamParser}; 4 5 /// Pre-parsed markdown with source text for span resolution. 6 #[derive(Debug, Clone)] 7 pub struct ParsedMarkdown { 8 pub source: String, 9 pub elements: Vec<MdElement>, 10 } 11 12 impl ParsedMarkdown { 13 /// Parse a markdown string into elements. 14 pub fn parse(text: &str) -> Self { 15 let mut parser = StreamParser::new(); 16 parser.push(text); 17 parser.finalize(); 18 let (elements, source) = parser.into_parts(); 19 Self { source, elements } 20 } 21 } 22 use nostrdb::{Ndb, Transaction}; 23 use serde::{Deserialize, Serialize}; 24 use tokio::sync::oneshot; 25 use uuid::Uuid; 26 27 /// A question option from AskUserQuestion 28 #[derive(Debug, Clone, Deserialize)] 29 pub struct QuestionOption { 30 pub label: String, 31 pub description: String, 32 } 33 34 /// A single question from AskUserQuestion 35 #[derive(Debug, Clone, Deserialize)] 36 #[serde(rename_all = "camelCase")] 37 pub struct UserQuestion { 38 pub question: String, 39 pub header: String, 40 #[serde(default)] 41 pub multi_select: bool, 42 pub options: Vec<QuestionOption>, 43 } 44 45 /// Parsed AskUserQuestion tool input 46 #[derive(Debug, Clone, Deserialize)] 47 pub struct AskUserQuestionInput { 48 pub questions: Vec<UserQuestion>, 49 } 50 51 /// User's answer to a question 52 #[derive(Debug, Clone, Default, Serialize)] 53 pub struct QuestionAnswer { 54 /// Selected option indices 55 pub selected: Vec<usize>, 56 /// Custom "Other" text if provided 57 pub other_text: Option<String>, 58 } 59 60 /// A request for user permission to use a tool (displayable data only) 61 #[derive(Debug, Clone)] 62 pub struct PermissionRequest { 63 /// Unique identifier for this permission request 64 pub id: Uuid, 65 /// The tool that wants to be used 66 pub tool_name: String, 67 /// The arguments the tool will be called with 68 pub tool_input: serde_json::Value, 69 /// The user's response (None if still pending) 70 pub response: Option<PermissionResponseType>, 71 /// For AskUserQuestion: pre-computed summary of answers for display 72 pub answer_summary: Option<AnswerSummary>, 73 /// For ExitPlanMode: pre-parsed markdown with source text for span resolution 74 pub cached_plan: Option<ParsedMarkdown>, 75 } 76 77 /// A single entry in an answer summary 78 #[derive(Debug, Clone)] 79 pub struct AnswerSummaryEntry { 80 /// The question header (e.g., "Library", "Approach") 81 pub header: String, 82 /// The selected answer text, comma-separated if multiple 83 pub answer: String, 84 } 85 86 /// Pre-computed summary of an AskUserQuestion response for display 87 #[derive(Debug, Clone)] 88 pub struct AnswerSummary { 89 pub entries: Vec<AnswerSummaryEntry>, 90 } 91 92 /// A permission request with the response channel (for channel communication) 93 pub struct PendingPermission { 94 /// The displayable request data 95 pub request: PermissionRequest, 96 /// Channel to send the user's response back 97 pub response_tx: oneshot::Sender<PermissionResponse>, 98 } 99 100 /// The user's response to a permission request 101 #[derive(Debug, Clone)] 102 pub enum PermissionResponse { 103 /// Allow the tool to execute, with an optional message for the AI 104 Allow { message: Option<String> }, 105 /// Deny the tool execution with a reason 106 Deny { reason: String }, 107 } 108 109 /// The recorded response type for display purposes (without channel details) 110 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 111 pub enum PermissionResponseType { 112 Allowed, 113 Denied, 114 } 115 116 /// Metadata about a completed tool execution from an agentic backend. 117 /// Used as a variant in `ToolResponses` to unify with other tool responses. 118 #[derive(Debug, Clone, Serialize, Deserialize)] 119 pub struct ExecutedTool { 120 pub tool_name: String, 121 pub summary: String, // e.g., "154 lines", "exit 0", "3 matches" 122 /// Which subagent (Task tool_use_id) produced this result, if any 123 pub parent_task_id: Option<String>, 124 /// Pre-computed file update for diff rendering (not serialized) 125 #[serde(skip)] 126 pub file_update: Option<crate::file_update::FileUpdate>, 127 } 128 129 /// Session initialization info from Claude Code CLI 130 #[derive(Debug, Clone, Default)] 131 pub struct SessionInfo { 132 /// Available tools in this session 133 pub tools: Vec<String>, 134 /// Model being used (e.g., "claude-opus-4-5-20251101") 135 pub model: Option<String>, 136 /// Permission mode (e.g., "default", "plan") 137 pub permission_mode: Option<String>, 138 /// Available slash commands 139 pub slash_commands: Vec<String>, 140 /// Available agent types for Task tool 141 pub agents: Vec<String>, 142 /// Claude Code CLI version 143 pub cli_version: Option<String>, 144 /// Current working directory 145 pub cwd: Option<String>, 146 /// Session ID from Claude Code 147 pub claude_session_id: Option<String>, 148 } 149 150 /// Status of a subagent spawned by the Task tool 151 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 152 pub enum SubagentStatus { 153 /// Subagent is running 154 Running, 155 /// Subagent completed successfully 156 Completed, 157 /// Subagent failed with an error 158 Failed, 159 } 160 161 /// Information about a subagent spawned by the Task tool 162 #[derive(Debug, Clone)] 163 pub struct SubagentInfo { 164 /// Unique ID for this subagent task 165 pub task_id: String, 166 /// Description of what the subagent is doing 167 pub description: String, 168 /// Type of subagent (e.g., "Explore", "Plan", "Bash") 169 pub subagent_type: String, 170 /// Current status 171 pub status: SubagentStatus, 172 /// Output content (truncated for display) 173 pub output: String, 174 /// Maximum output size to keep (for size-restricted window) 175 pub max_output_size: usize, 176 /// Tool results produced by this subagent 177 pub tool_results: Vec<ExecutedTool>, 178 } 179 180 /// An assistant message with incremental markdown parsing support. 181 /// 182 /// During streaming, tokens are pushed to the parser incrementally. 183 /// After finalization (stream end), parsed elements are cached. 184 pub struct AssistantMessage { 185 /// Raw accumulated text (kept for API serialization) 186 text: String, 187 /// Incremental parser for this message (None after finalization) 188 parser: Option<StreamParser>, 189 /// Cached parsed elements (populated after finalization) 190 cached_elements: Option<Vec<MdElement>>, 191 } 192 193 impl std::fmt::Debug for AssistantMessage { 194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 195 f.debug_struct("AssistantMessage") 196 .field("text", &self.text) 197 .field("is_streaming", &self.parser.is_some()) 198 .field( 199 "cached_elements", 200 &self.cached_elements.as_ref().map(|e| e.len()), 201 ) 202 .finish() 203 } 204 } 205 206 impl Clone for AssistantMessage { 207 fn clone(&self) -> Self { 208 // StreamParser doesn't implement Clone, so we need special handling. 209 // For cloned messages (which are typically finalized), we just clone 210 // the text and cached elements. If there's an active parser, we 211 // re-parse from the raw text. 212 if let Some(cached) = &self.cached_elements { 213 Self { 214 text: self.text.clone(), 215 parser: None, 216 cached_elements: Some(cached.clone()), 217 } 218 } else { 219 // Active streaming - re-parse from text 220 let mut parser = StreamParser::new(); 221 parser.push(&self.text); 222 Self { 223 text: self.text.clone(), 224 parser: Some(parser), 225 cached_elements: None, 226 } 227 } 228 } 229 } 230 231 impl AssistantMessage { 232 /// Create a new assistant message with a fresh parser. 233 pub fn new() -> Self { 234 Self { 235 text: String::new(), 236 parser: Some(StreamParser::new()), 237 cached_elements: None, 238 } 239 } 240 241 /// Create from existing text (e.g., when loading from storage). 242 pub fn from_text(text: String) -> Self { 243 let mut parser = StreamParser::new(); 244 parser.push(&text); 245 parser.finalize(); 246 let cached = parser.parsed().to_vec(); 247 Self { 248 text, 249 parser: None, 250 cached_elements: Some(cached), 251 } 252 } 253 254 /// Push a new token and update the parser. 255 pub fn push_token(&mut self, token: &str) { 256 self.text.push_str(token); 257 if let Some(parser) = &mut self.parser { 258 parser.push(token); 259 } 260 } 261 262 /// Finalize the message (call when stream ends). 263 /// This caches the parsed elements and drops the parser. 264 pub fn finalize(&mut self) { 265 if let Some(mut parser) = self.parser.take() { 266 parser.finalize(); 267 self.cached_elements = Some(parser.parsed().to_vec()); 268 } 269 } 270 271 /// Get the raw text content. 272 pub fn text(&self) -> &str { 273 &self.text 274 } 275 276 /// Get the buffer for resolving spans in parsed elements. 277 /// This is the same as text() — both the parser and AssistantMessage 278 /// maintain identical buffers via push_str(token). 279 pub fn buffer(&self) -> &str { 280 &self.text 281 } 282 283 /// Get parsed markdown elements. 284 pub fn parsed_elements(&self) -> &[MdElement] { 285 if let Some(cached) = &self.cached_elements { 286 cached 287 } else if let Some(parser) = &self.parser { 288 parser.parsed() 289 } else { 290 &[] 291 } 292 } 293 294 /// Get the current partial (in-progress) element, if any. 295 pub fn partial(&self) -> Option<&Partial> { 296 self.parser.as_ref().and_then(|p| p.partial()) 297 } 298 299 /// Check if the message is still being streamed. 300 pub fn is_streaming(&self) -> bool { 301 self.parser.is_some() 302 } 303 } 304 305 impl Default for AssistantMessage { 306 fn default() -> Self { 307 Self::new() 308 } 309 } 310 311 #[derive(Debug, Clone)] 312 pub enum Message { 313 System(String), 314 Error(String), 315 User(String), 316 Assistant(AssistantMessage), 317 ToolCalls(Vec<ToolCall>), 318 ToolResponse(ToolResponse), 319 /// A permission request from the AI that needs user response 320 PermissionRequest(PermissionRequest), 321 /// Conversation was compacted 322 CompactionComplete(CompactionInfo), 323 /// A subagent spawned by Task tool 324 Subagent(SubagentInfo), 325 } 326 327 /// Compaction info from compact_boundary system message 328 #[derive(Debug, Clone)] 329 pub struct CompactionInfo { 330 /// Number of tokens before compaction 331 pub pre_tokens: u64, 332 } 333 334 /// Usage metrics from a completed query's Result message 335 #[derive(Debug, Clone, Default)] 336 pub struct UsageInfo { 337 pub input_tokens: u64, 338 pub output_tokens: u64, 339 pub cost_usd: Option<f64>, 340 pub num_turns: u32, 341 } 342 343 impl UsageInfo { 344 /// Context window fill: only input tokens consume context space. 345 /// Output tokens are generated from the context, not part of it. 346 pub fn context_tokens(&self) -> u64 { 347 self.input_tokens 348 } 349 } 350 351 /// Get context window size for a model name. 352 /// All current Claude models have 200K context. 353 pub fn context_window_for_model(_model: Option<&str>) -> u64 { 354 200_000 355 } 356 357 /// The ai backends response. Since we are using streaming APIs these are 358 /// represented as individual tokens or tool calls 359 pub enum DaveApiResponse { 360 ToolCalls(Vec<ToolCall>), 361 Token(String), 362 Failed(String), 363 /// A permission request that needs to be displayed to the user 364 PermissionRequest(PendingPermission), 365 /// Metadata from a completed tool execution 366 ToolResult(ExecutedTool), 367 /// Session initialization info from Claude Code CLI 368 SessionInfo(SessionInfo), 369 /// Subagent spawned by Task tool 370 SubagentSpawned(SubagentInfo), 371 /// Subagent output update 372 SubagentOutput { 373 task_id: String, 374 output: String, 375 }, 376 /// Subagent completed 377 SubagentCompleted { 378 task_id: String, 379 result: String, 380 }, 381 /// Conversation compaction started 382 CompactionStarted, 383 /// Conversation compaction completed with token info 384 CompactionComplete(CompactionInfo), 385 /// Query completed with usage metrics 386 QueryComplete(UsageInfo), 387 } 388 389 impl Message { 390 pub fn tool_error(id: String, msg: String) -> Self { 391 Self::ToolResponse(ToolResponse::error(id, msg)) 392 } 393 394 pub fn to_api_msg(&self, txn: &Transaction, ndb: &Ndb) -> Option<ChatCompletionRequestMessage> { 395 match self { 396 Message::Error(_err) => None, 397 398 Message::User(msg) => Some(ChatCompletionRequestMessage::User( 399 ChatCompletionRequestUserMessage { 400 name: None, 401 content: ChatCompletionRequestUserMessageContent::Text(msg.clone()), 402 }, 403 )), 404 405 Message::Assistant(msg) => Some(ChatCompletionRequestMessage::Assistant( 406 ChatCompletionRequestAssistantMessage { 407 content: Some(ChatCompletionRequestAssistantMessageContent::Text( 408 msg.text().to_string(), 409 )), 410 ..Default::default() 411 }, 412 )), 413 414 Message::System(msg) => Some(ChatCompletionRequestMessage::System( 415 ChatCompletionRequestSystemMessage { 416 content: ChatCompletionRequestSystemMessageContent::Text(msg.clone()), 417 ..Default::default() 418 }, 419 )), 420 421 Message::ToolCalls(calls) => Some(ChatCompletionRequestMessage::Assistant( 422 ChatCompletionRequestAssistantMessage { 423 tool_calls: Some(calls.iter().map(|c| c.to_api()).collect()), 424 ..Default::default() 425 }, 426 )), 427 428 Message::ToolResponse(resp) => { 429 // ExecutedTool results are UI-only, not sent to the API 430 if matches!(resp.responses(), ToolResponses::ExecutedTool(_)) { 431 return None; 432 } 433 434 let tool_response = resp.responses().format_for_dave(txn, ndb); 435 436 Some(ChatCompletionRequestMessage::Tool( 437 ChatCompletionRequestToolMessage { 438 tool_call_id: resp.id().to_owned(), 439 content: ChatCompletionRequestToolMessageContent::Text(tool_response), 440 }, 441 )) 442 } 443 444 // Permission requests are UI-only, not sent to the API 445 Message::PermissionRequest(_) => None, 446 447 // Compaction complete is UI-only, not sent to the API 448 Message::CompactionComplete(_) => None, 449 450 // Subagent info is UI-only, not sent to the API 451 Message::Subagent(_) => None, 452 } 453 } 454 }