1use std::collections::{HashMap, HashSet};
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(tag = "type")]
12pub enum Message {
13 User(UserMessage),
14 Assistant(AssistantMessage),
15 Progress(ProgressMessage),
16 Attachment(AttachmentMessage),
17 System(SystemMessage),
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct UserMessage {
23 pub message: MessageContent,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub is_meta: Option<bool>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub is_visible_in_transcript_only: Option<bool>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub is_virtual: Option<bool>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub is_compact_summary: Option<bool>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub summarize_metadata: Option<SummarizeMetadata>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub tool_use_result: Option<serde_json::Value>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub mcp_meta: Option<serde_json::Value>,
38 pub uuid: String,
39 pub timestamp: String,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub image_paste_ids: Option<Vec<u32>>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub source_tool_assistant_uuid: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub permission_mode: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub origin: Option<MessageOrigin>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct SummarizeMetadata {
53 pub messages_summarized: u32,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub user_context: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub direction: Option<String>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct MessageOrigin {
63 #[serde(rename = "type")]
64 pub origin_type: String,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(untagged)]
70pub enum MessageContent {
71 String(String),
72 Blocks(Vec<ContentBlock>),
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(tag = "type", rename_all = "snake_case")]
78pub enum ContentBlock {
79 Text {
80 text: String,
81 },
82 Image {
83 source: ImageSource,
84 },
85 ToolUse {
86 id: String,
87 name: String,
88 input: serde_json::Value,
89 },
90 ToolResult {
91 tool_use_id: String,
92 content: Option<Vec<ContentBlock>>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 is_error: Option<bool>,
95 },
96 #[serde(rename = "server_tool_use")]
98 ServerToolUse {
99 id: String,
100 name: String,
101 input: serde_json::Value,
102 },
103 #[serde(rename = "mcp_tool_use")]
104 McpToolUse {
105 id: String,
106 name: String,
107 input: serde_json::Value,
108 },
109 #[serde(rename = "advisor_tool_result")]
110 AdvisorToolResult {
111 tool_use_id: String,
112 content: serde_json::Value,
113 },
114 #[serde(rename = "web_search_tool_result")]
115 WebSearchToolResult {
116 tool_use_id: String,
117 content: serde_json::Value,
118 },
119 #[serde(rename = "web_fetch_tool_result")]
120 WebFetchToolResult {
121 tool_use_id: String,
122 content: serde_json::Value,
123 },
124 #[serde(rename = "tool_reference")]
125 ToolReference {
126 tool_name: String,
127 },
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ImageSource {
133 #[serde(rename = "type")]
134 pub source_type: String,
135 pub media_type: String,
136 pub data: String,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct AssistantMessage {
142 pub message: AssistantMessageContent,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub request_id: Option<String>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub api_error: Option<serde_json::Value>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub error: Option<serde_json::Value>,
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub error_details: Option<String>,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub is_api_error_message: Option<bool>,
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub is_virtual: Option<bool>,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub is_meta: Option<bool>,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub advisor_model: Option<String>,
159 pub uuid: String,
160 pub timestamp: String,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub parent_uuid: Option<String>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct AssistantMessageContent {
168 pub id: String,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub container: Option<String>,
171 pub model: String,
172 pub role: String,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub stop_reason: Option<String>,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 pub stop_sequence: Option<String>,
177 #[serde(rename = "type")]
178 pub message_type: String,
179 pub usage: Option<Usage>,
180 pub content: Vec<serde_json::Value>,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub context_management: Option<serde_json::Value>,
183}
184
185#[derive(Debug, Clone, Default, Serialize, Deserialize)]
187pub struct Usage {
188 #[serde(rename = "input_tokens")]
189 pub input_tokens: u32,
190 #[serde(rename = "output_tokens")]
191 pub output_tokens: u32,
192 #[serde(rename = "cache_creation_input_tokens")]
193 pub cache_creation_input_tokens: u32,
194 #[serde(rename = "cache_read_input_tokens")]
195 pub cache_read_input_tokens: u32,
196 #[serde(rename = "server_tool_use")]
197 pub server_tool_use: ServerToolUse,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub service_tier: Option<String>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub cache_creation: Option<CacheCreation>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub inference_geo: Option<String>,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub iterations: Option<u32>,
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub speed: Option<f64>,
208}
209
210#[derive(Debug, Clone, Default, Serialize, Deserialize)]
212pub struct ServerToolUse {
213 #[serde(rename = "web_search_requests")]
214 pub web_search_requests: u32,
215 #[serde(rename = "web_fetch_requests")]
216 pub web_fetch_requests: u32,
217}
218
219#[derive(Debug, Clone, Default, Serialize, Deserialize)]
221pub struct CacheCreation {
222 #[serde(rename = "ephemeral_1h_input_tokens")]
223 pub ephemeral_1h_input_tokens: u32,
224 #[serde(rename = "ephemeral_5m_input_tokens")]
225 pub ephemeral_5m_input_tokens: u32,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct ProgressMessage<T = serde_json::Value> {
231 #[serde(rename = "type")]
232 pub data_type: String,
233 pub data: T,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub tool_use_id: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub parent_tool_use_id: Option<String>,
238 pub uuid: String,
239 pub timestamp: String,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub parent_uuid: Option<String>,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct AttachmentMessage {
247 pub attachment: serde_json::Value,
248 pub uuid: String,
249 pub timestamp: String,
250 #[serde(skip_serializing_if = "Option::is_none")]
251 pub parent_uuid: Option<String>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct SystemMessage {
257 pub message: SystemMessageContent,
258 pub uuid: String,
259 pub timestamp: String,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub parent_uuid: Option<String>,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct SystemMessageContent {
267 #[serde(rename = "type")]
268 pub message_type: String,
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub subtype: Option<String>,
271 pub content: String,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub level: Option<SystemMessageLevel>,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278#[serde(rename_all = "lowercase")]
279pub enum SystemMessageLevel {
280 Info,
281 Warning,
282 Error,
283}
284
285pub const INTERRUPT_MESSAGE: &str = "[Request interrupted by user]";
288pub const INTERRUPT_MESSAGE_FOR_TOOL_USE: &str = "[Request interrupted by user for tool use]";
289pub const CANCEL_MESSAGE: &str = "The user doesn't want to take this action right now. STOP what you are doing and wait for the user to tell you how to proceed.";
290pub const REJECT_MESSAGE: &str = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
291pub const REJECT_MESSAGE_WITH_REASON_PREFIX: &str = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). To tell you how to proceed, the user said:\n";
292pub const SUBAGENT_REJECT_MESSAGE: &str = "Permission for this tool use was denied. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). Try a different approach or report the limitation to complete your task.";
293pub const SUBAGENT_REJECT_MESSAGE_WITH_REASON_PREFIX: &str = "Permission for this tool use was denied. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). The user said:\n";
294pub const NO_RESPONSE_REQUESTED: &str = "No response requested.";
295pub const SYNTHETIC_MODEL: &str = "<synthetic>";
296
297pub const DENIAL_WORKAROUND_GUIDANCE: &str = "IMPORTANT: You *may* attempt to accomplish this action using other tools that might naturally be used to accomplish this goal, e.g. using head instead of cat. But you *should not* attempt to work around this denial in malicious ways, e.g. do not use your ability to run tests to execute non-test actions. You should only try to work around this restriction in reasonable ways that do not attempt to bypass the intent behind this denial. If you believe this capability is essential to complete your request, STOP and explain to the user what you were trying to do and why you need this permission. Let the user decide how to proceed.";
299
300const AUTO_MODE_REJECTION_PREFIX: &str = "Permission for this action has been denied. Reason: ";
301
302pub fn build_yolo_rejection_message(reason: &str) -> String {
306 format!(
307 "{}{}. If you have other tasks that don't depend on this action, continue working on those. {} To allow this type of action in the future, the user can add a Bash permission rule to their settings.",
308 AUTO_MODE_REJECTION_PREFIX, reason, DENIAL_WORKAROUND_GUIDANCE
309 )
310}
311
312pub fn build_classifier_unavailable_message(tool_name: &str, classifier_model: &str) -> String {
314 format!(
315 "{} is temporarily unavailable, so auto mode cannot determine the safety of {} right now. Wait briefly and then try this action again. If it keeps failing, continue with other tasks that don't require this action and come back to it later. Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used.",
316 classifier_model, tool_name
317 )
318}
319
320pub fn is_classifier_denial(content: &str) -> bool {
322 content.starts_with(AUTO_MODE_REJECTION_PREFIX)
323}
324
325pub fn auto_reject_message(tool_name: &str) -> String {
327 format!(
328 "Permission to use {} has been denied. {}",
329 tool_name, DENIAL_WORKAROUND_GUIDANCE
330 )
331}
332
333pub fn dont_ask_reject_message(tool_name: &str) -> String {
335 format!(
336 "Permission to use {} has been denied because Claude Code is running in don't ask mode. {}",
337 tool_name, DENIAL_WORKAROUND_GUIDANCE
338 )
339}
340
341pub fn derive_short_message_id(uuid: &str) -> String {
343 let hex: String = uuid.replace('-', "").chars().take(10).collect();
345 let parsed = u64::from_str_radix(&hex, 16).unwrap_or(0);
347 let base36 = format!("{:36}", parsed);
348 base36.chars().take(6).collect()
349}
350
351pub fn create_user_message(content: impl Into<MessageContent>) -> UserMessage {
353 let content = content.into();
354 UserMessage {
355 message: content,
356 is_meta: None,
357 is_visible_in_transcript_only: None,
358 is_virtual: None,
359 is_compact_summary: None,
360 summarize_metadata: None,
361 tool_use_result: None,
362 mcp_meta: None,
363 uuid: uuid::Uuid::new_v4().to_string(),
364 timestamp: chrono::Utc::now().to_rfc3339(),
365 image_paste_ids: None,
366 source_tool_assistant_uuid: None,
367 permission_mode: None,
368 origin: None,
369 }
370}
371
372pub fn create_assistant_message(content: Vec<serde_json::Value>) -> AssistantMessage {
374 AssistantMessage {
375 message: AssistantMessageContent {
376 id: uuid::Uuid::new_v4().to_string(),
377 container: None,
378 model: SYNTHETIC_MODEL.to_string(),
379 role: "assistant".to_string(),
380 stop_reason: Some("stop_sequence".to_string()),
381 stop_sequence: Some("".to_string()),
382 message_type: "message".to_string(),
383 usage: Some(Usage::default()),
384 content,
385 context_management: None,
386 },
387 request_id: None,
388 api_error: None,
389 error: None,
390 error_details: None,
391 is_api_error_message: Some(false),
392 is_virtual: None,
393 is_meta: None,
394 advisor_model: None,
395 uuid: uuid::Uuid::new_v4().to_string(),
396 timestamp: chrono::Utc::now().to_rfc3339(),
397 parent_uuid: None,
398 }
399}
400
401pub fn create_progress_message(
403 tool_use_id: &str,
404 parent_tool_use_id: &str,
405 data: serde_json::Value,
406) -> ProgressMessage {
407 ProgressMessage {
408 data_type: "progress".to_string(),
409 data,
410 tool_use_id: Some(tool_use_id.to_string()),
411 parent_tool_use_id: Some(parent_tool_use_id.to_string()),
412 uuid: uuid::Uuid::new_v4().to_string(),
413 timestamp: chrono::Utc::now().to_rfc3339(),
414 parent_uuid: None,
415 }
416}
417
418pub fn create_tool_result_stop_message(tool_use_id: &str) -> serde_json::Value {
420 serde_json::json!({
421 "type": "tool_result",
422 "content": CANCEL_MESSAGE,
423 "is_error": true,
424 "tool_use_id": tool_use_id
425 })
426}
427
428pub const COMMAND_MESSAGE_TAG: &str = "command-message";
430pub const COMMAND_NAME_TAG: &str = "command-name";
431
432pub fn create_synthetic_user_caveat_message() -> Message {
434 let content = "The user didn't say anything. Continue working.".to_string();
435 Message::User(UserMessage {
436 message: MessageContent::String(content),
437 is_meta: Some(true),
438 is_visible_in_transcript_only: None,
439 is_virtual: None,
440 is_compact_summary: None,
441 summarize_metadata: None,
442 tool_use_result: None,
443 mcp_meta: None,
444 uuid: uuid::Uuid::new_v4().to_string(),
445 timestamp: chrono::Utc::now().to_rfc3339(),
446 image_paste_ids: None,
447 source_tool_assistant_uuid: None,
448 permission_mode: None,
449 origin: None,
450 })
451}
452
453pub fn create_system_message(content: impl Into<String>, level: SystemMessageLevel) -> Message {
455 Message::System(SystemMessage {
456 message: SystemMessageContent {
457 message_type: "system".to_string(),
458 subtype: None,
459 content: content.into(),
460 level: Some(level),
461 },
462 uuid: uuid::Uuid::new_v4().to_string(),
463 timestamp: chrono::Utc::now().to_rfc3339(),
464 parent_uuid: None,
465 })
466}
467
468pub fn create_system_local_command_message(content: impl Into<String>) -> Message {
470 Message::System(SystemMessage {
471 message: SystemMessageContent {
472 message_type: "system".to_string(),
473 subtype: Some("local_command".to_string()),
474 content: content.into(),
475 level: None,
476 },
477 uuid: uuid::Uuid::new_v4().to_string(),
478 timestamp: chrono::Utc::now().to_rfc3339(),
479 parent_uuid: None,
480 })
481}
482
483pub fn create_user_interruption_message(tool_use: bool) -> Message {
485 let content = if tool_use {
486 INTERRUPT_MESSAGE_FOR_TOOL_USE.to_string()
487 } else {
488 INTERRUPT_MESSAGE.to_string()
489 };
490 Message::User(UserMessage {
491 message: MessageContent::String(content),
492 is_meta: None,
493 is_visible_in_transcript_only: None,
494 is_virtual: None,
495 is_compact_summary: None,
496 summarize_metadata: None,
497 tool_use_result: None,
498 mcp_meta: None,
499 uuid: uuid::Uuid::new_v4().to_string(),
500 timestamp: chrono::Utc::now().to_rfc3339(),
501 image_paste_ids: None,
502 source_tool_assistant_uuid: None,
503 permission_mode: None,
504 origin: None,
505 })
506}
507
508pub fn format_command_input_tags(command_name: &str, args: &str) -> String {
510 let mut parts = vec![
511 format!("<{COMMAND_MESSAGE_TAG}>{command_name}</{COMMAND_MESSAGE_TAG}>"),
512 format!("<{COMMAND_NAME_TAG}>/{command_name}</{COMMAND_NAME_TAG}>"),
513 ];
514 if !args.trim().is_empty() {
515 parts.push(format!("<command-args>{args}</command-args>"));
516 }
517 parts.join("\n")
518}
519
520pub fn is_system_local_command_message(message: &Message) -> bool {
522 match message {
523 Message::System(sys) => sys.message.subtype.as_deref() == Some("local_command"),
524 _ => false,
525 }
526}
527
528pub fn is_compact_boundary_message(message: &Message) -> bool {
530 match message {
531 Message::User(user) => user.is_compact_summary == Some(true),
532 Message::System(sys) => sys.message.subtype.as_deref() == Some("compact"),
533 _ => false,
534 }
535}
536
537pub fn extract_tag(html: &str, tag_name: &str) -> Option<String> {
539 use regex::Regex;
540
541 if html.trim().is_empty() || tag_name.trim().is_empty() {
542 return None;
543 }
544
545 let escaped_tag = tag_name.replace(
546 [
547 '.', '*', '+', '?', '^', '$', '{', '}', '[', ']', '(', ')', '|', '\\',
548 ],
549 "\\$&",
550 );
551
552 let pattern = format!(
553 r"<{}(?:\s+[^>]*)?>([\s\S]*?)</{}>",
554 escaped_tag, escaped_tag
555 );
556
557 let re = Regex::new(&pattern).ok()?;
558
559 let mut depth = 0i32;
560 let mut last_index = 0;
561
562 let opening_tag_re = Regex::new(&format!(r"<{}(?:\s+[^>]*)?>", escaped_tag)).ok()?;
563 let closing_tag_re = Regex::new(&format!(r"</{}>", escaped_tag)).ok()?;
564
565 for caps in re.captures_iter(html) {
566 let content = caps.get(1)?.as_str();
567 let start = caps.get(0)?.start();
568
569 depth = 0;
570
571 for _ in opening_tag_re.find_iter(&html[..start]) {
572 depth += 1;
573 }
574
575 for _ in closing_tag_re.find_iter(&html[..start]) {
576 depth -= 1;
577 }
578
579 if depth == 0 && !content.is_empty() {
580 return Some(content.to_string());
581 }
582
583 last_index = start + caps.get(0)?.len();
584 }
585
586 None
587}
588
589pub fn is_not_empty_message(message: &Message) -> bool {
591 match message {
592 Message::Progress(_) | Message::Attachment(_) | Message::System(_) => true,
593 Message::User(user) => {
594 match &user.message {
595 MessageContent::String(s) => !s.trim().is_empty(),
596 MessageContent::Blocks(blocks) => {
597 if blocks.is_empty() {
598 return false;
599 }
600 if blocks.len() > 1 {
602 return true;
603 }
604 match &blocks[0] {
606 ContentBlock::Text { text } => {
607 !text.trim().is_empty()
608 && text != NO_RESPONSE_REQUESTED
609 && text != INTERRUPT_MESSAGE_FOR_TOOL_USE
610 }
611 _ => true,
612 }
613 }
614 }
615 }
616 Message::Assistant(assistant) => !assistant.message.content.is_empty(),
617 }
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize)]
622#[serde(tag = "type")]
623pub enum NormalizedMessage {
624 User(NormalizedUserMessage),
625 Assistant(NormalizedAssistantMessage),
626 Progress(ProgressMessage),
627 Attachment(AttachmentMessage),
628 System(SystemMessage),
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize)]
633pub struct NormalizedUserMessage {
634 pub message: MessageContent,
635 #[serde(flatten)]
636 pub extra: UserMessageExtra,
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize)]
641pub struct NormalizedAssistantMessage {
642 pub message: AssistantMessageContent,
643 #[serde(flatten)]
644 pub extra: AssistantMessageExtra,
645}
646
647#[derive(Debug, Clone, Default, Serialize, Deserialize)]
649pub struct UserMessageExtra {
650 #[serde(skip_serializing_if = "Option::is_none")]
651 pub is_meta: Option<bool>,
652 #[serde(skip_serializing_if = "Option::is_none")]
653 pub is_visible_in_transcript_only: Option<bool>,
654 #[serde(skip_serializing_if = "Option::is_none")]
655 pub is_virtual: Option<bool>,
656 #[serde(skip_serializing_if = "Option::is_none")]
657 pub is_compact_summary: Option<bool>,
658 #[serde(skip_serializing_if = "Option::is_none")]
659 pub summarize_metadata: Option<serde_json::Value>,
660 #[serde(skip_serializing_if = "Option::is_none")]
661 pub tool_use_result: Option<serde_json::Value>,
662 #[serde(skip_serializing_if = "Option::is_none")]
663 pub mcp_meta: Option<serde_json::Value>,
664 pub uuid: String,
665 pub timestamp: String,
666 #[serde(skip_serializing_if = "Option::is_none")]
667 pub image_paste_ids: Option<Vec<u32>>,
668 #[serde(skip_serializing_if = "Option::is_none")]
669 pub source_tool_assistant_uuid: Option<String>,
670 #[serde(skip_serializing_if = "Option::is_none")]
671 pub permission_mode: Option<String>,
672 #[serde(skip_serializing_if = "Option::is_none")]
673 pub origin: Option<MessageOrigin>,
674 #[serde(skip_serializing_if = "Option::is_none")]
675 pub parent_uuid: Option<String>,
676}
677
678#[derive(Debug, Clone, Default, Serialize, Deserialize)]
680pub struct AssistantMessageExtra {
681 #[serde(skip_serializing_if = "Option::is_none")]
682 pub request_id: Option<String>,
683 #[serde(skip_serializing_if = "Option::is_none")]
684 pub api_error: Option<serde_json::Value>,
685 #[serde(skip_serializing_if = "Option::is_none")]
686 pub error: Option<serde_json::Value>,
687 #[serde(skip_serializing_if = "Option::is_none")]
688 pub error_details: Option<String>,
689 #[serde(skip_serializing_if = "Option::is_none")]
690 pub is_api_error_message: Option<bool>,
691 #[serde(skip_serializing_if = "Option::is_none")]
692 pub is_virtual: Option<bool>,
693 #[serde(skip_serializing_if = "Option::is_none")]
694 pub is_meta: Option<bool>,
695 #[serde(skip_serializing_if = "Option::is_none")]
696 pub advisor_model: Option<String>,
697 pub uuid: String,
698 pub timestamp: String,
699 #[serde(skip_serializing_if = "Option::is_none")]
700 pub parent_uuid: Option<String>,
701}
702
703pub fn derive_uuid(parent_uuid: &str, index: usize) -> String {
705 let hex = format!("{:012x}", index);
706 let parent_trimmed = parent_uuid.replace('-', "");
707 let prefix = &parent_trimmed[..24.min(parent_trimmed.len())];
708 format!("{}-{}-{}", &prefix[0..8], &prefix[8..12], hex)
709}
710
711pub fn get_tool_use_id(message: &NormalizedMessage) -> Option<String> {
713 match message {
714 NormalizedMessage::Assistant(msg) => {
715 if let Some(first) = msg.message.content.first() {
716 if let Ok(block) = serde_json::from_value::<ContentBlock>(first.clone()) {
717 match block {
718 ContentBlock::ToolUse { id, .. } => Some(id),
719 _ => None,
720 }
721 } else {
722 first.get("id").and_then(|v| v.as_str()).map(String::from)
724 }
725 } else {
726 None
727 }
728 }
729 _ => None,
730 }
731}
732
733#[derive(Debug, Default)]
735pub struct MessageLookups {
736 pub sibling_tool_use_ids: HashMap<String, HashSet<String>>,
737 pub progress_messages_by_tool_use_id: HashMap<String, Vec<ProgressMessage>>,
738 pub in_progress_hook_counts: HashMap<String, HashMap<String, u32>>,
739 pub resolved_hook_counts: HashMap<String, HashMap<String, u32>>,
740 pub tool_result_by_tool_use_id: HashMap<String, NormalizedMessage>,
741 pub tool_use_by_tool_use_id: HashMap<String, serde_json::Value>,
742 pub normalized_message_count: usize,
743 pub resolved_tool_use_ids: HashSet<String>,
744 pub errored_tool_use_ids: HashSet<String>,
745}
746
747pub fn build_message_lookups(
749 normalized_messages: &[NormalizedMessage],
750 messages: &[Message],
751) -> MessageLookups {
752 let mut lookups = MessageLookups::default();
753
754 let mut tool_use_ids_by_message_id: HashMap<String, HashSet<String>> = HashMap::new();
756 let mut tool_use_id_to_message_id: HashMap<String, String> = HashMap::new();
757 let mut tool_use_by_tool_use_id: HashMap<String, serde_json::Value> = HashMap::new();
758
759 for msg in messages {
760 if let Message::Assistant(assistant) = msg {
761 let id = &assistant.message.id;
762 let mut tool_use_ids = HashSet::new();
763 for content in &assistant.message.content {
764 if let Ok(block) = serde_json::from_value::<ContentBlock>(content.clone()) {
765 if let ContentBlock::ToolUse { id: tool_id, .. } = block {
766 tool_use_ids.insert(tool_id.clone());
767 tool_use_id_to_message_id.insert(tool_id.clone(), id.clone());
768 tool_use_by_tool_use_id.insert(tool_id.clone(), content.clone());
769 }
770 }
771 }
772 if !tool_use_ids.is_empty() {
773 tool_use_ids_by_message_id.insert(id.clone(), tool_use_ids);
774 }
775 }
776 }
777
778 for (tool_use_id, message_id) in &tool_use_id_to_message_id {
780 if let Some(ids) = tool_use_ids_by_message_id.get(message_id) {
781 lookups
782 .sibling_tool_use_ids
783 .insert(tool_use_id.clone(), ids.clone());
784 }
785 }
786
787 for msg in normalized_messages {
789 if let NormalizedMessage::Progress(progress) = msg {
790 let tool_use_id = progress.parent_tool_use_id.clone().unwrap_or_default();
791 if !tool_use_id.is_empty() {
792 lookups
793 .progress_messages_by_tool_use_id
794 .entry(tool_use_id.clone())
795 .or_insert_with(Vec::new)
796 .push(progress.clone());
797 }
798 }
799
800 if let NormalizedMessage::User(user) = msg {
802 if let MessageContent::Blocks(blocks) = &user.message {
803 for block in blocks {
804 if let ContentBlock::ToolResult {
805 tool_use_id,
806 is_error,
807 ..
808 } = block
809 {
810 lookups.resolved_tool_use_ids.insert(tool_use_id.clone());
811 if is_error == &Some(true) {
812 lookups.errored_tool_use_ids.insert(tool_use_id.clone());
813 }
814 }
815 }
816 }
817 }
818
819 if let NormalizedMessage::Assistant(assistant) = msg {
821 for content in &assistant.message.content {
822 if let Some(tool_use_id) = content.get("id") {
824 if let Some(id_str) = tool_use_id.as_str() {
825 let has_result = lookups.resolved_tool_use_ids.contains(id_str);
827 if !has_result {
828 lookups.resolved_tool_use_ids.insert(id_str.to_string());
830 }
831 }
832 }
833 }
834 }
835 }
836
837 lookups.tool_use_by_tool_use_id = tool_use_by_tool_use_id;
838 lookups.normalized_message_count = normalized_messages.len();
839
840 lookups
841}
842
843pub fn get_sibling_tool_use_ids_from_lookup(
845 message: &NormalizedMessage,
846 lookups: &MessageLookups,
847) -> HashSet<String> {
848 let tool_use_id = match get_tool_use_id(message) {
849 Some(id) => id,
850 None => return HashSet::new(),
851 };
852 lookups
853 .sibling_tool_use_ids
854 .get(&tool_use_id)
855 .cloned()
856 .unwrap_or_default()
857}
858
859pub fn get_progress_messages_from_lookup(
861 message: &NormalizedMessage,
862 lookups: &MessageLookups,
863) -> Vec<ProgressMessage> {
864 let tool_use_id = match get_tool_use_id(message) {
865 Some(id) => id,
866 None => return Vec::new(),
867 };
868 lookups
869 .progress_messages_by_tool_use_id
870 .get(&tool_use_id)
871 .cloned()
872 .unwrap_or_default()
873}
874
875pub fn get_tool_result_ids(normalized_messages: &[NormalizedMessage]) -> HashMap<String, bool> {
877 let mut result = HashMap::new();
878
879 for msg in normalized_messages {
880 if let NormalizedMessage::User(user) = msg {
881 if let MessageContent::Blocks(blocks) = &user.message {
882 for block in blocks {
883 if let ContentBlock::ToolResult {
884 tool_use_id,
885 is_error,
886 ..
887 } = block
888 {
889 result.insert(tool_use_id.clone(), is_error.unwrap_or(false));
890 }
891 }
892 }
893 }
894 }
895
896 result
897}
898
899pub fn reorder_attachments_for_api(messages: Vec<Message>) -> Vec<Message> {
901 let mut result = Vec::new();
902 let mut pending_attachments: Vec<Message> = Vec::new();
903
904 for i in (0..messages.len()).rev() {
906 let message = messages[i].clone();
907
908 if let Message::Attachment(_) = message {
909 pending_attachments.push(message);
910 } else {
911 let is_stopping_point = matches!(
912 message,
913 Message::Assistant(_) | Message::User(_) if has_tool_result(&message)
914 );
915
916 if is_stopping_point && !pending_attachments.is_empty() {
917 for att in pending_attachments.drain(..).rev() {
919 result.push(att);
920 }
921 result.push(message);
922 } else {
923 result.push(message);
924 }
925 }
926 }
927
928 for att in pending_attachments.drain(..).rev() {
930 result.push(att);
931 }
932
933 result.reverse();
934 result
935}
936
937fn has_tool_result(message: &Message) -> bool {
939 if let Message::User(user) = message {
940 if let MessageContent::Blocks(blocks) = &user.message {
941 return blocks
942 .iter()
943 .any(|b| matches!(b, ContentBlock::ToolResult { .. }));
944 }
945 }
946 false
947}
948
949pub fn is_tool_use_request_message(message: &Message) -> bool {
951 if let Message::Assistant(assistant) = message {
952 assistant.message.content.iter().any(|c| {
953 if let Ok(block) = serde_json::from_value::<ContentBlock>(c.clone()) {
954 matches!(block, ContentBlock::ToolUse { .. })
955 } else {
956 c.get("type").and_then(|t| t.as_str()) == Some("tool_use")
957 }
958 })
959 } else {
960 false
961 }
962}
963
964pub fn is_tool_use_result_message(message: &Message) -> bool {
966 if let Message::User(user) = message {
967 if let MessageContent::Blocks(blocks) = &user.message {
968 return blocks
969 .iter()
970 .any(|b| matches!(b, ContentBlock::ToolResult { .. }));
971 }
972 }
973 false
974}
975
976pub fn get_last_assistant_message(messages: &[Message]) -> Option<&AssistantMessage> {
978 messages.iter().rev().find_map(|m| {
979 if let Message::Assistant(a) = m {
980 Some(a)
981 } else {
982 None
983 }
984 })
985}
986
987pub fn has_tool_calls_in_last_assistant_turn(messages: &[Message]) -> bool {
989 for msg in messages.iter().rev() {
990 if let Message::Assistant(assistant) = msg {
991 return assistant.message.content.iter().any(|c| {
992 if let Ok(block) = serde_json::from_value::<ContentBlock>(c.clone()) {
993 matches!(block, ContentBlock::ToolUse { .. })
994 } else {
995 c.get("type").and_then(|t| t.as_str()) == Some("tool_use")
996 }
997 });
998 }
999 }
1000 false
1001}
1002
1003pub fn empty_lookups() -> MessageLookups {
1005 MessageLookups::default()
1006}
1007
1008pub fn empty_string_set() -> HashSet<String> {
1010 HashSet::new()
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::*;
1016
1017 #[test]
1018 fn test_derive_short_message_id() {
1019 let uuid = "550e8400-e29b-41d4-a716-446655440000";
1020 let short_id = derive_short_message_id(uuid);
1021 assert_eq!(short_id.len(), 6);
1022 }
1023
1024 #[test]
1025 fn test_build_yolo_rejection_message() {
1026 let msg = build_yolo_rejection_message("dangerous command");
1027 assert!(msg.contains("Permission for this action has been denied"));
1028 assert!(msg.contains("dangerous command"));
1029 }
1030
1031 #[test]
1032 fn test_is_classifier_denial() {
1033 assert!(is_classifier_denial(
1034 "Permission for this action has been denied. Reason: testing"
1035 ));
1036 assert!(!is_classifier_denial("Just a regular message"));
1037 }
1038
1039 #[test]
1040 fn test_extract_tag() {
1041 let html = "<test>Hello World</test>";
1042 let extracted = extract_tag(html, "test");
1043 assert_eq!(extracted, Some("Hello World".to_string()));
1044 }
1045
1046 #[test]
1047 fn test_derive_uuid() {
1048 let parent = "550e8400-e29b-41d4-a716-446655440000";
1049 let derived = derive_uuid(parent, 0);
1050 assert!(!derived.is_empty());
1051 }
1052
1053 #[test]
1054 fn test_get_tool_result_ids() {
1055 let messages = vec![NormalizedMessage::User(NormalizedUserMessage {
1056 message: MessageContent::Blocks(vec![ContentBlock::ToolResult {
1057 tool_use_id: "test-id".to_string(),
1058 content: None,
1059 is_error: Some(false),
1060 }]),
1061 extra: UserMessageExtra {
1062 uuid: "uuid".to_string(),
1063 timestamp: "timestamp".to_string(),
1064 ..Default::default()
1065 },
1066 })];
1067
1068 let ids = get_tool_result_ids(&messages);
1069 assert!(ids.contains_key("test-id"));
1070 }
1071}