Download crates/forge_domain/src/compact/summary.rs from SaylorTwift/forgecode: direct link, hf CLI and curl.
- Browser
- Download file 54 kB
-
https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_domain/src/compact/summary.rs
- Command line
-
hf download hf://SaylorTwift/forgecode/crates/forge_domain/src/compact/summary.rs
-
curl -L -o summary.rs https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_domain/src/compact/summary.rs
54 kB
| use std::collections::HashMap; | |
| use std::ops::Deref; | |
| use derive_more::From; | |
| use serde::{Deserialize, Serialize}; | |
| use crate::{ | |
| Context, ContextMessage, Role, SearchQuery, TextMessage, Todo, ToolCallFull, ToolCallId, | |
| ToolCatalog, ToolResult, | |
| }; | |
| /// A simplified summary of a context, focusing on messages and their tool calls | |
| pub struct ContextSummary { | |
| pub messages: Vec<SummaryBlock>, | |
| } | |
| /// A simplified representation of a message with its key information | |
| pub struct SummaryBlock { | |
| pub role: Role, | |
| pub contents: Vec<SummaryMessage>, | |
| } | |
| /// A message block that can be either content or a tool call | |
| pub enum SummaryMessage { | |
| Text(String), | |
| ToolCall( SummaryToolCall), | |
| } | |
| /// Tool call data with execution status | |
| pub struct SummaryToolCall { | |
| pub id: Option<ToolCallId>, | |
| pub tool: SummaryTool, | |
| pub is_success: bool, | |
| } | |
| impl ContextSummary { | |
| /// Creates a new ContextSummary with the given messages | |
| pub fn new(messages: Vec<SummaryBlock>) -> Self { | |
| Self { messages } | |
| } | |
| } | |
| impl SummaryBlock { | |
| /// Creates a new SummaryMessage with the given role and blocks | |
| pub fn new(role: Role, blocks: Vec<SummaryMessage>) -> Self { | |
| Self { role, contents: blocks } | |
| } | |
| } | |
| impl SummaryMessage { | |
| /// Creates a content block | |
| pub fn content(text: impl Into<String>) -> Self { | |
| Self::Text(text.into()) | |
| } | |
| } | |
| impl SummaryToolCall { | |
| /// Creates a FileRead tool call with default values (id: None, is_success: | |
| /// true) | |
| pub fn read(path: impl Into<String>) -> Self { | |
| Self { | |
| id: None, | |
| tool: SummaryTool::FileRead { path: path.into() }, | |
| is_success: true, | |
| } | |
| } | |
| /// Creates a FileUpdate tool call with default values (id: None, | |
| /// is_success: true) | |
| pub fn update(path: impl Into<String>) -> Self { | |
| Self { | |
| id: None, | |
| tool: SummaryTool::FileUpdate { path: path.into() }, | |
| is_success: true, | |
| } | |
| } | |
| /// Creates a FileRemove tool call with default values (id: None, | |
| /// is_success: true) | |
| pub fn remove(path: impl Into<String>) -> Self { | |
| Self { | |
| id: None, | |
| tool: SummaryTool::FileRemove { path: path.into() }, | |
| is_success: true, | |
| } | |
| } | |
| /// Creates a Shell tool call with default values (id: None, is_success: | |
| /// true) | |
| pub fn shell(command: impl Into<String>) -> Self { | |
| Self { | |
| id: None, | |
| tool: SummaryTool::Shell { command: command.into() }, | |
| is_success: true, | |
| } | |
| } | |
| /// Creates a Search tool call with default values (id: None, is_success: | |
| /// true) | |
| pub fn search(pattern: impl Into<String>) -> Self { | |
| Self { | |
| id: None, | |
| tool: SummaryTool::Search { pattern: pattern.into() }, | |
| is_success: true, | |
| } | |
| } | |
| /// Creates a CodebaseSearch tool call with default values (id: None, | |
| /// is_success: true) | |
| pub fn codebase_search(queries: Vec<SearchQuery>) -> Self { | |
| Self { | |
| id: None, | |
| tool: SummaryTool::SemSearch { queries }, | |
| is_success: true, | |
| } | |
| } | |
| /// Creates an Undo tool call with default values (id: None, is_success: | |
| /// true) | |
| pub fn undo(path: impl Into<String>) -> Self { | |
| Self { | |
| id: None, | |
| tool: SummaryTool::Undo { path: path.into() }, | |
| is_success: true, | |
| } | |
| } | |
| /// Creates a Fetch tool call with default values (id: None, is_success: | |
| /// true) | |
| pub fn fetch(url: impl Into<String>) -> Self { | |
| Self { | |
| id: None, | |
| tool: SummaryTool::Fetch { url: url.into() }, | |
| is_success: true, | |
| } | |
| } | |
| /// Creates a Followup tool call with default values (id: None, is_success: | |
| /// true) | |
| pub fn followup(question: impl Into<String>) -> Self { | |
| Self { | |
| id: None, | |
| tool: SummaryTool::Followup { question: question.into() }, | |
| is_success: true, | |
| } | |
| } | |
| /// Creates a Plan tool call with default values (id: None, is_success: | |
| /// true) | |
| pub fn plan(plan_name: impl Into<String>) -> Self { | |
| Self { | |
| id: None, | |
| tool: SummaryTool::Plan { plan_name: plan_name.into() }, | |
| is_success: true, | |
| } | |
| } | |
| /// Creates an MCP tool call with default values (id: None, is_success: | |
| /// true) | |
| pub fn mcp(name: impl Into<String>) -> Self { | |
| Self { | |
| id: None, | |
| tool: SummaryTool::Mcp { name: name.into() }, | |
| is_success: true, | |
| } | |
| } | |
| } | |
| pub enum SummaryTool { | |
| FileRead { path: String }, | |
| FileUpdate { path: String }, | |
| FileRemove { path: String }, | |
| Shell { command: String }, | |
| Search { pattern: String }, | |
| SemSearch { queries: Vec<SearchQuery> }, | |
| Undo { path: String }, | |
| Fetch { url: String }, | |
| Followup { question: String }, | |
| Plan { plan_name: String }, | |
| Skill { name: String }, | |
| Task { agent_id: String }, | |
| Mcp { name: String }, | |
| TodoWrite { changes: Vec<TodoChange> }, | |
| TodoRead, | |
| } | |
| /// The kind of change applied to a todo item | |
| pub enum TodoChangeKind { | |
| Added, | |
| Updated, | |
| Removed, | |
| } | |
| /// A single todo change entry capturing what changed and how | |
| pub struct TodoChange { | |
| pub todo: Todo, | |
| pub kind: TodoChangeKind, | |
| } | |
| impl From<&Context> for ContextSummary { | |
| fn from(value: &Context) -> Self { | |
| let mut messages = vec![]; | |
| let mut buffer: Vec<SummaryMessage> = vec![]; | |
| let mut tool_results: HashMap<&ToolCallId, &ToolResult> = Default::default(); | |
| let mut current_role = Role::System; | |
| // Track the current todo state to compute diffs across tool calls | |
| let mut current_todos: Vec<Todo> = vec![]; | |
| for msg in &value.messages { | |
| match msg.deref() { | |
| ContextMessage::Text(text_msg) => { | |
| // Skip system messages | |
| if text_msg.role == Role::System { | |
| continue; | |
| } | |
| if current_role != text_msg.role { | |
| // Only push if buffer is not empty (avoid empty System role at start) | |
| if !buffer.is_empty() { | |
| messages.push(SummaryBlock { | |
| role: current_role, | |
| contents: std::mem::take(&mut buffer), | |
| }); | |
| } | |
| current_role = text_msg.role; | |
| } | |
| buffer.extend(extract_summary_messages(text_msg, ¤t_todos)); | |
| // Update current_todos if this is a TodoWrite call | |
| if let Some(calls) = &text_msg.tool_calls { | |
| for call in calls { | |
| if let Ok(ToolCatalog::TodoWrite(input)) = | |
| ToolCatalog::try_from(call.clone()) | |
| { | |
| for item in &input.todos { | |
| if item.status == crate::TodoStatus::Cancelled { | |
| current_todos.retain(|t| t.content != item.content); | |
| } else if let Some(existing) = | |
| current_todos.iter_mut().find(|t| t.content == item.content) | |
| { | |
| existing.status = item.status; | |
| } else { | |
| current_todos.push(Todo { | |
| id: String::new(), | |
| content: item.content.clone(), | |
| status: item.status, | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| ContextMessage::Tool(tool_result) => { | |
| if let Some(ref call_id) = tool_result.call_id { | |
| tool_results.insert(call_id, tool_result); | |
| } | |
| } | |
| ContextMessage::Image(_) => {} | |
| } | |
| } | |
| // Insert the last chunk if buffer is not empty | |
| if !buffer.is_empty() { | |
| messages | |
| .push(SummaryBlock { role: current_role, contents: std::mem::take(&mut buffer) }); | |
| } | |
| // Update tool call success status based on results | |
| messages | |
| .iter_mut() | |
| .flat_map(|message| message.contents.iter_mut()) | |
| .for_each(|block| { | |
| if let SummaryMessage::ToolCall(tool_data) = block | |
| && let Some(call_id) = &tool_data.id | |
| && let Some(result) = tool_results.get(call_id) | |
| { | |
| tool_data.is_success = !result.is_error(); | |
| } | |
| }); | |
| ContextSummary { messages } | |
| } | |
| } | |
| /// Extracts summary messages from a text message, using current_todos for diff | |
| /// computation | |
| fn extract_summary_messages(text_msg: &TextMessage, current_todos: &[Todo]) -> Vec<SummaryMessage> { | |
| let mut blocks = vec![]; | |
| // Add content block if there's text content | |
| if !text_msg.content.is_empty() { | |
| blocks.push(SummaryMessage::Text(text_msg.content.clone())); | |
| } | |
| // Add tool call blocks if present | |
| if let Some(calls) = &text_msg.tool_calls { | |
| blocks.extend(calls.iter().filter_map(|tool_call| { | |
| extract_tool_info(tool_call, current_todos).map(|call| { | |
| SummaryMessage::ToolCall(SummaryToolCall { | |
| id: tool_call.call_id.clone(), | |
| tool: call, | |
| is_success: false, | |
| }) | |
| }) | |
| })); | |
| } | |
| blocks | |
| } | |
| impl From<&TextMessage> for Vec<SummaryMessage> { | |
| fn from(text_msg: &TextMessage) -> Self { | |
| extract_summary_messages(text_msg, &[]) | |
| } | |
| } | |
| /// Extracts tool information from a tool call, using current_todos as the | |
| /// before-state for diffs | |
| fn extract_tool_info(call: &ToolCallFull, current_todos: &[Todo]) -> Option<SummaryTool> { | |
| // Try to parse as a Tools enum variant | |
| if let Ok(tool) = ToolCatalog::try_from(call.clone()) { | |
| return match tool { | |
| ToolCatalog::Read(input) => Some(SummaryTool::FileRead { path: input.file_path }), | |
| ToolCatalog::Write(input) => Some(SummaryTool::FileUpdate { path: input.file_path }), | |
| ToolCatalog::Patch(input) => Some(SummaryTool::FileUpdate { path: input.file_path }), | |
| ToolCatalog::MultiPatch(input) => { | |
| Some(SummaryTool::FileUpdate { path: input.file_path }) | |
| } | |
| ToolCatalog::Remove(input) => Some(SummaryTool::FileRemove { path: input.path }), | |
| ToolCatalog::Shell(input) => Some(SummaryTool::Shell { command: input.command }), | |
| ToolCatalog::FsSearch(input) => { | |
| // Use glob, file_type, or pattern as the search identifier | |
| let pattern = input.glob.or(input.file_type).unwrap_or(input.pattern); | |
| Some(SummaryTool::Search { pattern }) | |
| } | |
| ToolCatalog::SemSearch(input) => { | |
| Some(SummaryTool::SemSearch { queries: input.queries }) | |
| } | |
| ToolCatalog::Undo(input) => Some(SummaryTool::Undo { path: input.path }), | |
| ToolCatalog::Fetch(input) => Some(SummaryTool::Fetch { url: input.url }), | |
| ToolCatalog::Followup(input) => { | |
| Some(SummaryTool::Followup { question: input.question }) | |
| } | |
| ToolCatalog::Plan(input) => Some(SummaryTool::Plan { plan_name: input.plan_name }), | |
| ToolCatalog::Skill(input) => Some(SummaryTool::Skill { name: input.name }), | |
| ToolCatalog::TodoWrite(input) => { | |
| let before_map: HashMap<&str, &Todo> = current_todos | |
| .iter() | |
| .map(|t| (t.content.as_str(), t)) | |
| .collect(); | |
| let mut changes = vec![]; | |
| for item in &input.todos { | |
| if item.status == crate::TodoStatus::Cancelled { | |
| if let Some(prev) = before_map.get(item.content.as_str()) { | |
| changes.push(TodoChange { | |
| todo: (*prev).clone(), | |
| kind: TodoChangeKind::Removed, | |
| }); | |
| } | |
| } else { | |
| match before_map.get(item.content.as_str()) { | |
| None => changes.push(TodoChange { | |
| todo: Todo { | |
| id: String::new(), | |
| content: item.content.clone(), | |
| status: item.status, | |
| }, | |
| kind: TodoChangeKind::Added, | |
| }), | |
| Some(prev) if prev.status != item.status => { | |
| changes.push(TodoChange { | |
| todo: Todo { | |
| id: prev.id.clone(), | |
| content: item.content.clone(), | |
| status: item.status, | |
| }, | |
| kind: TodoChangeKind::Updated, | |
| }); | |
| } | |
| _ => {} | |
| } | |
| } | |
| } | |
| Some(SummaryTool::TodoWrite { changes }) | |
| } | |
| ToolCatalog::TodoRead(_) => Some(SummaryTool::TodoRead), | |
| ToolCatalog::Task(input) => Some(SummaryTool::Task { agent_id: input.agent_id }), | |
| }; | |
| } | |
| // If not a known tool catalog item, treat as MCP tool | |
| Some(SummaryTool::Mcp { name: call.name.to_string() }) | |
| } | |
| mod tests { | |
| use pretty_assertions::assert_eq; | |
| use super::*; | |
| use crate::{ContextMessage, TextMessage, ToolCallArguments, ToolCallId, ToolName, ToolOutput}; | |
| type Block = SummaryMessage; | |
| fn context(messages: Vec<ContextMessage>) -> Context { | |
| Context::default().messages(messages.into_iter().map(|m| m.into()).collect::<Vec<_>>()) | |
| } | |
| fn user(content: impl Into<String>) -> ContextMessage { | |
| ContextMessage::Text(TextMessage::new(Role::User, content)) | |
| } | |
| fn assistant(content: impl Into<String>) -> ContextMessage { | |
| ContextMessage::Text(TextMessage::new(Role::Assistant, content)) | |
| } | |
| fn assistant_with_tools( | |
| content: impl Into<String>, | |
| tool_calls: Vec<ToolCallFull>, | |
| ) -> ContextMessage { | |
| ContextMessage::Text(TextMessage::new(Role::Assistant, content).tool_calls(tool_calls)) | |
| } | |
| fn system(content: impl Into<String>) -> ContextMessage { | |
| ContextMessage::Text(TextMessage::new(Role::System, content)) | |
| } | |
| fn tool_result(name: &str, call_id: &str, is_error: bool) -> ContextMessage { | |
| ContextMessage::Tool(ToolResult { | |
| name: ToolName::new(name), | |
| call_id: Some(ToolCallId::new(call_id)), | |
| output: ToolOutput::text("result").is_error(is_error), | |
| }) | |
| } | |
| fn test_summary_message_block_read_helper() { | |
| let actual: SummaryMessage = SummaryToolCall::read("/path/to/file.rs").into(); | |
| let expected = Block::ToolCall(SummaryToolCall { | |
| id: None, | |
| tool: SummaryTool::FileRead { path: "/path/to/file.rs".to_string() }, | |
| is_success: true, | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_summary_message_block_update_helper() { | |
| let actual: SummaryMessage = SummaryToolCall::update("/path/to/file.rs").into(); | |
| let expected = Block::ToolCall(SummaryToolCall { | |
| id: None, | |
| tool: SummaryTool::FileUpdate { path: "/path/to/file.rs".to_string() }, | |
| is_success: true, | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_summary_message_block_remove_helper() { | |
| let actual: SummaryMessage = SummaryToolCall::remove("/path/to/file.rs").into(); | |
| let expected = Block::ToolCall(SummaryToolCall { | |
| id: None, | |
| tool: SummaryTool::FileRemove { path: "/path/to/file.rs".to_string() }, | |
| is_success: true, | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_empty_context() { | |
| let fixture = Context::default(); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::default(); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_user_and_assistant_without_tools() { | |
| let fixture = context(vec![ | |
| user("Please help me"), | |
| assistant("Sure, I can help"), | |
| user("Thanks"), | |
| assistant("You're welcome"), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![ | |
| SummaryBlock::new(Role::User, vec![Block::content("Please help me")]), | |
| SummaryBlock::new(Role::Assistant, vec![Block::content("Sure, I can help")]), | |
| SummaryBlock::new(Role::User, vec![Block::content("Thanks")]), | |
| SummaryBlock::new(Role::Assistant, vec![Block::content("You're welcome")]), | |
| ]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_skips_system_messages() { | |
| let fixture = context(vec![system("System prompt"), user("User message")]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::User, | |
| vec![Block::content("User message")], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_extracts_file_read_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Reading file", | |
| vec![ToolCatalog::tool_call_read("/test/file.rs").call_id("call_1")], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Reading file"), | |
| SummaryToolCall::read("/test/file.rs") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_extracts_file_write_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Writing file", | |
| vec![ToolCatalog::tool_call_write("/test/file.rs", "test").call_id("call_1")], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Writing file"), | |
| SummaryToolCall::update("/test/file.rs") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_extracts_file_patch_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Patching file", | |
| vec![ | |
| ToolCatalog::tool_call_patch("/test/file.rs", "new", "old", false) | |
| .call_id("call_1"), | |
| ], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Patching file"), | |
| SummaryToolCall::update("/test/file.rs") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_extracts_file_remove_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Removing file", | |
| vec![ToolCatalog::tool_call_remove("/test/file.rs").call_id("call_1")], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Removing file"), | |
| SummaryToolCall::remove("/test/file.rs") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_extracts_read_image_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Reading image", | |
| vec![ToolCatalog::tool_call_read("/test/image.png").call_id("call_1")], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Reading image"), | |
| SummaryToolCall::read("/test/image.png") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_extracts_shell_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Running shell", | |
| vec![ToolCatalog::tool_call_shell("ls -la", "/test").call_id("call_1")], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Running shell"), | |
| SummaryToolCall::shell("ls -la") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_multiple_tool_calls_in_message() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Multiple operations", | |
| vec![ | |
| ToolCatalog::tool_call_read("/test/file1.rs").call_id("call_1"), | |
| ToolCatalog::tool_call_write("/test/file2.rs", "test").call_id("call_2"), | |
| ToolCatalog::tool_call_remove("/test/file3.rs").call_id("call_3"), | |
| ], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Multiple operations"), | |
| SummaryToolCall::read("/test/file1.rs") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::update("/test/file2.rs") | |
| .id("call_2") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::remove("/test/file3.rs") | |
| .id("call_3") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_links_tool_results_to_calls_success() { | |
| let fixture = context(vec![ | |
| assistant_with_tools( | |
| "Reading file", | |
| vec![ToolCatalog::tool_call_read("/test/file.rs").call_id("call_1")], | |
| ), | |
| tool_result("read", "call_1", false), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Reading file"), | |
| SummaryToolCall::read("/test/file.rs").id("call_1").into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_links_tool_results_to_calls_failure() { | |
| let fixture = context(vec![ | |
| assistant_with_tools( | |
| "Reading file", | |
| vec![ToolCatalog::tool_call_read("/test/file.rs").call_id("call_1")], | |
| ), | |
| tool_result("read", "call_1", true), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Reading file"), | |
| SummaryToolCall::read("/test/file.rs") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_multiple_tool_results() { | |
| let fixture = context(vec![ | |
| assistant_with_tools( | |
| "Multiple operations", | |
| vec![ | |
| ToolCatalog::tool_call_read("/test/file1.rs").call_id("call_1"), | |
| ToolCatalog::tool_call_write("/test/file2.rs", "test").call_id("call_2"), | |
| ], | |
| ), | |
| tool_result("read", "call_1", false), | |
| tool_result("write", "call_2", true), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Multiple operations"), | |
| SummaryToolCall::read("/test/file1.rs").id("call_1").into(), | |
| SummaryToolCall::update("/test/file2.rs") | |
| .id("call_2") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_tool_result_without_call_id() { | |
| let fixture = context(vec![ | |
| assistant_with_tools( | |
| "Reading file", | |
| vec![ToolCatalog::tool_call_read("/test/file.rs").call_id("call_1")], | |
| ), | |
| ContextMessage::Tool(ToolResult { | |
| name: ToolName::new("read"), | |
| call_id: None, | |
| output: ToolOutput::text("result"), | |
| }), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Reading file"), | |
| SummaryToolCall::read("/test/file.rs") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_complex_conversation() { | |
| let fixture = context(vec![ | |
| system("System prompt"), | |
| user("Read this file"), | |
| assistant_with_tools( | |
| "Reading", | |
| vec![ToolCatalog::tool_call_read("/test/file1.rs").call_id("call_1")], | |
| ), | |
| tool_result("read", "call_1", false), | |
| user("Now update it"), | |
| assistant_with_tools( | |
| "Updating", | |
| vec![ | |
| ToolCatalog::tool_call_write("/test/file1.rs", "new content").call_id("call_2"), | |
| ], | |
| ), | |
| tool_result("write", "call_2", false), | |
| assistant("Done"), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![ | |
| SummaryBlock::new(Role::User, vec![Block::content("Read this file")]), | |
| SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Reading"), | |
| SummaryToolCall::read("/test/file1.rs").id("call_1").into(), | |
| ], | |
| ), | |
| SummaryBlock::new(Role::User, vec![Block::content("Now update it")]), | |
| SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Updating"), | |
| SummaryToolCall::update("/test/file1.rs") | |
| .id("call_2") | |
| .into(), | |
| Block::content("Done"), | |
| ], | |
| ), | |
| ]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_ignores_image_messages() { | |
| let fixture = context(vec![ | |
| user("User message"), | |
| ContextMessage::Image(crate::Image::new_base64( | |
| "test_image_data".to_string(), | |
| "image/png", | |
| )), | |
| assistant("Assistant"), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![ | |
| SummaryBlock::new(Role::User, vec![Block::content("User message")]), | |
| SummaryBlock::new(Role::Assistant, vec![Block::content("Assistant")]), | |
| ]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_extract_tool_info_with_mcp_tool() { | |
| let fixture = ToolCallFull { | |
| name: ToolName::new("mcp_github_create_issue"), | |
| call_id: Some(ToolCallId::new("call_1")), | |
| arguments: ToolCallArguments::from_json(r#"{"title": "Bug report"}"#), | |
| thought_signature: None, | |
| }; | |
| let actual = extract_tool_info(&fixture, &[]); | |
| assert_eq!( | |
| actual, | |
| Some(SummaryTool::Mcp { name: "mcp_github_create_issue".to_string() }) | |
| ); | |
| } | |
| fn test_summary_message_block_shell_helper() { | |
| let actual: SummaryMessage = SummaryToolCall::shell("cargo build").into(); | |
| let expected = Block::ToolCall(SummaryToolCall { | |
| id: None, | |
| tool: SummaryTool::Shell { command: "cargo build".to_string() }, | |
| is_success: true, | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_links_shell_results_to_calls() { | |
| let fixture = context(vec![ | |
| assistant_with_tools( | |
| "Running command", | |
| vec![ToolCatalog::tool_call_shell("echo test", "/test").call_id("call_1")], | |
| ), | |
| tool_result("shell", "call_1", false), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Running command"), | |
| SummaryToolCall::shell("echo test").id("call_1").into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_mixed_file_and_shell_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Multiple operations", | |
| vec![ | |
| ToolCatalog::tool_call_read("/test/file.rs").call_id("call_1"), | |
| ToolCatalog::tool_call_shell("cargo test", "/test").call_id("call_2"), | |
| ToolCatalog::tool_call_write("/test/output.txt", "result").call_id("call_3"), | |
| ], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Multiple operations"), | |
| SummaryToolCall::read("/test/file.rs") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::shell("cargo test") | |
| .id("call_2") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::update("/test/output.txt") | |
| .id("call_3") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_ignores_non_file_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Searching", | |
| vec![ToolCallFull { | |
| name: ToolName::new("fs_search"), | |
| call_id: Some(ToolCallId::new("call_1")), | |
| arguments: ToolCallArguments::from_json( | |
| r#"{"path": "/test", "pattern": "pattern"}"#, | |
| ), | |
| thought_signature: None, | |
| }], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Searching"), | |
| SummaryToolCall::search("pattern") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_summary_message_block_search_helper() { | |
| let actual: SummaryMessage = SummaryToolCall::search("/project/src").into(); | |
| let expected = Block::ToolCall(SummaryToolCall { | |
| id: None, | |
| tool: SummaryTool::Search { pattern: "/project/src".to_string() }, | |
| is_success: true, | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_extracts_search_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Searching files", | |
| vec![ToolCatalog::tool_call_search("/test", "/test/src").call_id("call_1")], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Searching files"), | |
| SummaryToolCall::search("/test/src") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_extracts_codebase_search_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Searching codebase", | |
| vec![ | |
| ToolCatalog::tool_call_semantic_search(vec![SearchQuery::new( | |
| "retry mechanism", | |
| "find retry logic", | |
| )]) | |
| .call_id("call_1"), | |
| ], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Searching codebase"), | |
| SummaryToolCall::codebase_search(vec![SearchQuery::new( | |
| "retry mechanism", | |
| "find retry logic", | |
| )]) | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_links_search_results_to_calls() { | |
| let fixture = context(vec![ | |
| assistant_with_tools( | |
| "Searching", | |
| vec![ToolCatalog::tool_call_search("/test", "/test/src").call_id("call_1")], | |
| ), | |
| tool_result("search", "call_1", false), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Searching"), | |
| SummaryToolCall::search("/test/src").id("call_1").into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_mixed_file_shell_and_search_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Multiple operations", | |
| vec![ | |
| ToolCatalog::tool_call_read("/test/file.rs").call_id("call_1"), | |
| ToolCatalog::tool_call_shell("cargo test", "/test").call_id("call_2"), | |
| ToolCatalog::tool_call_search("/test", "/test/src").call_id("call_3"), | |
| ToolCatalog::tool_call_write("/test/output.txt", "result").call_id("call_4"), | |
| ], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Multiple operations"), | |
| SummaryToolCall::read("/test/file.rs") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::shell("cargo test") | |
| .id("call_2") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::search("/test/src") | |
| .id("call_3") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::update("/test/output.txt") | |
| .id("call_4") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_summary_message_block_undo_helper() { | |
| let actual: SummaryMessage = SummaryToolCall::undo("/test/file.rs").into(); | |
| let expected = Block::ToolCall(SummaryToolCall { | |
| id: None, | |
| tool: SummaryTool::Undo { path: "/test/file.rs".to_string() }, | |
| is_success: true, | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_summary_message_block_fetch_helper() { | |
| let actual: SummaryMessage = SummaryToolCall::fetch("https://example.com").into(); | |
| let expected = Block::ToolCall(SummaryToolCall { | |
| id: None, | |
| tool: SummaryTool::Fetch { url: "https://example.com".to_string() }, | |
| is_success: true, | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_summary_message_block_followup_helper() { | |
| let actual: SummaryMessage = SummaryToolCall::followup("What should I do next?").into(); | |
| let expected = Block::ToolCall(SummaryToolCall { | |
| id: None, | |
| tool: SummaryTool::Followup { question: "What should I do next?".to_string() }, | |
| is_success: true, | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_summary_message_block_plan_helper() { | |
| let actual: SummaryMessage = SummaryToolCall::plan("feature-implementation").into(); | |
| let expected = Block::ToolCall(SummaryToolCall { | |
| id: None, | |
| tool: SummaryTool::Plan { plan_name: "feature-implementation".to_string() }, | |
| is_success: true, | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_extracts_undo_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Undoing changes", | |
| vec![ToolCatalog::tool_call_undo("/test/file.rs").call_id("call_1")], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Undoing changes"), | |
| SummaryToolCall::undo("/test/file.rs") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_extracts_fetch_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Fetching data", | |
| vec![ToolCatalog::tool_call_fetch("https://api.example.com").call_id("call_1")], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Fetching data"), | |
| SummaryToolCall::fetch("https://api.example.com") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_extracts_followup_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Asking question", | |
| vec![ToolCatalog::tool_call_followup("Should I proceed?").call_id("call_1")], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Asking question"), | |
| SummaryToolCall::followup("Should I proceed?") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_extracts_plan_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Creating plan", | |
| vec![ToolCatalog::tool_call_plan("feature-plan", "v1", "test").call_id("call_1")], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Creating plan"), | |
| SummaryToolCall::plan("feature-plan") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_links_undo_results_to_calls() { | |
| let fixture = context(vec![ | |
| assistant_with_tools( | |
| "Undoing", | |
| vec![ToolCatalog::tool_call_undo("/test/file.rs").call_id("call_1")], | |
| ), | |
| tool_result("undo", "call_1", false), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Undoing"), | |
| SummaryToolCall::undo("/test/file.rs").id("call_1").into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_links_fetch_results_to_calls() { | |
| let fixture = context(vec![ | |
| assistant_with_tools( | |
| "Fetching", | |
| vec![ToolCatalog::tool_call_fetch("https://example.com").call_id("call_1")], | |
| ), | |
| tool_result("fetch", "call_1", false), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Fetching"), | |
| SummaryToolCall::fetch("https://example.com") | |
| .id("call_1") | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_links_followup_results_to_calls() { | |
| let fixture = context(vec![ | |
| assistant_with_tools( | |
| "Asking", | |
| vec![ToolCatalog::tool_call_followup("Continue?").call_id("call_1")], | |
| ), | |
| tool_result("followup", "call_1", false), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Asking"), | |
| SummaryToolCall::followup("Continue?").id("call_1").into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_links_plan_results_to_calls() { | |
| let fixture = context(vec![ | |
| assistant_with_tools( | |
| "Planning", | |
| vec![ToolCatalog::tool_call_plan("my-plan", "v1", "test").call_id("call_1")], | |
| ), | |
| tool_result("plan", "call_1", false), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Planning"), | |
| SummaryToolCall::plan("my-plan").id("call_1").into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_all_tools_mixed() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "All operations", | |
| vec![ | |
| ToolCatalog::tool_call_read("/test/file.rs").call_id("call_1"), | |
| ToolCatalog::tool_call_write("/test/output.txt", "content").call_id("call_2"), | |
| ToolCatalog::tool_call_remove("/test/old.txt").call_id("call_3"), | |
| ToolCatalog::tool_call_shell("cargo build", "/test").call_id("call_4"), | |
| ToolCatalog::tool_call_search("/test", "/test/src").call_id("call_5"), | |
| ToolCatalog::tool_call_undo("/test/undo.txt").call_id("call_6"), | |
| ToolCatalog::tool_call_fetch("https://example.com").call_id("call_7"), | |
| ToolCatalog::tool_call_followup("Proceed?").call_id("call_8"), | |
| ToolCatalog::tool_call_plan("implementation", "v1", "test").call_id("call_9"), | |
| ], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("All operations"), | |
| SummaryToolCall::read("/test/file.rs") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::update("/test/output.txt") | |
| .id("call_2") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::remove("/test/old.txt") | |
| .id("call_3") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::shell("cargo build") | |
| .id("call_4") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::search("/test/src") | |
| .id("call_5") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::undo("/test/undo.txt") | |
| .id("call_6") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::fetch("https://example.com") | |
| .id("call_7") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::followup("Proceed?") | |
| .id("call_8") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::plan("implementation") | |
| .id("call_9") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_summary_message_block_mcp_helper() { | |
| let actual: SummaryMessage = SummaryToolCall::mcp("mcp_github_create_issue").into(); | |
| let expected = Block::ToolCall(SummaryToolCall { | |
| id: None, | |
| tool: SummaryTool::Mcp { name: "mcp_github_create_issue".to_string() }, | |
| is_success: true, | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_extracts_mcp_tool_calls() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Creating GitHub issue", | |
| vec![ToolCallFull { | |
| name: ToolName::new("mcp_github_create_issue"), | |
| call_id: Some(ToolCallId::new("call_1")), | |
| arguments: ToolCallArguments::from_json( | |
| r#"{"title": "Bug report", "body": "Description"}"#, | |
| ), | |
| thought_signature: None, | |
| }], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Creating GitHub issue"), | |
| SummaryToolCall::mcp("mcp_github_create_issue") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_links_mcp_results_to_calls() { | |
| let fixture = context(vec![ | |
| assistant_with_tools( | |
| "Creating issue", | |
| vec![ToolCallFull { | |
| name: ToolName::new("mcp_github_create_issue"), | |
| call_id: Some(ToolCallId::new("call_1")), | |
| arguments: ToolCallArguments::from_json(r#"{"title": "Bug"}"#), | |
| thought_signature: None, | |
| }], | |
| ), | |
| tool_result("mcp_github_create_issue", "call_1", false), | |
| ]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Creating issue"), | |
| SummaryToolCall::mcp("mcp_github_create_issue") | |
| .id("call_1") | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_multiple_mcp_tools() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Multiple MCP operations", | |
| vec![ | |
| ToolCallFull { | |
| name: ToolName::new("mcp_github_create_issue"), | |
| call_id: Some(ToolCallId::new("call_1")), | |
| arguments: ToolCallArguments::from_json(r#"{"title": "Bug"}"#), | |
| thought_signature: None, | |
| }, | |
| ToolCallFull { | |
| name: ToolName::new("mcp_slack_post_message"), | |
| call_id: Some(ToolCallId::new("call_2")), | |
| arguments: ToolCallArguments::from_json( | |
| r##"{"channel": "#dev", "text": "Hello"}"##, | |
| ), | |
| thought_signature: None, | |
| }, | |
| ], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Multiple MCP operations"), | |
| SummaryToolCall::mcp("mcp_github_create_issue") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::mcp("mcp_slack_post_message") | |
| .id("call_2") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_context_summary_mixed_system_and_mcp_tools() { | |
| let fixture = context(vec![assistant_with_tools( | |
| "Mixed operations", | |
| vec![ | |
| ToolCatalog::tool_call_read("/test/file.rs").call_id("call_1"), | |
| ToolCallFull { | |
| name: ToolName::new("mcp_github_create_issue"), | |
| call_id: Some(ToolCallId::new("call_2")), | |
| arguments: ToolCallArguments::from_json(r#"{"title": "Bug"}"#), | |
| thought_signature: None, | |
| }, | |
| ToolCatalog::tool_call_write("/test/output.txt", "result").call_id("call_3"), | |
| ], | |
| )]); | |
| let actual = ContextSummary::from(&fixture); | |
| let expected = ContextSummary::new(vec![SummaryBlock::new( | |
| Role::Assistant, | |
| vec![ | |
| Block::content("Mixed operations"), | |
| SummaryToolCall::read("/test/file.rs") | |
| .id("call_1") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::mcp("mcp_github_create_issue") | |
| .id("call_2") | |
| .is_success(false) | |
| .into(), | |
| SummaryToolCall::update("/test/output.txt") | |
| .id("call_3") | |
| .is_success(false) | |
| .into(), | |
| ], | |
| )]); | |
| assert_eq!(actual, expected); | |
| } | |
| } | |