Files
galaxy/app/src/ai/agent/api/convert_conversation_tests.rs
T

1982 lines
75 KiB
Rust

use crate::ai::agent::api::convert_conversation::*;
use crate::ai::agent::{AIAgentInput, UserQueryMode};
use std::collections::HashMap;
use warp_multi_agent_api as api;
fn test_skill() -> api::Skill {
api::Skill {
descriptor: Some(api::SkillDescriptor {
skill_reference: Some(api::skill_descriptor::SkillReference::Path(
"/tmp/test-skill.md".to_string(),
)),
name: "test-skill".to_string(),
description: "A test skill".to_string(),
scope: Some(api::skill_descriptor::Scope {
r#type: Some(api::skill_descriptor::scope::Type::Project(())),
}),
provider: Some(api::skill_descriptor::Provider {
r#type: Some(api::skill_descriptor::provider::Type::Warp(())),
}),
}),
content: Some(api::FileContent {
file_path: "/tmp/test-skill.md".to_string(),
content: "Do the thing".to_string(),
line_range: None,
}),
}
}
#[test]
fn test_convert_tool_call_result_to_input_transfer_control_snapshot() {
let task_id = crate::ai::agent::task::TaskId::new("task".to_string());
let mut document_versions = HashMap::new();
let tool_call_result = api::message::ToolCallResult {
tool_call_id: "tool_call".to_string(),
context: None,
result: Some(
api::message::tool_call_result::Result::TransferShellCommandControlToUser(
api::TransferShellCommandControlToUserResult {
result: Some(
api::transfer_shell_command_control_to_user_result::Result::LongRunningCommandSnapshot(
api::LongRunningShellCommandSnapshot {
command_id: "cmd1".to_string(),
output: "snapshot".to_string(),
cursor: "<|cursor|>".to_string(),
is_alt_screen_active: false,
is_preempted: false,
},
),
),
},
),
),
};
let input = convert_tool_call_result_to_input(
&task_id,
&tool_call_result,
&HashMap::new(),
&mut document_versions,
)
.unwrap();
match input {
AIAgentInput::ActionResult { result, .. } => match result.result {
crate::ai::agent::AIAgentActionResultType::TransferShellCommandControlToUser(
crate::ai::agent::TransferShellCommandControlToUserResult::Snapshot {
block_id,
grid_contents,
cursor,
..
},
) => {
assert_eq!(block_id.to_string(), "cmd1");
assert_eq!(grid_contents, "snapshot");
assert_eq!(cursor, "<|cursor|>");
}
other => panic!("Expected transfer-control snapshot result, got {other:?}"),
},
other => panic!("Expected action-result input, got {other:?}"),
}
}
#[test]
fn test_convert_tool_call_result_to_input_upload_artifact_success() {
let task_id = crate::ai::agent::task::TaskId::new("task".to_string());
let mut document_versions = HashMap::new();
let tool_call_result = api::message::ToolCallResult {
tool_call_id: "tool_call".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::UploadFileArtifact(
api::UploadFileArtifactResult {
result: Some(api::upload_file_artifact_result::Result::Success(
api::upload_file_artifact_result::Success {
artifact_uid: "artifact-123".to_string(),
mime_type: "text/plain".to_string(),
size_bytes: 42,
},
)),
},
)),
};
let input = convert_tool_call_result_to_input(
&task_id,
&tool_call_result,
&HashMap::new(),
&mut document_versions,
)
.unwrap();
match input {
AIAgentInput::ActionResult { result, .. } => match result.result {
crate::ai::agent::AIAgentActionResultType::UploadArtifact(
crate::ai::agent::UploadArtifactResult::Success {
artifact_uid,
filepath,
mime_type,
description,
size_bytes,
},
) => {
assert_eq!(artifact_uid, "artifact-123");
assert_eq!(filepath, None);
assert_eq!(mime_type, "text/plain");
assert_eq!(description, None);
assert_eq!(size_bytes, 42);
}
other => panic!("Expected upload-artifact success result, got {other:?}"),
},
other => panic!("Expected action-result input, got {other:?}"),
}
}
#[test]
fn test_convert_tool_call_result_to_input_upload_artifact_missing_result_is_error() {
let task_id = crate::ai::agent::task::TaskId::new("task".to_string());
let mut document_versions = HashMap::new();
let tool_call_result = api::message::ToolCallResult {
tool_call_id: "tool_call".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::UploadFileArtifact(
api::UploadFileArtifactResult { result: None },
)),
};
let input = convert_tool_call_result_to_input(
&task_id,
&tool_call_result,
&HashMap::new(),
&mut document_versions,
)
.unwrap();
match input {
AIAgentInput::ActionResult { result, .. } => match result.result {
crate::ai::agent::AIAgentActionResultType::UploadArtifact(
crate::ai::agent::UploadArtifactResult::Error(message),
) => {
assert_eq!(message, "Upload artifact tool call returned no result");
}
other => panic!("Expected upload-artifact error result, got {other:?}"),
},
other => panic!("Expected action-result input, got {other:?}"),
}
}
#[test]
fn test_convert_tool_call_result_to_input_start_agent_v2_results() {
let task_id = crate::ai::agent::task::TaskId::new("task".to_string());
let cases = [
(
"success",
Some(api::start_agent_v2_result::Result::Success(
api::start_agent_v2_result::Success {
agent_id: "agent-123".to_string(),
},
)),
),
(
"error",
Some(api::start_agent_v2_result::Result::Error(
api::start_agent_v2_result::Error {
error: "child failed".to_string(),
},
)),
),
("cancelled", None),
];
for (name, result) in cases {
let mut document_versions = HashMap::new();
let tool_call_result = api::message::ToolCallResult {
tool_call_id: format!("tool_call_{name}"),
context: None,
result: Some(api::message::tool_call_result::Result::StartAgentV2(
api::StartAgentV2Result { result },
)),
};
let input = convert_tool_call_result_to_input(
&task_id,
&tool_call_result,
&HashMap::new(),
&mut document_versions,
)
.unwrap();
match input {
AIAgentInput::ActionResult { result, .. } => match result.result {
crate::ai::agent::AIAgentActionResultType::StartAgent(
crate::ai::agent::StartAgentResult::Success { agent_id, version },
) if name == "success" => {
assert_eq!(agent_id, "agent-123");
assert_eq!(version, ai::agent::action_result::StartAgentVersion::V2);
}
crate::ai::agent::AIAgentActionResultType::StartAgent(
crate::ai::agent::StartAgentResult::Error { error, version },
) if name == "error" => {
assert_eq!(error, "child failed");
assert_eq!(version, ai::agent::action_result::StartAgentVersion::V2);
}
crate::ai::agent::AIAgentActionResultType::StartAgent(
crate::ai::agent::StartAgentResult::Cancelled { version },
) if name == "cancelled" => {
assert_eq!(version, ai::agent::action_result::StartAgentVersion::V2);
}
other => panic!("Unexpected start-agent-v2 result for {name}: {other:?}"),
},
other => panic!("Expected action-result input for {name}, got {other:?}"),
}
}
}
#[test]
fn test_convert_tool_call_result_to_input_transfer_control_cancelled() {
let task_id = crate::ai::agent::task::TaskId::new("task".to_string());
let mut document_versions = HashMap::new();
let original_tool_call = api::message::ToolCall {
tool_call_id: "tool_call".to_string(),
tool: Some(
api::message::tool_call::Tool::TransferShellCommandControlToUser(
api::message::tool_call::TransferShellCommandControlToUser {
reason: "Need user help".to_string(),
},
),
),
};
let mut tool_call_map = HashMap::new();
tool_call_map.insert("tool_call".to_string(), &original_tool_call);
let tool_call_result = api::message::ToolCallResult {
tool_call_id: "tool_call".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::Cancel(())),
};
let input = convert_tool_call_result_to_input(
&task_id,
&tool_call_result,
&tool_call_map,
&mut document_versions,
)
.unwrap();
match input {
AIAgentInput::ActionResult { result, .. } => match result.result {
crate::ai::agent::AIAgentActionResultType::TransferShellCommandControlToUser(
crate::ai::agent::TransferShellCommandControlToUserResult::Cancelled,
) => {}
other => panic!("Expected cancelled transfer-control result, got {other:?}"),
},
other => panic!("Expected action-result input, got {other:?}"),
}
}
#[test]
fn test_into_exchanges_basic() {
// Create minimal test data
let messages = vec![
api::Message {
id: "user_msg".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "test query".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: None,
intended_agent: Default::default(),
})),
request_id: "req1".to_string(),
timestamp: None,
},
api::Message {
id: "agent_msg".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "test response".to_string(),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
api::Message {
id: "user_msg2".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "second query".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: None,
intended_agent: Default::default(),
})),
request_id: "req2".to_string(),
timestamp: None,
},
api::Message {
id: "agent_msg2".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "second response".to_string(),
},
)),
request_id: "req2".to_string(),
timestamp: None,
},
api::Message {
id: "user_msg3".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "third query".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: None,
intended_agent: Default::default(),
})),
request_id: "req3".to_string(),
timestamp: None,
},
api::Message {
id: "agent_msg3".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "third response".to_string(),
},
)),
request_id: "req3".to_string(),
timestamp: None,
},
];
let task = api::Task {
id: "task1".to_string(),
messages,
dependencies: None,
description: "".to_string(),
summary: "".to_string(),
server_data: "".to_string(),
};
// Test the function
let exchanges = task.into_exchanges();
// We expect 3 exchanges (one for each user query + agent response pair)
assert_eq!(exchanges.len(), 3, "Should create 3 exchanges");
// Each exchange should have exactly one input (the user query)
for exchange in &exchanges {
assert_eq!(
exchange.input.len(),
1,
"Each exchange should have one input"
);
}
}
#[test]
fn test_invoke_skill_arguments_round_trip() {
let query = "arg1 arg2".to_string();
let messages = vec![
api::Message {
id: "invoke_skill_msg".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::InvokeSkill(
api::message::InvokeSkill {
skill: Some(test_skill()),
user_query: Some(api::message::UserQuery {
query: query.clone(),
context: None,
referenced_attachments: HashMap::new(),
mode: None,
intended_agent: Default::default(),
}),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
api::Message {
id: "agent_msg".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "Done".to_string(),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
];
let task = api::Task {
id: "task1".to_string(),
messages,
dependencies: None,
description: "".to_string(),
summary: "".to_string(),
server_data: "".to_string(),
};
let exchanges = task.into_exchanges();
assert_eq!(exchanges.len(), 1);
match &exchanges[0].input[0] {
AIAgentInput::InvokeSkill {
skill, user_query, ..
} => {
assert_eq!(skill.name, "test-skill");
assert_eq!(
user_query.as_ref().map(|uq| uq.query.as_str()),
Some("arg1 arg2")
);
assert_eq!(
exchanges[0].input[0].user_query().as_deref(),
Some("/test-skill arg1 arg2")
);
}
input => panic!("Expected InvokeSkill input, got {input:?}"),
}
}
#[test]
fn test_invoke_skill_missing_user_query_maps_to_none() {
let messages = vec![api::Message {
id: "invoke_skill_msg".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::InvokeSkill(
api::message::InvokeSkill {
skill: Some(test_skill()),
user_query: None,
},
)),
request_id: "req1".to_string(),
timestamp: None,
}];
let task = api::Task {
id: "task1".to_string(),
messages,
dependencies: None,
description: "".to_string(),
summary: "".to_string(),
server_data: "".to_string(),
};
let exchanges = task.into_exchanges();
assert_eq!(exchanges.len(), 1);
match &exchanges[0].input[0] {
AIAgentInput::InvokeSkill {
skill, user_query, ..
} => {
assert_eq!(skill.name, "test-skill");
assert_eq!(user_query, &None);
assert_eq!(
exchanges[0].input[0].user_query().as_deref(),
Some("/test-skill")
);
}
input => panic!("Expected InvokeSkill input, got {input:?}"),
}
}
#[test]
fn test_into_exchanges_with_tool_calls_and_cancellation() {
let messages = vec![
// User query
api::Message {
id: "user_query".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "run parallel commands".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: None,
intended_agent: Default::default(),
})),
request_id: "req1".to_string(),
timestamp: None,
},
// Agent response
api::Message {
id: "agent_response".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "Running commands in parallel".to_string(),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
// Tool call 1
api::Message {
id: "tool_call_1".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
tool_call_id: "call_1".to_string(),
tool: Some(api::message::tool_call::Tool::RunShellCommand(
api::message::tool_call::RunShellCommand {
command: "echo 1".to_string(),
is_read_only: false,
uses_pager: false,
citations: vec![],
is_risky: false,
wait_until_complete_value: None,
risk_category: 0,
},
)),
})),
request_id: "req1".to_string(),
timestamp: None,
},
// Tool call 2
api::Message {
id: "tool_call_2".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
tool_call_id: "call_2".to_string(),
tool: Some(api::message::tool_call::Tool::RunShellCommand(
api::message::tool_call::RunShellCommand {
command: "echo 2".to_string(),
is_read_only: false,
uses_pager: false,
citations: vec![],
is_risky: false,
wait_until_complete_value: None,
risk_category: 0,
},
)),
})),
request_id: "req1".to_string(),
timestamp: None,
},
// Tool call 3
api::Message {
id: "tool_call_3".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
tool_call_id: "call_3".to_string(),
tool: Some(api::message::tool_call::Tool::RunShellCommand(
api::message::tool_call::RunShellCommand {
command: "echo 3".to_string(),
is_read_only: false,
uses_pager: false,
citations: vec![],
is_risky: false,
wait_until_complete_value: None,
risk_category: 0,
},
)),
})),
request_id: "req1".to_string(),
timestamp: None,
},
// Tool call result - cancelled (call_2)
api::Message {
id: "result_cancelled".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: "call_2".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::Cancel(())),
},
)),
request_id: "req2".to_string(),
timestamp: None,
},
// Tool call result - success (call_1)
api::Message {
id: "result_success_1".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: "call_1".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::RunShellCommand(
#[allow(deprecated)]
api::RunShellCommandResult {
command: "echo 1".to_string(),
output: Default::default(),
exit_code: Default::default(),
result: Some(api::run_shell_command_result::Result::CommandFinished(
api::ShellCommandFinished {
command_id: "command_1".to_string(),
output: "1".to_string(),
exit_code: 0,
},
)),
},
)),
},
)),
request_id: "req2".to_string(),
timestamp: None,
},
// Tool call result - success (call_3)
api::Message {
id: "result_success_3".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: "call_3".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::RunShellCommand(
#[allow(deprecated)]
api::RunShellCommandResult {
command: "echo 3".to_string(),
output: "".to_string(),
exit_code: 0,
result: Some(api::run_shell_command_result::Result::CommandFinished(
api::ShellCommandFinished {
command_id: "command_2".to_string(),
output: "3".to_string(),
exit_code: 0,
},
)),
},
)),
},
)),
request_id: "req2".to_string(),
timestamp: None,
},
// Final agent response
api::Message {
id: "final_response".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "Commands completed - 2 succeeded, 1 cancelled".to_string(),
},
)),
request_id: "req2".to_string(),
timestamp: None,
},
// Follow-up user query
api::Message {
id: "followup_query".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "did it work?".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: None,
intended_agent: Default::default(),
})),
request_id: "req3".to_string(),
timestamp: None,
},
// Final agent response
api::Message {
id: "final_response2".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "Yes, partially worked".to_string(),
},
)),
request_id: "req3".to_string(),
timestamp: None,
},
];
let task = api::Task {
id: "task1".to_string(),
messages,
dependencies: None,
description: "".to_string(),
summary: "".to_string(),
server_data: "".to_string(),
};
// Expected 3 persisted blocks based on the pattern
let expected_persisted_block_count = 3;
// Test the function
let exchanges = task.into_exchanges();
// Verify we get the expected number of exchanges
assert_eq!(
exchanges.len(),
expected_persisted_block_count,
"Should create {expected_persisted_block_count} exchanges to match persisted blocks"
);
// First exchange: user query + agent output + tool calls
let first_exchange = &exchanges[0];
assert_eq!(
first_exchange.input.len(),
1,
"First exchange should have 1 input (user query)"
);
// Second exchange: action results + agent response
let second_exchange = &exchanges[1];
let action_result_count = second_exchange
.input
.iter()
.filter(|input| matches!(input, crate::ai::agent::AIAgentInput::ActionResult { .. }))
.count();
assert_eq!(
action_result_count, 3,
"Second exchange should have 3 action results"
);
// Third exchange: follow-up query + response
let third_exchange = &exchanges[2];
assert_eq!(
third_exchange.input.len(),
1,
"Third exchange should have 1 input (follow-up query)"
);
// Verify tool call results include both successful and cancelled
let mut found_cancelled = false;
let mut found_successful = 0;
for input in &second_exchange.input {
if let crate::ai::agent::AIAgentInput::ActionResult { result, .. } = input {
if let crate::ai::agent::AIAgentActionResultType::RequestCommandOutput(command_result) =
&result.result
{
match command_result {
crate::ai::agent::RequestCommandOutputResult::CancelledBeforeExecution => {
found_cancelled = true;
}
crate::ai::agent::RequestCommandOutputResult::Completed { .. } => {
found_successful += 1;
}
_ => {}
}
}
}
}
assert!(
found_cancelled,
"Should find at least one cancelled tool call result"
);
assert_eq!(
found_successful, 2,
"Should find exactly 2 successful tool call results"
);
}
#[test]
fn test_into_exchanges_with_code_diffs() {
let messages = vec![
// User query asking for code changes
api::Message {
id: "user_query".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "Fix the imports in main.rs".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: None,
intended_agent: Default::default(),
})),
request_id: "req1".to_string(),
timestamp: None,
},
// Agent response
api::Message {
id: "agent_response".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "I'll fix the import issues".to_string(),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
// File diff tool call
api::Message {
id: "diff_call".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
tool_call_id: "diff_1".to_string(),
tool: Some(api::message::tool_call::Tool::ApplyFileDiffs(
api::message::tool_call::ApplyFileDiffs {
summary: "".to_string(),
new_files: vec![],
diffs: vec![],
v4a_updates: vec![],
deleted_files: vec![],
},
)),
})),
request_id: "req1".to_string(),
timestamp: None,
},
// User cancels the diff
api::Message {
id: "diff_cancelled".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: "diff_1".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::Cancel(())),
},
)),
request_id: "req2".to_string(),
timestamp: None,
},
// User provides feedback
api::Message {
id: "user_feedback".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "Actually, let's remove the unused import instead".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: None,
intended_agent: Default::default(),
})),
request_id: "req2".to_string(),
timestamp: None,
},
// Agent response
api::Message {
id: "agent_response_2".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "I'll remove the unused import".to_string(),
},
)),
request_id: "req2".to_string(),
timestamp: None,
},
// Second file diff tool call
api::Message {
id: "diff_call_2".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
tool_call_id: "diff_2".to_string(),
tool: Some(api::message::tool_call::Tool::ApplyFileDiffs(
api::message::tool_call::ApplyFileDiffs {
summary: "".to_string(),
new_files: vec![],
diffs: vec![],
v4a_updates: vec![],
deleted_files: vec![],
},
)),
})),
request_id: "req2".to_string(),
timestamp: None,
},
// User accepts the diff
api::Message {
id: "diff_accepted".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: "diff_2".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::ApplyFileDiffs(
api::ApplyFileDiffsResult {
result: Some(api::apply_file_diffs_result::Result::Success(
#[allow(deprecated)]
api::apply_file_diffs_result::Success {
updated_files_v2: vec![],
updated_files: vec![],
deleted_files: vec![],
},
)),
},
)),
},
)),
request_id: "req3".to_string(),
timestamp: None,
},
// Final agent response
api::Message {
id: "final_response".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "Fixed! Removed the unused import.".to_string(),
},
)),
request_id: "req3".to_string(),
timestamp: None,
},
// Follow-up user query
api::Message {
id: "followup".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "Great! Now does it compile?".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: None,
intended_agent: Default::default(),
})),
request_id: "req4".to_string(),
timestamp: None,
},
// Final agent response
api::Message {
id: "final_response_2".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "Yes, it should compile cleanly now!".to_string(),
},
)),
request_id: "req4".to_string(),
timestamp: None,
},
];
let task = api::Task {
id: "task1".to_string(),
messages,
dependencies: None,
description: "".to_string(),
summary: "".to_string(),
server_data: "".to_string(),
};
// Expected 4 persisted blocks: initial query+diff, cancelled diff feedback, accepted diff, followup
let expected_persisted_block_count = 4;
// Test the function
let exchanges = task.into_exchanges();
// Verify we get the expected number of exchanges
assert_eq!(
exchanges.len(),
expected_persisted_block_count,
"Should create {expected_persisted_block_count} exchanges to match persisted blocks"
);
// Verify the first exchange has a user query
let first_exchange = &exchanges[0];
match &first_exchange.input[0] {
crate::ai::agent::AIAgentInput::UserQuery { query, .. } => {
assert_eq!(query, "Fix the imports in main.rs");
}
_ => panic!("First exchange should start with user query"),
}
// Verify the second exchange has a cancelled file diff result
let second_exchange = &exchanges[1];
let has_cancelled_diff = second_exchange.input.iter().any(|input| {
if let crate::ai::agent::AIAgentInput::ActionResult { result, .. } = input {
matches!(
result.result,
crate::ai::agent::AIAgentActionResultType::RequestFileEdits(
crate::ai::agent::RequestFileEditsResult::Cancelled
)
)
} else {
false
}
});
assert!(
has_cancelled_diff,
"Should have cancelled diff in second exchange"
);
// Verify the third exchange has a successful file diff result
let third_exchange = &exchanges[2];
let has_successful_diff = third_exchange.input.iter().any(|input| {
if let crate::ai::agent::AIAgentInput::ActionResult { result, .. } = input {
matches!(
result.result,
crate::ai::agent::AIAgentActionResultType::RequestFileEdits(
crate::ai::agent::RequestFileEditsResult::Success { .. }
)
)
} else {
false
}
});
assert!(
has_successful_diff,
"Should have successful diff in third exchange"
);
// Verify the fourth exchange is the follow-up query
let fourth_exchange = &exchanges[3];
match &fourth_exchange.input[0] {
crate::ai::agent::AIAgentInput::UserQuery { query, .. } => {
assert_eq!(query, "Great! Now does it compile?");
}
_ => panic!("Fourth exchange should start with follow-up query"),
}
}
#[test]
fn test_user_query_mode_conversion() {
// Test conversion with Plan mode
let messages = vec![api::Message {
id: "user_msg".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "test query with plan mode".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: Some(api::UserQueryMode {
r#type: Some(api::user_query_mode::Type::Plan(())),
}),
intended_agent: Default::default(),
})),
request_id: String::new(),
timestamp: None,
}];
let task = api::Task {
id: "task1".to_string(),
messages,
dependencies: None,
description: "".to_string(),
summary: "".to_string(),
server_data: "".to_string(),
};
let exchanges = task.into_exchanges();
assert_eq!(exchanges.len(), 1);
match &exchanges[0].input[0] {
AIAgentInput::UserQuery {
user_query_mode: UserQueryMode::Plan,
..
} => {
// Success - the mode was correctly converted
}
AIAgentInput::UserQuery {
user_query_mode, ..
} => {
panic!("Expected Plan mode, got: {user_query_mode:?}");
}
_ => panic!("Expected UserQuery input"),
}
// Test conversion with Normal mode (no type set)
let messages_normal = vec![api::Message {
id: "user_msg".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "test query with normal mode".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: Some(api::UserQueryMode { r#type: None }),
intended_agent: Default::default(),
})),
request_id: String::new(),
timestamp: None,
}];
let task_normal = api::Task {
id: "task1".to_string(),
messages: messages_normal,
dependencies: None,
description: "".to_string(),
summary: "".to_string(),
server_data: "".to_string(),
};
let exchanges_normal = task_normal.into_exchanges();
assert_eq!(exchanges_normal.len(), 1);
match &exchanges_normal[0].input[0] {
AIAgentInput::UserQuery {
user_query_mode: UserQueryMode::Normal,
..
} => {
// Success - the mode was correctly converted
}
AIAgentInput::UserQuery {
user_query_mode, ..
} => {
panic!("Expected Normal mode, got: {user_query_mode:?}");
}
_ => panic!("Expected UserQuery input"),
}
// Test conversion with no mode field (should default to Normal)
let messages_default = vec![api::Message {
id: "user_msg".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "test query with default mode".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: None,
intended_agent: Default::default(),
})),
request_id: String::new(),
timestamp: None,
}];
let task_default = api::Task {
id: "task1".to_string(),
messages: messages_default,
dependencies: None,
description: "".to_string(),
summary: "".to_string(),
server_data: "".to_string(),
};
let exchanges_default = task_default.into_exchanges();
assert_eq!(exchanges_default.len(), 1);
match &exchanges_default[0].input[0] {
AIAgentInput::UserQuery {
user_query_mode: UserQueryMode::Normal,
..
} => {
// Success - the mode was correctly converted to default (Normal)
}
AIAgentInput::UserQuery {
user_query_mode, ..
} => {
panic!("Expected Normal (default) mode, got: {user_query_mode:?}");
}
_ => panic!("Expected UserQuery input"),
}
}
#[test]
fn test_exchanges_grouped_by_request_id() {
// This test is based on a real example where messages should be grouped by request_id
// Request 1: 78e236b8-84a2-45df-876e-ebfb86ceafc4 (UserQuery + AgentOutput + ToolCall)
// Request 2: 59a3947f-fc7e-413a-96b5-baecd7e406dc (ToolCallResult + ToolCall for subagent)
// Request 3: 9f85acb2-0b1f-41b1-a0de-3623e131758a (Subagent result + Final ToolCallResult + AgentOutput)
let messages = vec![
// Message 0: Server message (should be ignored or handled gracefully)
api::Message {
id: "2512077c-0ede-46b0-8f69-230c8792df07".to_string(),
task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(),
request_id: "78e236b8-84a2-45df-876e-ebfb86ceafc4".to_string(),
timestamp: None,
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
tool_call_id: "39740c13-892c-4f0e-8aa9-6d305d81f174".to_string(),
tool: Some(api::message::tool_call::Tool::Server(
api::message::tool_call::Server {
payload: String::new(),
},
)),
})),
},
// Message 1: User query with request_id 78e236b8
api::Message {
id: "4d6c450d-3d54-446f-974c-5c414e6083e9".to_string(),
task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(),
request_id: "78e236b8-84a2-45df-876e-ebfb86ceafc4".to_string(),
timestamp: None,
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "demonstrate your ability".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: None,
intended_agent: Default::default(),
})),
},
// Message 2: Agent output with same request_id
api::Message {
id: "10210d1a-5298-45ef-90ba-df6367805080".to_string(),
task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(),
request_id: "78e236b8-84a2-45df-876e-ebfb86ceafc4".to_string(),
timestamp: None,
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "I'll demonstrate opening".to_string(),
},
)),
},
// Message 3: Tool call with same request_id
api::Message {
id: "936c7c86-eb4a-4edf-97c0-22f5c61b35a6".to_string(),
task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(),
request_id: "78e236b8-84a2-45df-876e-ebfb86ceafc4".to_string(),
timestamp: None,
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
tool_call_id: "toolu_016dGTLFAPqvxW9yyFcF1WzT".to_string(),
tool: Some(api::message::tool_call::Tool::RunShellCommand(
api::message::tool_call::RunShellCommand {
command: "vim".to_string(),
is_read_only: true,
uses_pager: false,
citations: vec![],
is_risky: false,
wait_until_complete_value: None,
risk_category: 0,
},
)),
})),
},
// Message 4: Tool call result with NEW request_id 59a3947f (starts new exchange)
api::Message {
id: "cbebf5fb-4dd8-4aef-be45-bb916eff552c".to_string(),
task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(),
request_id: "59a3947f-fc7e-413a-96b5-baecd7e406dc".to_string(),
timestamp: None,
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: "toolu_016dGTLFAPqvxW9yyFcF1WzT".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::RunShellCommand(
#[allow(deprecated)]
api::RunShellCommandResult {
command: "vim".to_string(),
output: "".to_string(),
exit_code: 0,
result: Some(
api::run_shell_command_result::Result::LongRunningCommandSnapshot(
api::LongRunningShellCommandSnapshot {
command_id: "cmd1".to_string(),
output: "\n<|cursor|>".to_string(),
cursor: String::new(),
is_alt_screen_active: false,
is_preempted: false,
},
),
),
},
)),
},
)),
},
// Message 5: Agent output with same request_id
api::Message {
id: "7a89857d-fa33-4d45-88e3-5fa9cbce3f20".to_string(),
task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(),
request_id: "59a3947f-fc7e-413a-96b5-baecd7e406dc".to_string(),
timestamp: None,
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "Vim opened in interact mode".to_string(),
},
)),
},
// Message 6: Write to long running command with NEW request_id 9f85acb2 (starts new exchange)
api::Message {
id: "dac6d336-9fcb-4e34-bc2b-b06e70f52ec5".to_string(),
task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(),
request_id: "9f85acb2-0b1f-41b1-a0de-3623e131758a".to_string(),
timestamp: None,
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: "toolu_write_to_vim".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::WriteToLongRunningShellCommand(
api::WriteToLongRunningShellCommandResult {
result: Some(
api::write_to_long_running_shell_command_result::Result::LongRunningCommandSnapshot(
api::LongRunningShellCommandSnapshot {
command_id: "cmd1".to_string(),
output: "wrote to vim".to_string(),
cursor: String::new(),
is_alt_screen_active: false,
is_preempted: false,
},
),
),
},
)),
},
)),
},
// Message 7: Final tool call result with same request_id
api::Message {
id: "ad319d66-fac0-4169-8bf1-e6004aca1619".to_string(),
task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(),
request_id: "9f85acb2-0b1f-41b1-a0de-3623e131758a".to_string(),
timestamp: None,
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: "toolu_016dGTLFAPqvxW9yyFcF1WzT".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::RunShellCommand(
#[allow(deprecated)]
api::RunShellCommandResult {
command: "vim".to_string(),
output: "".to_string(),
exit_code: 0,
result: Some(api::run_shell_command_result::Result::CommandFinished(
api::ShellCommandFinished {
command_id: "cmd1".to_string(),
output: "Done".to_string(),
exit_code: 0,
},
)),
},
)),
},
)),
},
// Message 8: Final agent output with same request_id
api::Message {
id: "f15f8a59-2e9c-416e-b216-83b3bd52d6be".to_string(),
task_id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(),
request_id: "9f85acb2-0b1f-41b1-a0de-3623e131758a".to_string(),
timestamp: None,
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "Done. Vim was opened and closed successfully".to_string(),
},
)),
},
];
let task = api::Task {
id: "d02463e1-2429-48de-ac8f-552df4acc4d0".to_string(),
messages,
dependencies: None,
description: "".to_string(),
summary: "".to_string(),
server_data: "".to_string(),
};
let exchanges = task.into_exchanges();
// We expect 3 exchanges based on the 3 different request_ids
assert_eq!(
exchanges.len(),
3,
"Should create 3 exchanges based on 3 different request_ids"
);
// First exchange: request_id 78e236b8 (UserQuery + outputs)
let first_exchange = &exchanges[0];
assert_eq!(
first_exchange.input.len(),
1,
"First exchange should have 1 input (user query)"
);
assert!(
matches!(first_exchange.input[0], AIAgentInput::UserQuery { .. }),
"First input should be a UserQuery"
);
// Second exchange: request_id 59a3947f (ToolCallResult input)
let second_exchange = &exchanges[1];
assert_eq!(
second_exchange.input.len(),
1,
"Second exchange should have 1 input (tool call result from vim command)"
);
assert!(
matches!(second_exchange.input[0], AIAgentInput::ActionResult { .. }),
"Second input should be an ActionResult"
);
// Third exchange: request_id 9f85acb2 (2 ToolCallResults + output)
let third_exchange = &exchanges[2];
assert_eq!(
third_exchange.input.len(),
2,
"Third exchange should have 2 inputs (write command + final result)"
);
assert!(
matches!(third_exchange.input[0], AIAgentInput::ActionResult { .. }),
"Third exchange first input should be an ActionResult"
);
assert!(
matches!(third_exchange.input[1], AIAgentInput::ActionResult { .. }),
"Third exchange second input should be an ActionResult"
);
}
/// Regression test for APP-3273: Multiple CreateDocuments tool call results should each get
/// the default document version (v1), since each creates a brand-new document.
/// Previously, a global version counter was used, so the second CreateDocuments got v2,
/// causing a version mismatch during restoration and preventing the inline action from rendering.
#[test]
fn test_multiple_create_documents_get_default_version() {
use crate::ai::agent::{AIAgentActionResultType, CreateDocumentsResult};
use crate::ai::document::ai_document_model::AIDocumentVersion;
let doc_id_a = uuid::Uuid::new_v4().to_string();
let doc_id_b = uuid::Uuid::new_v4().to_string();
let messages = vec![
// User query
api::Message {
id: "user_msg".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "create two plans".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: None,
intended_agent: Default::default(),
})),
request_id: "req1".to_string(),
timestamp: None,
},
// Agent output
api::Message {
id: "agent_text".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "Creating plan A".to_string(),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
// First CreateDocuments tool call
api::Message {
id: "tool_call_create_a".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
tool_call_id: "tc_create_a".to_string(),
tool: Some(api::message::tool_call::Tool::CreateDocuments(
api::message::tool_call::CreateDocuments {
new_documents: vec![
api::message::tool_call::create_documents::NewDocument {
title: "Plan A".to_string(),
content: "# Plan A".to_string(),
},
],
},
)),
})),
request_id: "req1".to_string(),
timestamp: None,
},
// First CreateDocuments result
api::Message {
id: "result_create_a".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: "tc_create_a".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::CreateDocuments(
api::CreateDocumentsResult {
result: Some(api::create_documents_result::Result::Success(
api::create_documents_result::Success {
created_documents: vec![api::DocumentContent {
document_id: doc_id_a.clone(),
content: "# Plan A content".to_string(),
line_range: None,
}],
},
)),
},
)),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
// Agent output before second plan
api::Message {
id: "agent_text_2".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "Creating plan B".to_string(),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
// Second CreateDocuments tool call
api::Message {
id: "tool_call_create_b".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
tool_call_id: "tc_create_b".to_string(),
tool: Some(api::message::tool_call::Tool::CreateDocuments(
api::message::tool_call::CreateDocuments {
new_documents: vec![
api::message::tool_call::create_documents::NewDocument {
title: "Plan B".to_string(),
content: "# Plan B".to_string(),
},
],
},
)),
})),
request_id: "req1".to_string(),
timestamp: None,
},
// Second CreateDocuments result
api::Message {
id: "result_create_b".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: "tc_create_b".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::CreateDocuments(
api::CreateDocumentsResult {
result: Some(api::create_documents_result::Result::Success(
api::create_documents_result::Success {
created_documents: vec![api::DocumentContent {
document_id: doc_id_b.clone(),
content: "# Plan B content".to_string(),
line_range: None,
}],
},
)),
},
)),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
];
let task = api::Task {
id: "task1".to_string(),
messages,
dependencies: None,
description: "".to_string(),
summary: "".to_string(),
server_data: "".to_string(),
};
let exchanges = task.into_exchanges();
assert_eq!(exchanges.len(), 1, "Should create 1 exchange");
// Find all CreateDocuments action results and verify their versions
let create_doc_versions: Vec<AIDocumentVersion> = exchanges[0]
.input
.iter()
.filter_map(|input| match input {
AIAgentInput::ActionResult { result, .. } => match &result.result {
AIAgentActionResultType::CreateDocuments(CreateDocumentsResult::Success {
created_documents,
}) => Some(
created_documents
.iter()
.map(|doc| doc.document_version)
.collect::<Vec<_>>(),
),
_ => None,
},
_ => None,
})
.flatten()
.collect();
assert_eq!(
create_doc_versions.len(),
2,
"Should have 2 CreateDocuments results"
);
// Both documents should have the default version since they are newly created.
let default_version = AIDocumentVersion::default();
assert_eq!(
create_doc_versions[0], default_version,
"First created document should have default version"
);
assert_eq!(
create_doc_versions[1], default_version,
"Second created document should also have default version (regression: APP-3273)"
);
}
/// Test that the create-then-edit flow produces correct per-document versions:
/// - Create doc A → v1
/// - Edit doc A → v2 (incremented from v1)
/// - Create doc B → v1 (independent new document)
#[test]
fn test_create_then_edit_then_create_version_tracking() {
use crate::ai::agent::{AIAgentActionResultType, CreateDocumentsResult, EditDocumentsResult};
use crate::ai::document::ai_document_model::AIDocumentVersion;
let doc_id_a = uuid::Uuid::new_v4().to_string();
let doc_id_b = uuid::Uuid::new_v4().to_string();
let messages = vec![
// User query
api::Message {
id: "user_msg".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::UserQuery(api::message::UserQuery {
query: "create and edit plans".to_string(),
context: None,
referenced_attachments: HashMap::new(),
mode: None,
intended_agent: Default::default(),
})),
request_id: "req1".to_string(),
timestamp: None,
},
// Agent output
api::Message {
id: "agent_text".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "Creating plan A".to_string(),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
// Create doc A tool call
api::Message {
id: "tool_call_create_a".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
tool_call_id: "tc_create_a".to_string(),
tool: Some(api::message::tool_call::Tool::CreateDocuments(
api::message::tool_call::CreateDocuments {
new_documents: vec![
api::message::tool_call::create_documents::NewDocument {
title: "Plan A".to_string(),
content: "# Plan A".to_string(),
},
],
},
)),
})),
request_id: "req1".to_string(),
timestamp: None,
},
// Create doc A result
api::Message {
id: "result_create_a".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: "tc_create_a".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::CreateDocuments(
api::CreateDocumentsResult {
result: Some(api::create_documents_result::Result::Success(
api::create_documents_result::Success {
created_documents: vec![api::DocumentContent {
document_id: doc_id_a.clone(),
content: "# Plan A v1".to_string(),
line_range: None,
}],
},
)),
},
)),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
// Agent output before edit
api::Message {
id: "agent_text_2".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "Editing plan A".to_string(),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
// Edit doc A tool call
api::Message {
id: "tool_call_edit_a".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
tool_call_id: "tc_edit_a".to_string(),
tool: Some(api::message::tool_call::Tool::EditDocuments(
api::message::tool_call::EditDocuments {
diffs: vec![api::message::tool_call::edit_documents::DocumentDiff {
document_id: doc_id_a.clone(),
search: "# Plan A".to_string(),
replace: "# Plan A (edited)".to_string(),
}],
},
)),
})),
request_id: "req1".to_string(),
timestamp: None,
},
// Edit doc A result
api::Message {
id: "result_edit_a".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: "tc_edit_a".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::EditDocuments(
api::EditDocumentsResult {
result: Some(api::edit_documents_result::Result::Success(
api::edit_documents_result::Success {
updated_documents: vec![api::DocumentContent {
document_id: doc_id_a.clone(),
content: "# Plan A (edited)".to_string(),
line_range: None,
}],
},
)),
},
)),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
// Agent output before second create
api::Message {
id: "agent_text_3".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::AgentOutput(
api::message::AgentOutput {
text: "Creating plan B".to_string(),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
// Create doc B tool call
api::Message {
id: "tool_call_create_b".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
tool_call_id: "tc_create_b".to_string(),
tool: Some(api::message::tool_call::Tool::CreateDocuments(
api::message::tool_call::CreateDocuments {
new_documents: vec![
api::message::tool_call::create_documents::NewDocument {
title: "Plan B".to_string(),
content: "# Plan B".to_string(),
},
],
},
)),
})),
request_id: "req1".to_string(),
timestamp: None,
},
// Create doc B result
api::Message {
id: "result_create_b".to_string(),
task_id: "task1".to_string(),
server_message_data: "".to_string(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: "tc_create_b".to_string(),
context: None,
result: Some(api::message::tool_call_result::Result::CreateDocuments(
api::CreateDocumentsResult {
result: Some(api::create_documents_result::Result::Success(
api::create_documents_result::Success {
created_documents: vec![api::DocumentContent {
document_id: doc_id_b.clone(),
content: "# Plan B v1".to_string(),
line_range: None,
}],
},
)),
},
)),
},
)),
request_id: "req1".to_string(),
timestamp: None,
},
];
let task = api::Task {
id: "task1".to_string(),
messages,
dependencies: None,
description: "".to_string(),
summary: "".to_string(),
server_data: "".to_string(),
};
let exchanges = task.into_exchanges();
assert_eq!(exchanges.len(), 1, "Should create 1 exchange");
let default_version = AIDocumentVersion::default();
// Collect all document-related action results in order
let mut create_a_version: Option<AIDocumentVersion> = None;
let mut edit_a_version: Option<AIDocumentVersion> = None;
let mut create_b_version: Option<AIDocumentVersion> = None;
for input in &exchanges[0].input {
if let AIAgentInput::ActionResult { result, .. } = input {
match &result.result {
AIAgentActionResultType::CreateDocuments(CreateDocumentsResult::Success {
created_documents,
}) => {
for doc in created_documents {
let id_str = doc.document_id.to_string();
if id_str == doc_id_a {
create_a_version = Some(doc.document_version);
} else if id_str == doc_id_b {
create_b_version = Some(doc.document_version);
}
}
}
AIAgentActionResultType::EditDocuments(EditDocumentsResult::Success {
updated_documents,
}) => {
for doc in updated_documents {
let id_str = doc.document_id.to_string();
if id_str == doc_id_a {
edit_a_version = Some(doc.document_version);
}
}
}
_ => {}
}
}
}
// Create doc A should be v1 (default)
assert_eq!(
create_a_version,
Some(default_version),
"Created doc A should have default version (v1)"
);
// Edit doc A should be v2 (one increment from v1)
assert_eq!(
edit_a_version,
Some(default_version.next()),
"Edited doc A should have version v2 (incremented from create)"
);
// Create doc B should be v1 (independent new document)
assert_eq!(
create_b_version,
Some(default_version),
"Created doc B should have default version (v1), independent of doc A"
);
}