first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -4,12 +4,28 @@
|
||||
//! If some UI state is stored in the client, it needs to also be represented in the proto tasks somehow so it can be restored.
|
||||
//! Some conversions may be lossy if it's not important to recover that UI state.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ai::agent::action_result::{
|
||||
AskUserQuestionAnswerItem, AskUserQuestionResult, FetchConversationResult, ReadSkillResult,
|
||||
RequestComputerUseResult, SendMessageToAgentResult, StartAgentResult, StartAgentVersion,
|
||||
UseComputerResult,
|
||||
};
|
||||
use ai::skills::{ParsedSkill, SkillPathOrigin};
|
||||
use chrono::{DateTime, Local, TimeZone};
|
||||
use persistence::model::AgentConversationData;
|
||||
use galaxy_core::command::ExitCode;
|
||||
use warp_multi_agent_api as api;
|
||||
use warp_multi_agent_api::ask_user_question_result::answer_item::Answer as AskUserQuestionAnswer;
|
||||
|
||||
use crate::ai::agent::api::convert_from::{
|
||||
convert_user_query_mode, ConversionParams, ConvertAPIMessageToClientOutputMessage,
|
||||
MaybeAIAgentOutputMessage,
|
||||
};
|
||||
use crate::ai::agent::conversation::update_todo_list_from_todo_op;
|
||||
use crate::ai::agent::conversation::{AIConversation, AIConversationId};
|
||||
use crate::ai::agent::conversation::{
|
||||
update_todo_list_from_todo_op, AIConversation, AIConversationId, ServerAIConversationMetadata,
|
||||
};
|
||||
use crate::ai::agent::task::TaskId;
|
||||
use crate::ai::agent::todos::AIAgentTodoList;
|
||||
use crate::ai::agent::{
|
||||
@@ -24,7 +40,7 @@ use crate::ai::agent::{
|
||||
RequestFileEditsResult, SearchCodebaseFailureReason, SearchCodebaseResult, ServerOutputId,
|
||||
Shared, ShellCommandCompletedTrigger, ShellCommandError, SuggestNewConversationResult,
|
||||
SuggestPromptResult, TransferShellCommandControlToUserResult, UpdatedFileContext,
|
||||
UploadArtifactResult, WriteToLongRunningShellCommandResult,
|
||||
UploadArtifactResult, UserQueryMode, WriteToLongRunningShellCommandResult,
|
||||
};
|
||||
use crate::ai::block_context::BlockContext;
|
||||
use crate::ai::document::ai_document_model::{AIDocumentId, AIDocumentVersion};
|
||||
@@ -32,22 +48,6 @@ use crate::ai::llms::LLMId;
|
||||
use crate::ai_assistant::execution_context::{WarpAiExecutionContext, WarpAiOsContext};
|
||||
use crate::terminal::model::block::BlockId;
|
||||
use crate::terminal::model::terminal_model::BlockIndex;
|
||||
use ai::agent::action_result::{
|
||||
AskUserQuestionAnswerItem, AskUserQuestionResult, FetchConversationResult, ReadSkillResult,
|
||||
RequestComputerUseResult, SendMessageToAgentResult, StartAgentResult, StartAgentVersion,
|
||||
UseComputerResult,
|
||||
};
|
||||
use ai::skills::ParsedSkill;
|
||||
use chrono::{DateTime, Local, TimeZone};
|
||||
use galaxy_core::command::ExitCode;
|
||||
use persistence::model::AgentConversationData;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use warp_multi_agent_api as api;
|
||||
use warp_multi_agent_api::ask_user_question_result::answer_item::Answer as AskUserQuestionAnswer;
|
||||
|
||||
use crate::ai::agent::conversation::ServerAIConversationMetadata;
|
||||
use crate::ai::agent::UserQueryMode;
|
||||
|
||||
/// How to restore a conversation from the cloud.
|
||||
pub enum RestorationMode {
|
||||
@@ -81,12 +81,14 @@ pub fn convert_conversation_data_to_ai_conversation(
|
||||
artifacts_json: None,
|
||||
parent_agent_id: None,
|
||||
agent_name: None,
|
||||
orchestration_harness_type: None,
|
||||
parent_conversation_id: None,
|
||||
is_remote_child: false,
|
||||
root_task_is_optimistic: None,
|
||||
run_id: None,
|
||||
autoexecute_override: None,
|
||||
last_event_sequence: None,
|
||||
progressive_summary: None,
|
||||
messages_summarized_up_to: 0,
|
||||
pinned: false,
|
||||
},
|
||||
RestorationMode::Continue => AgentConversationData {
|
||||
server_conversation_token: Some(
|
||||
@@ -98,16 +100,16 @@ pub fn convert_conversation_data_to_ai_conversation(
|
||||
artifacts_json: serde_json::to_string(&metadata.artifacts).ok(),
|
||||
parent_agent_id: None,
|
||||
agent_name: None,
|
||||
orchestration_harness_type: None,
|
||||
parent_conversation_id: None,
|
||||
// TODO: Populate run_id from server metadata once it is exposed
|
||||
// in ServerAIConversationMetadata. For cloud conversations that
|
||||
// were spawned via the server API, the run_id is created at task
|
||||
// dispatch time; adding it here would avoid a round-trip to StreamInit.
|
||||
run_id: None,
|
||||
is_remote_child: false,
|
||||
root_task_is_optimistic: None,
|
||||
run_id: metadata
|
||||
.ambient_agent_task_id
|
||||
.map(|task_id| task_id.to_string()),
|
||||
autoexecute_override: None,
|
||||
last_event_sequence: None,
|
||||
progressive_summary: None,
|
||||
messages_summarized_up_to: 0,
|
||||
pinned: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -239,7 +241,8 @@ pub(crate) fn convert_input_context(context: Option<&api::InputContext>) -> Arc<
|
||||
};
|
||||
|
||||
// Convert binary data to base64
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine;
|
||||
let data = general_purpose::STANDARD.encode(&image.data);
|
||||
|
||||
result.push(AIAgentContext::Image(ImageContext {
|
||||
@@ -436,7 +439,10 @@ impl ConvertToExchanges for &api::Task {
|
||||
api::message::system_query::Type::ResumeConversation(_)
|
||||
| api::message::system_query::Type::GeneratePassiveSuggestions(_)
|
||||
// TODO: Implement this for real. ZB adding this to bump proto version for unrelated API changes.
|
||||
| api::message::system_query::Type::SummarizeConversation(_)=> false,
|
||||
| api::message::system_query::Type::SummarizeConversation(_)
|
||||
// HandoffRehydration is injected by the server for agent-only
|
||||
// context; the client must never render it as user input.
|
||||
| api::message::system_query::Type::HandoffRehydration(_) => false,
|
||||
}
|
||||
}
|
||||
api::message::Message::ToolCallResult(tool_call_result) => {
|
||||
@@ -462,7 +468,10 @@ impl ConvertToExchanges for &api::Task {
|
||||
}
|
||||
api::message::Message::InvokeSkill(invoke_skill) => {
|
||||
if let Some(api_skill) = invoke_skill.skill.clone() {
|
||||
if let Ok(parsed_skill) = ParsedSkill::try_from(api_skill) {
|
||||
if let Ok(parsed_skill) = ParsedSkill::try_from_api_with_origin(
|
||||
api_skill,
|
||||
&SkillPathOrigin::RestoredDisplayOnly,
|
||||
) {
|
||||
let user_query = invoke_skill
|
||||
.user_query
|
||||
.clone()
|
||||
@@ -513,7 +522,8 @@ impl ConvertToExchanges for &api::Task {
|
||||
| api::message::Message::DebugOutput(_)
|
||||
| api::message::Message::ArtifactEvent(_)
|
||||
| api::message::Message::MessagesReceivedFromAgents(_)
|
||||
| api::message::Message::ModelUsed(_) => false,
|
||||
| api::message::Message::ModelUsed(_)
|
||||
| api::message::Message::OrchestrationConfigSnapshot(_) => false,
|
||||
};
|
||||
|
||||
if !added_message_as_exchange_input {
|
||||
@@ -524,6 +534,7 @@ impl ConvertToExchanges for &api::Task {
|
||||
// TODO(alokedesai): Support persistence for the code review state.
|
||||
active_code_review: None,
|
||||
task_id: &TaskId::new(api_message.task_id.clone()),
|
||||
skill_path_origin: &SkillPathOrigin::Unavailable,
|
||||
})
|
||||
{
|
||||
current_outputs.push(output_msg);
|
||||
@@ -581,6 +592,14 @@ pub(crate) fn convert_tool_call_result_to_input(
|
||||
command: result.command.clone(),
|
||||
output: finished.output.clone(),
|
||||
exit_code: ExitCode::from(finished.exit_code),
|
||||
start_ts: finished
|
||||
.start_ts
|
||||
.as_ref()
|
||||
.map(|ts| proto_timestamp_to_local_datetime(ts.seconds, ts.nanos)),
|
||||
completed_ts: finished
|
||||
.finish_ts
|
||||
.as_ref()
|
||||
.map(|ts| proto_timestamp_to_local_datetime(ts.seconds, ts.nanos)),
|
||||
}
|
||||
}
|
||||
Some(api::run_shell_command_result::Result::LongRunningCommandSnapshot(
|
||||
@@ -627,6 +646,8 @@ pub(crate) fn convert_tool_call_result_to_input(
|
||||
block_id: finished.command_id.clone().into(),
|
||||
output: finished.output.clone(),
|
||||
exit_code: ExitCode::from(finished.exit_code),
|
||||
start_ts: finished.start_ts.as_ref().map(|ts| proto_timestamp_to_local_datetime(ts.seconds, ts.nanos)),
|
||||
completed_ts: finished.finish_ts.as_ref().map(|ts| proto_timestamp_to_local_datetime(ts.seconds, ts.nanos)),
|
||||
},
|
||||
Some(api::write_to_long_running_shell_command_result::Result::Error(api::ShellCommandError{
|
||||
r#type: Some(api::shell_command_error::Type::CommandNotFound(()))
|
||||
@@ -1217,6 +1238,14 @@ pub(crate) fn convert_tool_call_result_to_input(
|
||||
block_id: finished.command_id.clone().into(),
|
||||
output: finished.output.clone(),
|
||||
exit_code: ExitCode::from(finished.exit_code),
|
||||
start_ts: finished
|
||||
.start_ts
|
||||
.as_ref()
|
||||
.map(|ts| proto_timestamp_to_local_datetime(ts.seconds, ts.nanos)),
|
||||
completed_ts: finished
|
||||
.finish_ts
|
||||
.as_ref()
|
||||
.map(|ts| proto_timestamp_to_local_datetime(ts.seconds, ts.nanos)),
|
||||
}
|
||||
}
|
||||
Some(
|
||||
@@ -1267,6 +1296,8 @@ pub(crate) fn convert_tool_call_result_to_input(
|
||||
block_id: finished.command_id.clone().into(),
|
||||
output: finished.output.clone(),
|
||||
exit_code: ExitCode::from(finished.exit_code),
|
||||
start_ts: finished.start_ts.as_ref().map(|ts| proto_timestamp_to_local_datetime(ts.seconds, ts.nanos)),
|
||||
completed_ts: finished.finish_ts.as_ref().map(|ts| proto_timestamp_to_local_datetime(ts.seconds, ts.nanos)),
|
||||
},
|
||||
Some(api::transfer_shell_command_control_to_user_result::Result::Error(
|
||||
api::ShellCommandError {
|
||||
@@ -1315,34 +1346,52 @@ pub(crate) fn convert_tool_call_result_to_input(
|
||||
})
|
||||
}
|
||||
Some(ToolCallResultType::UseComputer(result)) => {
|
||||
let use_computer_result = match &result.result {
|
||||
Some(api::use_computer_result::Result::Success(success)) => {
|
||||
let screenshot = success.screenshot.as_ref().map(|s| {
|
||||
// The original dimensions are not preserved through the API, so we use
|
||||
// the current dimensions for both.
|
||||
computer_use::Screenshot {
|
||||
width: s.width as usize,
|
||||
height: s.height as usize,
|
||||
original_width: s.width as usize,
|
||||
original_height: s.height as usize,
|
||||
data: s.data.clone(),
|
||||
mime_type: s.mime_type.clone().into(),
|
||||
}
|
||||
});
|
||||
let cursor_position = success
|
||||
.cursor_position
|
||||
.as_ref()
|
||||
.map(|c| computer_use::Vector2I::new(c.x, c.y));
|
||||
UseComputerResult::Success(computer_use::ActionResult {
|
||||
screenshot,
|
||||
cursor_position,
|
||||
})
|
||||
}
|
||||
Some(api::use_computer_result::Result::Error(error)) => {
|
||||
UseComputerResult::Error(error.message.clone())
|
||||
}
|
||||
None => UseComputerResult::Cancelled,
|
||||
};
|
||||
let use_computer_result =
|
||||
match &result.result {
|
||||
Some(api::use_computer_result::Result::Success(success)) => {
|
||||
let screenshot = success.screenshot.as_ref().map(|s| {
|
||||
// The original dimensions are not preserved through the API, so we use
|
||||
// the current dimensions for both.
|
||||
computer_use::Screenshot {
|
||||
width: s.width as usize,
|
||||
height: s.height as usize,
|
||||
original_width: s.width as usize,
|
||||
original_height: s.height as usize,
|
||||
data: s.data.clone(),
|
||||
mime_type: s.mime_type.clone().into(),
|
||||
}
|
||||
});
|
||||
let cursor_position = success
|
||||
.cursor_position
|
||||
.as_ref()
|
||||
.map(|c| computer_use::Vector2I::new(c.x, c.y));
|
||||
let windows = success
|
||||
.windows
|
||||
.iter()
|
||||
.map(convert_api_window_info)
|
||||
.collect();
|
||||
// A present captured-window message indicates a window screenshot was taken.
|
||||
// The window id is an opaque string on the wire; on macOS it is a CGWindowID,
|
||||
// so parse it back to a u32, defaulting to 0 when it is not parseable.
|
||||
let captured_window = success.captured_window.as_ref().map(|c| {
|
||||
computer_use::CapturedWindow {
|
||||
window_id: c.window_id.parse().unwrap_or(0),
|
||||
width_px: c.width_px,
|
||||
height_px: c.height_px,
|
||||
}
|
||||
});
|
||||
UseComputerResult::Success(computer_use::ActionResult {
|
||||
screenshot,
|
||||
cursor_position,
|
||||
windows,
|
||||
captured_window,
|
||||
})
|
||||
}
|
||||
Some(api::use_computer_result::Result::Error(error)) => {
|
||||
UseComputerResult::Error(error.message.clone())
|
||||
}
|
||||
None => UseComputerResult::Cancelled,
|
||||
};
|
||||
|
||||
Some(AIAgentInput::ActionResult {
|
||||
result: AIAgentActionResult {
|
||||
@@ -1361,6 +1410,7 @@ pub(crate) fn convert_tool_call_result_to_input(
|
||||
api::request_computer_use_result::Approved {
|
||||
screen_dimensions: Some(screen_dimensions),
|
||||
initial_screenshot: Some(initial_screenshot),
|
||||
windows,
|
||||
..
|
||||
},
|
||||
Some(platform),
|
||||
@@ -1374,6 +1424,7 @@ pub(crate) fn convert_tool_call_result_to_input(
|
||||
mime_type: initial_screenshot.mime_type.clone().into(),
|
||||
},
|
||||
platform,
|
||||
windows: windows.iter().map(convert_api_window_info).collect(),
|
||||
},
|
||||
_ => RequestComputerUseResult::Error(
|
||||
"Missing screen dimensions, initial screenshot, or valid platform"
|
||||
@@ -1544,6 +1595,76 @@ pub(crate) fn convert_tool_call_result_to_input(
|
||||
context,
|
||||
})
|
||||
}
|
||||
Some(ToolCallResultType::RunAgentsResult(result)) => {
|
||||
use ai::agent::action_result::{
|
||||
RunAgentsAgentOutcome, RunAgentsAgentOutcomeKind, RunAgentsLaunchedExecutionMode,
|
||||
RunAgentsResult,
|
||||
};
|
||||
let run_agents_result = match &result.outcome {
|
||||
Some(api::run_agents_result::Outcome::Launched(launched)) => {
|
||||
let execution_mode = match &launched.resolved_execution_mode {
|
||||
Some(api::run_agents_result::launched::ResolvedExecutionMode::Remote(
|
||||
remote,
|
||||
)) => RunAgentsLaunchedExecutionMode::Remote {
|
||||
environment_id: remote.environment_id.clone(),
|
||||
worker_host: remote.worker_host.clone(),
|
||||
computer_use_enabled: remote.computer_use_enabled,
|
||||
},
|
||||
Some(api::run_agents_result::launched::ResolvedExecutionMode::Local(_))
|
||||
| None => RunAgentsLaunchedExecutionMode::Local,
|
||||
};
|
||||
let agents = launched
|
||||
.agents
|
||||
.iter()
|
||||
.map(|outcome| RunAgentsAgentOutcome {
|
||||
name: outcome.name.clone(),
|
||||
kind: match &outcome.result {
|
||||
Some(api::run_agents_result::agent_outcome::Result::Launched(
|
||||
launched_agent,
|
||||
)) => RunAgentsAgentOutcomeKind::Launched {
|
||||
agent_id: launched_agent.agent_id.clone(),
|
||||
},
|
||||
Some(api::run_agents_result::agent_outcome::Result::Failed(
|
||||
failed,
|
||||
)) => RunAgentsAgentOutcomeKind::Failed {
|
||||
error: failed.error.clone(),
|
||||
},
|
||||
None => RunAgentsAgentOutcomeKind::Failed {
|
||||
error: String::new(),
|
||||
},
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
RunAgentsResult::Launched {
|
||||
model_id: launched.resolved_model_id.clone(),
|
||||
harness_type:
|
||||
crate::ai::agent::api::convert_from::convert_run_agents_harness(
|
||||
launched.resolved_harness.as_ref(),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
execution_mode,
|
||||
agents,
|
||||
}
|
||||
}
|
||||
Some(api::run_agents_result::Outcome::Denied(denied)) => RunAgentsResult::Denied {
|
||||
reason: denied.reason.clone(),
|
||||
},
|
||||
Some(api::run_agents_result::Outcome::Failure(failure)) => {
|
||||
RunAgentsResult::Failure {
|
||||
error: failure.error.clone(),
|
||||
}
|
||||
}
|
||||
None => RunAgentsResult::Cancelled,
|
||||
};
|
||||
Some(AIAgentInput::ActionResult {
|
||||
result: AIAgentActionResult {
|
||||
id: tool_call_id.into(),
|
||||
task_id: task_id.clone(),
|
||||
result: AIAgentActionResultType::RunAgents(run_agents_result),
|
||||
},
|
||||
context,
|
||||
})
|
||||
}
|
||||
// Deprecated/unused result types or absent result.
|
||||
Some(ToolCallResultType::SuggestCreatePlan(..))
|
||||
| Some(ToolCallResultType::SuggestPlan(..))
|
||||
@@ -1551,6 +1672,7 @@ pub(crate) fn convert_tool_call_result_to_input(
|
||||
log::warn!("No result present for tool call ID: {tool_call_id}");
|
||||
None
|
||||
}
|
||||
Some(ToolCallResultType::WaitForEvents(_)) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1678,8 +1800,14 @@ fn create_cancelled_result_for_tool_call(
|
||||
ToolType::SendMessageToAgent(_) => {
|
||||
AIAgentActionResultType::SendMessageToAgent(SendMessageToAgentResult::Cancelled)
|
||||
}
|
||||
ToolType::RunAgents(_) => {
|
||||
AIAgentActionResultType::RunAgents(ai::agent::action_result::RunAgentsResult::Cancelled)
|
||||
}
|
||||
// These tools are deprecated.
|
||||
ToolType::SuggestCreatePlan(_) | ToolType::SuggestPlan(_) => return None,
|
||||
ToolType::WaitForEvents(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(AIAgentInput::ActionResult {
|
||||
@@ -1777,6 +1905,10 @@ fn create_exchange_from_messages(
|
||||
model_id: model.model_id.clone().into(),
|
||||
display_name: model.model_display_name.clone(),
|
||||
is_fallback: model.is_fallback,
|
||||
prompt_cache_expires_at: model
|
||||
.prompt_cache_expires_at
|
||||
.as_ref()
|
||||
.map(|ts| proto_timestamp_to_local_datetime(ts.seconds, ts.nanos)),
|
||||
}),
|
||||
request_cost: None,
|
||||
};
|
||||
@@ -1879,7 +2011,8 @@ where
|
||||
| api::message::Message::DebugOutput(_)
|
||||
| api::message::Message::ArtifactEvent(_)
|
||||
| api::message::Message::InvokeSkill(_)
|
||||
| api::message::Message::ModelUsed(_) => {
|
||||
| api::message::Message::ModelUsed(_)
|
||||
| api::message::Message::OrchestrationConfigSnapshot(_) => {
|
||||
message.timestamp.as_ref().map(|timestamp| {
|
||||
proto_timestamp_to_local_datetime(timestamp.seconds, timestamp.nanos)
|
||||
})
|
||||
@@ -1985,7 +2118,7 @@ fn convert_passive_suggestion_result_to_input(
|
||||
context,
|
||||
})
|
||||
}
|
||||
fn proto_timestamp_to_local_datetime(seconds: i64, nanos: i32) -> DateTime<Local> {
|
||||
pub(crate) fn proto_timestamp_to_local_datetime(seconds: i64, nanos: i32) -> DateTime<Local> {
|
||||
let nanos = if nanos < 0 { 0 } else { nanos as u32 };
|
||||
|
||||
Local
|
||||
@@ -2014,6 +2147,19 @@ fn convert_api_platform(platform: i32) -> Option<computer_use::Platform> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstructs the internal computer_use window record from the API `WindowInfo` message.
|
||||
fn convert_api_window_info(window: &api::WindowInfo) -> computer_use::WindowInfo {
|
||||
computer_use::WindowInfo {
|
||||
// The window id arrives as an opaque string; on macOS it is a CGWindowID (u32). Default to
|
||||
// 0 when it is not parseable.
|
||||
window_id: window.window_id.parse().unwrap_or(0),
|
||||
pid: window.pid,
|
||||
app_name: window.app_name.clone(),
|
||||
title: window.title.clone(),
|
||||
layer: window.layer,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "convert_conversation_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,7 +1,56 @@
|
||||
use crate::ai::agent::api::convert_conversation::*;
|
||||
use crate::ai::agent::{AIAgentInput, UserQueryMode};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::Utc;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use crate::ai::agent::api::convert_conversation::*;
|
||||
use crate::ai::agent::api::ServerConversationToken;
|
||||
use crate::ai::agent::conversation::{
|
||||
AIAgentHarness, AIConversationId, ServerAIConversationMetadata,
|
||||
};
|
||||
use crate::ai::agent::{AIAgentInput, UserQueryMode};
|
||||
use crate::ai::ambient_agents::AmbientAgentTaskId;
|
||||
use crate::cloud_object::{Revision, ServerMetadata, ServerPermissions};
|
||||
use crate::persistence::model::ConversationUsageMetadata;
|
||||
use crate::server::ids::ServerId;
|
||||
|
||||
fn test_server_metadata(
|
||||
server_token: &str,
|
||||
ambient_agent_task_id: Option<AmbientAgentTaskId>,
|
||||
) -> ServerAIConversationMetadata {
|
||||
ServerAIConversationMetadata {
|
||||
title: "test conversation".to_string(),
|
||||
working_directory: None,
|
||||
harness: AIAgentHarness::Oz,
|
||||
usage: ConversationUsageMetadata {
|
||||
was_summarized: false,
|
||||
context_window_usage: 0.0,
|
||||
credits_spent: 0.0,
|
||||
platform_credits_spent: 0.0,
|
||||
credits_spent_for_last_block: None,
|
||||
token_usage: vec![],
|
||||
tool_usage_metadata: Default::default(),
|
||||
context_window_segments: Vec::new(),
|
||||
},
|
||||
metadata: ServerMetadata {
|
||||
uid: ServerId::default(),
|
||||
revision: Revision::now(),
|
||||
metadata_last_updated_ts: Utc::now().into(),
|
||||
trashed_ts: None,
|
||||
folder_id: None,
|
||||
is_welcome_object: false,
|
||||
creator_uid: None,
|
||||
last_editor_uid: None,
|
||||
current_editor_uid: None,
|
||||
},
|
||||
permissions: ServerPermissions::mock_personal(),
|
||||
creator: None,
|
||||
ambient_agent_task_id,
|
||||
server_conversation_token: ServerConversationToken::new(server_token.to_string()),
|
||||
artifacts: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn test_skill() -> api::Skill {
|
||||
api::Skill {
|
||||
descriptor: Some(api::SkillDescriptor {
|
||||
@@ -25,6 +74,40 @@ fn test_skill() -> api::Skill {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn test_convert_conversation_data_to_ai_conversation_sets_restored_run_id() {
|
||||
let conversation_id = AIConversationId::new();
|
||||
let ambient_agent_task_id: AmbientAgentTaskId =
|
||||
"550e8400-e29b-41d4-a716-446655440000".parse().unwrap();
|
||||
let conversation_data = api::ConversationData {
|
||||
tasks: vec![api::Task {
|
||||
id: "root".to_string(),
|
||||
messages: vec![],
|
||||
dependencies: None,
|
||||
description: String::new(),
|
||||
summary: String::new(),
|
||||
server_data: String::new(),
|
||||
}],
|
||||
ordered_message_ids: vec![],
|
||||
};
|
||||
|
||||
let conversation = convert_conversation_data_to_ai_conversation(
|
||||
conversation_id,
|
||||
&conversation_data,
|
||||
test_server_metadata("server-token", Some(ambient_agent_task_id)),
|
||||
RestorationMode::Continue,
|
||||
)
|
||||
.expect("conversation should restore");
|
||||
|
||||
assert_eq!(conversation.id(), conversation_id);
|
||||
assert_eq!(conversation.task_id(), Some(ambient_agent_task_id));
|
||||
assert_eq!(
|
||||
conversation.run_id(),
|
||||
Some(ambient_agent_task_id.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_tool_call_result_to_input_transfer_control_snapshot() {
|
||||
let task_id = crate::ai::agent::task::TaskId::new("task".to_string());
|
||||
@@ -277,6 +360,7 @@ fn test_into_exchanges_basic() {
|
||||
// Create minimal test data
|
||||
let messages = vec![
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "user_msg".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -292,6 +376,7 @@ fn test_into_exchanges_basic() {
|
||||
timestamp: None,
|
||||
},
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "agent_msg".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -305,6 +390,7 @@ fn test_into_exchanges_basic() {
|
||||
timestamp: None,
|
||||
},
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "user_msg2".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -320,6 +406,7 @@ fn test_into_exchanges_basic() {
|
||||
timestamp: None,
|
||||
},
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "agent_msg2".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -333,6 +420,7 @@ fn test_into_exchanges_basic() {
|
||||
timestamp: None,
|
||||
},
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "user_msg3".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -348,6 +436,7 @@ fn test_into_exchanges_basic() {
|
||||
timestamp: None,
|
||||
},
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "agent_msg3".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -392,6 +481,7 @@ fn test_invoke_skill_arguments_round_trip() {
|
||||
let query = "arg1 arg2".to_string();
|
||||
let messages = vec![
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "invoke_skill_msg".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -412,6 +502,7 @@ fn test_invoke_skill_arguments_round_trip() {
|
||||
timestamp: None,
|
||||
},
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "agent_msg".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -448,7 +539,7 @@ fn test_invoke_skill_arguments_round_trip() {
|
||||
Some("arg1 arg2")
|
||||
);
|
||||
assert_eq!(
|
||||
exchanges[0].input[0].user_query().as_deref(),
|
||||
exchanges[0].input[0].display_query().as_deref(),
|
||||
Some("/test-skill arg1 arg2")
|
||||
);
|
||||
}
|
||||
@@ -459,6 +550,7 @@ fn test_invoke_skill_arguments_round_trip() {
|
||||
#[test]
|
||||
fn test_invoke_skill_missing_user_query_maps_to_none() {
|
||||
let messages = vec![api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "invoke_skill_msg".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -492,7 +584,7 @@ fn test_invoke_skill_missing_user_query_maps_to_none() {
|
||||
assert_eq!(skill.name, "test-skill");
|
||||
assert_eq!(user_query, &None);
|
||||
assert_eq!(
|
||||
exchanges[0].input[0].user_query().as_deref(),
|
||||
exchanges[0].input[0].display_query().as_deref(),
|
||||
Some("/test-skill")
|
||||
);
|
||||
}
|
||||
@@ -505,6 +597,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() {
|
||||
let messages = vec![
|
||||
// User query
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "user_query".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -521,6 +614,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() {
|
||||
},
|
||||
// Agent response
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "agent_response".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -535,6 +629,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() {
|
||||
},
|
||||
// Tool call 1
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "tool_call_1".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -558,6 +653,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() {
|
||||
},
|
||||
// Tool call 2
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "tool_call_2".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -581,6 +677,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() {
|
||||
},
|
||||
// Tool call 3
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "tool_call_3".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -604,6 +701,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() {
|
||||
},
|
||||
// Tool call result - cancelled (call_2)
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "result_cancelled".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -620,6 +718,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() {
|
||||
},
|
||||
// Tool call result - success (call_1)
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "result_success_1".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -639,6 +738,8 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() {
|
||||
command_id: "command_1".to_string(),
|
||||
output: "1".to_string(),
|
||||
exit_code: 0,
|
||||
start_ts: None,
|
||||
finish_ts: None,
|
||||
},
|
||||
)),
|
||||
},
|
||||
@@ -650,6 +751,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() {
|
||||
},
|
||||
// Tool call result - success (call_3)
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "result_success_3".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -669,6 +771,8 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() {
|
||||
command_id: "command_2".to_string(),
|
||||
output: "3".to_string(),
|
||||
exit_code: 0,
|
||||
start_ts: None,
|
||||
finish_ts: None,
|
||||
},
|
||||
)),
|
||||
},
|
||||
@@ -680,6 +784,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() {
|
||||
},
|
||||
// Final agent response
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "final_response".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -694,6 +799,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() {
|
||||
},
|
||||
// Follow-up user query
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "followup_query".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -710,6 +816,7 @@ fn test_into_exchanges_with_tool_calls_and_cancellation() {
|
||||
},
|
||||
// Final agent response
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "final_response2".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -811,6 +918,7 @@ fn test_into_exchanges_with_code_diffs() {
|
||||
let messages = vec![
|
||||
// User query asking for code changes
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "user_query".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -827,6 +935,7 @@ fn test_into_exchanges_with_code_diffs() {
|
||||
},
|
||||
// Agent response
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "agent_response".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -841,6 +950,7 @@ fn test_into_exchanges_with_code_diffs() {
|
||||
},
|
||||
// File diff tool call
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "diff_call".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -862,6 +972,7 @@ fn test_into_exchanges_with_code_diffs() {
|
||||
},
|
||||
// User cancels the diff
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "diff_cancelled".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -878,6 +989,7 @@ fn test_into_exchanges_with_code_diffs() {
|
||||
},
|
||||
// User provides feedback
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "user_feedback".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -894,6 +1006,7 @@ fn test_into_exchanges_with_code_diffs() {
|
||||
},
|
||||
// Agent response
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "agent_response_2".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -908,6 +1021,7 @@ fn test_into_exchanges_with_code_diffs() {
|
||||
},
|
||||
// Second file diff tool call
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "diff_call_2".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -929,6 +1043,7 @@ fn test_into_exchanges_with_code_diffs() {
|
||||
},
|
||||
// User accepts the diff
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "diff_accepted".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -956,6 +1071,7 @@ fn test_into_exchanges_with_code_diffs() {
|
||||
},
|
||||
// Final agent response
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "final_response".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -970,6 +1086,7 @@ fn test_into_exchanges_with_code_diffs() {
|
||||
},
|
||||
// Follow-up user query
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "followup".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -986,6 +1103,7 @@ fn test_into_exchanges_with_code_diffs() {
|
||||
},
|
||||
// Final agent response
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "final_response_2".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1083,6 +1201,7 @@ fn test_into_exchanges_with_code_diffs() {
|
||||
fn test_user_query_mode_conversion() {
|
||||
// Test conversion with Plan mode
|
||||
let messages = vec![api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "user_msg".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1129,6 +1248,7 @@ fn test_user_query_mode_conversion() {
|
||||
|
||||
// Test conversion with Normal mode (no type set)
|
||||
let messages_normal = vec![api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "user_msg".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1173,6 +1293,7 @@ fn test_user_query_mode_conversion() {
|
||||
|
||||
// Test conversion with no mode field (should default to Normal)
|
||||
let messages_default = vec![api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "user_msg".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1226,6 +1347,7 @@ fn test_exchanges_grouped_by_request_id() {
|
||||
let messages = vec![
|
||||
// Message 0: Server message (should be ignored or handled gracefully)
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
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(),
|
||||
@@ -1243,6 +1365,7 @@ fn test_exchanges_grouped_by_request_id() {
|
||||
},
|
||||
// Message 1: User query with request_id 78e236b8
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
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(),
|
||||
@@ -1259,6 +1382,7 @@ fn test_exchanges_grouped_by_request_id() {
|
||||
},
|
||||
// Message 2: Agent output with same request_id
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
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(),
|
||||
@@ -1273,6 +1397,7 @@ fn test_exchanges_grouped_by_request_id() {
|
||||
},
|
||||
// Message 3: Tool call with same request_id
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
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(),
|
||||
@@ -1296,6 +1421,7 @@ fn test_exchanges_grouped_by_request_id() {
|
||||
},
|
||||
// Message 4: Tool call result with NEW request_id 59a3947f (starts new exchange)
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
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(),
|
||||
@@ -1330,6 +1456,7 @@ fn test_exchanges_grouped_by_request_id() {
|
||||
},
|
||||
// Message 5: Agent output with same request_id
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
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(),
|
||||
@@ -1344,6 +1471,7 @@ fn test_exchanges_grouped_by_request_id() {
|
||||
},
|
||||
// Message 6: Write to long running command with NEW request_id 9f85acb2 (starts new exchange)
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
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(),
|
||||
@@ -1374,6 +1502,7 @@ fn test_exchanges_grouped_by_request_id() {
|
||||
},
|
||||
// Message 7: Final tool call result with same request_id
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
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(),
|
||||
@@ -1395,6 +1524,8 @@ fn test_exchanges_grouped_by_request_id() {
|
||||
command_id: "cmd1".to_string(),
|
||||
output: "Done".to_string(),
|
||||
exit_code: 0,
|
||||
start_ts: None,
|
||||
finish_ts: None,
|
||||
},
|
||||
)),
|
||||
},
|
||||
@@ -1404,6 +1535,7 @@ fn test_exchanges_grouped_by_request_id() {
|
||||
},
|
||||
// Message 8: Final agent output with same request_id
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
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(),
|
||||
@@ -1492,6 +1624,7 @@ fn test_multiple_create_documents_get_default_version() {
|
||||
let messages = vec![
|
||||
// User query
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "user_msg".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1508,6 +1641,7 @@ fn test_multiple_create_documents_get_default_version() {
|
||||
},
|
||||
// Agent output
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "agent_text".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1522,6 +1656,7 @@ fn test_multiple_create_documents_get_default_version() {
|
||||
},
|
||||
// First CreateDocuments tool call
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "tool_call_create_a".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1544,6 +1679,7 @@ fn test_multiple_create_documents_get_default_version() {
|
||||
},
|
||||
// First CreateDocuments result
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "result_create_a".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1572,6 +1708,7 @@ fn test_multiple_create_documents_get_default_version() {
|
||||
},
|
||||
// Agent output before second plan
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "agent_text_2".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1586,6 +1723,7 @@ fn test_multiple_create_documents_get_default_version() {
|
||||
},
|
||||
// Second CreateDocuments tool call
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "tool_call_create_b".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1608,6 +1746,7 @@ fn test_multiple_create_documents_get_default_version() {
|
||||
},
|
||||
// Second CreateDocuments result
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "result_create_b".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1694,7 +1833,6 @@ fn test_multiple_create_documents_get_default_version() {
|
||||
#[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();
|
||||
@@ -1702,6 +1840,7 @@ fn test_create_then_edit_then_create_version_tracking() {
|
||||
let messages = vec![
|
||||
// User query
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "user_msg".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1718,6 +1857,7 @@ fn test_create_then_edit_then_create_version_tracking() {
|
||||
},
|
||||
// Agent output
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "agent_text".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1732,6 +1872,7 @@ fn test_create_then_edit_then_create_version_tracking() {
|
||||
},
|
||||
// Create doc A tool call
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "tool_call_create_a".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1754,6 +1895,7 @@ fn test_create_then_edit_then_create_version_tracking() {
|
||||
},
|
||||
// Create doc A result
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "result_create_a".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1782,6 +1924,7 @@ fn test_create_then_edit_then_create_version_tracking() {
|
||||
},
|
||||
// Agent output before edit
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "agent_text_2".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1796,6 +1939,7 @@ fn test_create_then_edit_then_create_version_tracking() {
|
||||
},
|
||||
// Edit doc A tool call
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "tool_call_edit_a".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1817,6 +1961,7 @@ fn test_create_then_edit_then_create_version_tracking() {
|
||||
},
|
||||
// Edit doc A result
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "result_edit_a".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1845,6 +1990,7 @@ fn test_create_then_edit_then_create_version_tracking() {
|
||||
},
|
||||
// Agent output before second create
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "agent_text_3".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1859,6 +2005,7 @@ fn test_create_then_edit_then_create_version_tracking() {
|
||||
},
|
||||
// Create doc B tool call
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "tool_call_create_b".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1881,6 +2028,7 @@ fn test_create_then_edit_then_create_version_tracking() {
|
||||
},
|
||||
// Create doc B result
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "result_create_b".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
@@ -1979,3 +2127,74 @@ fn test_create_then_edit_then_create_version_tracking() {
|
||||
"Created doc B should have default version (v1), independent of doc A"
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify that a `SystemQuery::HandoffRehydration` message does not produce
|
||||
/// a displayed input when restoring a conversation. It must be treated as
|
||||
/// hidden, so the exchange should have zero user-visible inputs.
|
||||
#[test]
|
||||
fn test_handoff_rehydration_system_query_is_hidden() {
|
||||
let messages = vec![
|
||||
// HandoffRehydration system query – should be hidden
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "msg_handoff".to_string(),
|
||||
task_id: "task1".to_string(),
|
||||
server_message_data: "".to_string(),
|
||||
citations: vec![],
|
||||
message: Some(api::message::Message::SystemQuery(
|
||||
api::message::SystemQuery {
|
||||
r#type: Some(api::message::system_query::Type::HandoffRehydration(
|
||||
api::message::HandoffRehydration {
|
||||
instructions: "restore handoff state".to_string(),
|
||||
},
|
||||
)),
|
||||
context: None,
|
||||
},
|
||||
)),
|
||||
request_id: "req1".to_string(),
|
||||
timestamp: None,
|
||||
},
|
||||
// Agent output that follows the hidden system query
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "msg_output".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 have restored the handoff state.".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, "Should produce exactly one exchange");
|
||||
|
||||
let exchange = &exchanges[0];
|
||||
// The HandoffRehydration should NOT appear as input
|
||||
assert!(
|
||||
exchange.input.is_empty(),
|
||||
"HandoffRehydration must not produce a displayed input, got: {:?}",
|
||||
exchange.input
|
||||
);
|
||||
|
||||
// The agent output should still be present
|
||||
let output = exchange.output_status.output().expect("should have output");
|
||||
assert!(
|
||||
!output.get().messages.is_empty(),
|
||||
"Agent output should still be rendered"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,34 +2,33 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use ai::agent::action::{LifecycleEventType as StartAgentLifecycleEventType, ReadSkillRequest};
|
||||
use ai::agent::action_result::StartAgentVersion;
|
||||
use ai::agent::convert::ToolToAIAgentActionError;
|
||||
use ai::agent::UnknownCitationTypeError;
|
||||
use ai::skills::{
|
||||
skill_reference_from_api_skill_ref, skill_reference_from_read_skill_ref, SkillPathOrigin,
|
||||
};
|
||||
use api::ask_user_question::question::QuestionType;
|
||||
use galaxy_core::channel::ChannelState;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use crate::ai::agent::api::convert_conversation::{
|
||||
convert_input_context, convert_tool_call_result_to_input,
|
||||
};
|
||||
use crate::ai::agent::comment::CodeReview;
|
||||
use crate::ai::agent::task::TaskId;
|
||||
use crate::ai::agent::todos::AIAgentTodoList;
|
||||
use crate::ai::agent::util::parse_markdown_into_text_and_code_sections;
|
||||
use crate::ai::agent::{
|
||||
util::parse_markdown_into_text_and_code_sections, AIAgentAction, AIAgentActionType,
|
||||
AIAgentCitation, AIAgentInput, AIAgentOutputMessage, AIAgentText, AIAgentTodo,
|
||||
ArtifactCreatedData, MessageId, StartAgentExecutionMode, SuggestedAgentModeWorkflow,
|
||||
SuggestedRule, Suggestions, TodoOperation,
|
||||
};
|
||||
use crate::ai::agent::{
|
||||
CloneRepositoryURL, SubagentCall, SubagentType, SummarizationType, WebFetchStatus,
|
||||
WebSearchStatus,
|
||||
AIAgentAction, AIAgentActionType, AIAgentAttachment, AIAgentCitation, AIAgentInput,
|
||||
AIAgentOutputMessage, AIAgentText, AIAgentTodo, ArtifactCreatedData, CloneRepositoryURL,
|
||||
MessageId, RunAgentsAgentRunConfig, RunAgentsExecutionMode, RunAgentsRequest,
|
||||
StartAgentExecutionMode, SubagentCall, SubagentType, SuggestedAgentModeWorkflow, SuggestedRule,
|
||||
Suggestions, SummarizationType, TodoOperation, UserQueryMode, WebFetchStatus, WebSearchStatus,
|
||||
};
|
||||
use crate::ai::artifact_download::sanitized_basename;
|
||||
use crate::ai::document::ai_document_model::{AIDocumentId, AIDocumentVersion};
|
||||
use ai::agent::action::LifecycleEventType as StartAgentLifecycleEventType;
|
||||
use ai::agent::action_result::StartAgentVersion;
|
||||
use ai::agent::convert::ToolToAIAgentActionError;
|
||||
use ai::agent::UnknownCitationTypeError;
|
||||
use ai::skills::SkillReference;
|
||||
use api::ask_user_question::question::QuestionType;
|
||||
use galaxy_core::channel::ChannelState;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use crate::ai::agent::{AIAgentAttachment, UserQueryMode};
|
||||
|
||||
impl TryFrom<api::Attachment> for AIAgentAttachment {
|
||||
type Error = anyhow::Error;
|
||||
@@ -53,6 +52,18 @@ impl TryFrom<api::Attachment> for AIAgentAttachment {
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_read_skill(
|
||||
read_skill: api::message::tool_call::ReadSkill,
|
||||
skill_path_origin: &SkillPathOrigin,
|
||||
) -> Result<AIAgentActionType, ToolToAIAgentActionError> {
|
||||
let Some(reference) = read_skill.skill_reference else {
|
||||
return Err(ToolToAIAgentActionError::MissingSkillReference);
|
||||
};
|
||||
let skill = skill_reference_from_read_skill_ref(reference, skill_path_origin)
|
||||
.map_err(|_| ToolToAIAgentActionError::MissingSkillReference)?;
|
||||
Ok(AIAgentActionType::ReadSkill(ReadSkillRequest { skill }))
|
||||
}
|
||||
|
||||
/// Converts proto UserQueryMode to the internal UserQueryMode type
|
||||
pub(crate) fn convert_user_query_mode(mode: Option<&api::UserQueryMode>) -> UserQueryMode {
|
||||
let Some(mode) = mode else {
|
||||
@@ -81,6 +92,22 @@ fn convert_start_agent_v2_harness_type(
|
||||
.filter(|harness_type| !harness_type.trim().is_empty())
|
||||
}
|
||||
|
||||
/// Maps the proto `Harness` oneof to a client-side string identifier
|
||||
/// (e.g. "oz", "claude"). Returns `None` for an unset variant.
|
||||
pub(crate) fn convert_run_agents_harness(harness: Option<&api::Harness>) -> Option<String> {
|
||||
let variant = harness?.variant.as_ref()?;
|
||||
Some(
|
||||
match variant {
|
||||
api::harness::Variant::Oz(_) => "oz",
|
||||
api::harness::Variant::ClaudeCode(_) => "claude",
|
||||
api::harness::Variant::OpenCode(_) => "opencode",
|
||||
api::harness::Variant::Gemini(_) => "gemini",
|
||||
api::harness::Variant::Codex(_) => "codex",
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn convert_start_agent_execution_mode(
|
||||
execution_mode: Option<api::start_agent::ExecutionMode>,
|
||||
) -> StartAgentExecutionMode {
|
||||
@@ -94,8 +121,62 @@ fn convert_start_agent_execution_mode(
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_run_agents_execution_mode(
|
||||
execution_mode: Option<api::run_agents::ExecutionMode>,
|
||||
) -> RunAgentsExecutionMode {
|
||||
match execution_mode {
|
||||
Some(api::run_agents::ExecutionMode::Remote(remote)) => RunAgentsExecutionMode::Remote {
|
||||
environment_id: remote.environment_id,
|
||||
worker_host: remote.worker_host,
|
||||
computer_use_enabled: remote.computer_use_enabled,
|
||||
},
|
||||
Some(api::run_agents::ExecutionMode::Local(_)) | None => RunAgentsExecutionMode::Local,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_run_agents(
|
||||
run_agents: api::RunAgents,
|
||||
skill_path_origin: &SkillPathOrigin,
|
||||
) -> AIAgentActionType {
|
||||
let api::RunAgents {
|
||||
summary,
|
||||
base_prompt,
|
||||
skills,
|
||||
model_id,
|
||||
harness,
|
||||
agent_run_configs,
|
||||
execution_mode,
|
||||
plan_id,
|
||||
} = run_agents;
|
||||
AIAgentActionType::RunAgents(RunAgentsRequest {
|
||||
summary,
|
||||
base_prompt,
|
||||
skills: skills
|
||||
.into_iter()
|
||||
.filter_map(|skill| skill_reference_from_api_skill_ref(skill, skill_path_origin))
|
||||
.collect(),
|
||||
model_id,
|
||||
harness_type: convert_run_agents_harness(harness.as_ref()).unwrap_or_default(),
|
||||
execution_mode: convert_run_agents_execution_mode(execution_mode),
|
||||
agent_run_configs: agent_run_configs
|
||||
.into_iter()
|
||||
.map(|config| RunAgentsAgentRunConfig {
|
||||
name: config.name,
|
||||
prompt: config.prompt,
|
||||
title: config.title,
|
||||
})
|
||||
.collect(),
|
||||
plan_id,
|
||||
// Auth secret is a client-side dispatch concern populated by the
|
||||
// confirmation card from `CloudAgentSettings.last_selected_auth_secret`
|
||||
// before Accept. The proto does not carry it.
|
||||
harness_auth_secret_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_start_agent_v2_execution_mode(
|
||||
execution_mode: Option<api::start_agent_v2::ExecutionMode>,
|
||||
skill_path_origin: &SkillPathOrigin,
|
||||
) -> StartAgentExecutionMode {
|
||||
match execution_mode.and_then(|execution_mode| execution_mode.mode) {
|
||||
Some(api::start_agent_v2::execution_mode::Mode::Remote(remote)) => {
|
||||
@@ -104,7 +185,9 @@ fn convert_start_agent_v2_execution_mode(
|
||||
skill_references: remote
|
||||
.skills
|
||||
.into_iter()
|
||||
.filter_map(convert_skill_reference)
|
||||
.filter_map(|skill| {
|
||||
skill_reference_from_api_skill_ref(skill, skill_path_origin)
|
||||
})
|
||||
.collect(),
|
||||
model_id: remote.model_id,
|
||||
computer_use_enabled: remote.computer_use_enabled,
|
||||
@@ -112,6 +195,9 @@ fn convert_start_agent_v2_execution_mode(
|
||||
harness_type: convert_start_agent_v2_harness_type(remote.harness)
|
||||
.unwrap_or_default(),
|
||||
title: remote.title,
|
||||
// Auth secret is plumbed client-side via `RunAgentsRequest`;
|
||||
// StartAgentV2 from the server never carries it.
|
||||
auth_secret_name: None,
|
||||
}
|
||||
}
|
||||
Some(api::start_agent_v2::execution_mode::Mode::Local(local)) => {
|
||||
@@ -123,16 +209,6 @@ fn convert_start_agent_v2_execution_mode(
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_skill_reference(skill_ref: api::SkillRef) -> Option<SkillReference> {
|
||||
match skill_ref.skill_reference {
|
||||
Some(api::skill_ref::SkillReference::Path(path)) => Some(SkillReference::Path(path.into())),
|
||||
Some(api::skill_ref::SkillReference::BundledSkillId(id)) => {
|
||||
Some(SkillReference::BundledSkillId(id))
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Unexpected errors when trying to convert an [`api::Message`] to an [`AIAgentOutputMessage`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum MessageToAIAgentOutputMessageError {
|
||||
@@ -167,6 +243,7 @@ pub struct ConversionParams<'a> {
|
||||
pub task_id: &'a TaskId,
|
||||
pub current_todo_list: Option<&'a AIAgentTodoList>,
|
||||
pub active_code_review: Option<&'a CodeReview>,
|
||||
pub skill_path_origin: &'a SkillPathOrigin,
|
||||
}
|
||||
|
||||
/// Trait for converting an [`api::Message`] to an [`AIAgentOutputMessage`].
|
||||
@@ -569,7 +646,11 @@ impl ConvertAPIMessageToClientOutputMessage for api::Message {
|
||||
| api::message::Message::CodeReview(_)
|
||||
| api::message::Message::ServerEvent(_)
|
||||
| api::message::Message::InvokeSkill(_)
|
||||
| api::message::Message::PassiveSuggestionResult(_) => {
|
||||
| api::message::Message::PassiveSuggestionResult(_)
|
||||
// Stage 2 plan-card config snapshot: hydrated separately by the
|
||||
// plan card's `AIDocumentModel` subscription, not via the
|
||||
// exchange/output stream. No client output message representation.
|
||||
| api::message::Message::OrchestrationConfigSnapshot(_) => {
|
||||
Ok(MaybeAIAgentOutputMessage::NoClientRepresentation)
|
||||
}
|
||||
}
|
||||
@@ -600,7 +681,7 @@ trait ConvertAPIToolCallToAIAgentAction {
|
||||
) -> Result<MaybeAIAgentAction, ToolToAIAgentActionError>;
|
||||
}
|
||||
|
||||
/// Trys to convert an [`api::message::ToolCall`] to an [`AIAgentAction`].
|
||||
/// Tries to convert an [`api::message::ToolCall`] to an [`AIAgentAction`].
|
||||
///
|
||||
/// A [`Result::Error`] indicates an unexpected problem, while [`Ok(None)`]
|
||||
/// indicates a tool call that we aren't expected to parse.
|
||||
@@ -700,6 +781,7 @@ impl ConvertAPIToolCallToAIAgentAction for api::message::ToolCall {
|
||||
create_standard_action(request_computer_use.into())
|
||||
}
|
||||
api::message::tool_call::Tool::Subagent(subagent) => {
|
||||
use api::message::tool_call::subagent::conversation_search_metadata::Target;
|
||||
use api::message::tool_call::subagent::Metadata;
|
||||
let subagent_type = match subagent.metadata {
|
||||
Some(Metadata::Cli(_)) => SubagentType::Cli,
|
||||
@@ -713,14 +795,23 @@ impl ConvertAPIToolCallToAIAgentAction for api::message::ToolCall {
|
||||
} else {
|
||||
Some(cs_meta.query)
|
||||
};
|
||||
let conversation_id = if cs_meta.conversation_id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(cs_meta.conversation_id)
|
||||
let (conversation_id, agent_run_id) = match cs_meta.target {
|
||||
Some(Target::ConversationId(conversation_id))
|
||||
if !conversation_id.is_empty() =>
|
||||
{
|
||||
(Some(conversation_id), None)
|
||||
}
|
||||
Some(Target::AgentRunId(agent_run_id)) if !agent_run_id.is_empty() => {
|
||||
(None, Some(agent_run_id))
|
||||
}
|
||||
Some(Target::ConversationId(_))
|
||||
| Some(Target::AgentRunId(_))
|
||||
| None => (None, None),
|
||||
};
|
||||
SubagentType::ConversationSearch {
|
||||
query,
|
||||
conversation_id,
|
||||
agent_run_id,
|
||||
}
|
||||
}
|
||||
Some(Metadata::WarpDocumentationSearch(_)) => {
|
||||
@@ -757,6 +848,7 @@ impl ConvertAPIToolCallToAIAgentAction for api::message::ToolCall {
|
||||
prompt: start_agent.prompt,
|
||||
execution_mode: convert_start_agent_v2_execution_mode(
|
||||
start_agent.execution_mode,
|
||||
params.skill_path_origin,
|
||||
),
|
||||
lifecycle_subscription: start_agent.lifecycle_subscription.map(
|
||||
|subscription| {
|
||||
@@ -769,6 +861,9 @@ impl ConvertAPIToolCallToAIAgentAction for api::message::ToolCall {
|
||||
),
|
||||
})
|
||||
}
|
||||
api::message::tool_call::Tool::RunAgents(orchestrate) => {
|
||||
create_standard_action(convert_run_agents(orchestrate, params.skill_path_origin))
|
||||
}
|
||||
api::message::tool_call::Tool::SendMessageToAgent(send_message) => {
|
||||
create_standard_action(AIAgentActionType::SendMessageToAgent {
|
||||
addresses: send_message.addresses,
|
||||
@@ -780,7 +875,7 @@ impl ConvertAPIToolCallToAIAgentAction for api::message::ToolCall {
|
||||
create_standard_action(insert_review_comments.into())
|
||||
}
|
||||
api::message::tool_call::Tool::ReadSkill(read_skill) => {
|
||||
create_standard_action(read_skill.try_into()?)
|
||||
create_standard_action(convert_read_skill(read_skill, params.skill_path_origin)?)
|
||||
}
|
||||
api::message::tool_call::Tool::FetchConversation(fetch_conversation) => {
|
||||
create_standard_action(fetch_conversation.into())
|
||||
@@ -798,6 +893,12 @@ impl ConvertAPIToolCallToAIAgentAction for api::message::ToolCall {
|
||||
api::message::tool_call::Tool::Server(_) => {
|
||||
Ok(MaybeAIAgentAction::NoClientRepresentation)
|
||||
}
|
||||
api::message::tool_call::Tool::WaitForEvents(payload) => {
|
||||
create_standard_action(AIAgentActionType::WaitForEvents {
|
||||
tool_call_id: self.tool_call_id.clone(),
|
||||
idle_timeout_seconds: payload.idle_timeout_seconds,
|
||||
})
|
||||
}
|
||||
_ => Err(ToolToAIAgentActionError::UnexpectedTool),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ai::agent::action::AskUserQuestionType;
|
||||
use ai::skills::{SkillPathOrigin, SkillReference};
|
||||
use warp_multi_agent_api as api;
|
||||
use warp_util::local_or_remote_path::LocalOrRemotePath;
|
||||
|
||||
use super::{
|
||||
convert_api_question, ConversionParams, ConvertAPIMessageToClientOutputMessage,
|
||||
MaybeAIAgentOutputMessage,
|
||||
@@ -6,9 +13,6 @@ use crate::ai::agent::task::TaskId;
|
||||
use crate::ai::agent::{
|
||||
AIAgentActionType, AIAgentOutputMessageType, LifecycleEventType, StartAgentExecutionMode,
|
||||
};
|
||||
use ai::agent::action::AskUserQuestionType;
|
||||
use ai::skills::SkillReference;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
fn start_agent_tool_call_message(
|
||||
name: &str,
|
||||
@@ -17,6 +21,7 @@ fn start_agent_tool_call_message(
|
||||
lifecycle_subscription_event_types: Option<Vec<i32>>,
|
||||
) -> api::Message {
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "message-id".to_string(),
|
||||
task_id: "task-id".to_string(),
|
||||
server_message_data: String::new(),
|
||||
@@ -63,6 +68,7 @@ fn start_agent_v2_tool_call_message(
|
||||
lifecycle_subscription_event_types: Option<Vec<i32>>,
|
||||
) -> api::Message {
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "message-id".to_string(),
|
||||
task_id: "task-id".to_string(),
|
||||
server_message_data: String::new(),
|
||||
@@ -87,6 +93,7 @@ fn start_agent_v2_tool_call_message(
|
||||
|
||||
fn upload_artifact_tool_call_message(path: &str, description: &str) -> api::Message {
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "message-id".to_string(),
|
||||
task_id: "task-id".to_string(),
|
||||
server_message_data: String::new(),
|
||||
@@ -140,6 +147,7 @@ fn remote_start_agent_v2_execution_mode(
|
||||
|
||||
fn file_artifact_created_message(filepath: &str, description: &str) -> api::Message {
|
||||
api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "message-id".to_string(),
|
||||
task_id: "task-id".to_string(),
|
||||
server_message_data: String::new(),
|
||||
@@ -298,6 +306,7 @@ fn converts_start_agent_tool_call_to_action_with_prompt() {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
skill_path_origin: &SkillPathOrigin::Local,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
@@ -327,6 +336,7 @@ fn converts_local_start_agent_v2_without_harness_type_to_defaults() {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
skill_path_origin: &SkillPathOrigin::Local,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
@@ -354,6 +364,7 @@ fn converts_upload_artifact_tool_call_to_action() {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
skill_path_origin: &SkillPathOrigin::Local,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
@@ -377,6 +388,7 @@ fn converts_file_artifact_created_message_with_filename() {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
skill_path_origin: &SkillPathOrigin::Local,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
@@ -407,6 +419,7 @@ fn converts_start_agent_tool_calls_with_different_prompt_lengths() {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
skill_path_origin: &SkillPathOrigin::Local,
|
||||
})
|
||||
.expect("partial conversion should succeed");
|
||||
let updated_output = updated_message
|
||||
@@ -414,6 +427,7 @@ fn converts_start_agent_tool_calls_with_different_prompt_lengths() {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
skill_path_origin: &SkillPathOrigin::Local,
|
||||
})
|
||||
.expect("updated conversion should succeed");
|
||||
|
||||
@@ -442,6 +456,7 @@ fn converts_start_agent_with_explicit_empty_lifecycle_subscription() {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
skill_path_origin: &SkillPathOrigin::Local,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
@@ -471,6 +486,7 @@ fn converts_start_agent_with_cancelled_and_blocked_lifecycle_subscription() {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
skill_path_origin: &SkillPathOrigin::Local,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
@@ -503,6 +519,7 @@ fn converts_remote_start_agent_with_environment_id() {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
skill_path_origin: &SkillPathOrigin::Local,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
@@ -520,6 +537,7 @@ fn converts_remote_start_agent_with_environment_id() {
|
||||
worker_host: String::new(),
|
||||
harness_type: String::new(),
|
||||
title: String::new(),
|
||||
auth_secret_name: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(lifecycle_subscription, None);
|
||||
@@ -540,6 +558,7 @@ fn converts_remote_start_agent_v2_with_skill_references() {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
skill_path_origin: &SkillPathOrigin::Local,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
@@ -552,7 +571,7 @@ fn converts_remote_start_agent_v2_with_skill_references() {
|
||||
StartAgentExecutionMode::Remote {
|
||||
environment_id: "env-123".to_string(),
|
||||
skill_references: vec![
|
||||
SkillReference::Path("/tmp/SKILL.md".into()),
|
||||
SkillReference::Path(LocalOrRemotePath::Local(PathBuf::from("/tmp/SKILL.md",))),
|
||||
SkillReference::BundledSkillId("review-comments".to_string()),
|
||||
],
|
||||
model_id: "gpt-test".to_string(),
|
||||
@@ -560,6 +579,7 @@ fn converts_remote_start_agent_v2_with_skill_references() {
|
||||
worker_host: "worker-host".to_string(),
|
||||
harness_type: "claude-code".to_string(),
|
||||
title: "Remote child".to_string(),
|
||||
auth_secret_name: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(lifecycle_subscription, None);
|
||||
@@ -580,6 +600,7 @@ fn converts_local_start_agent_v2_with_harness_type() {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
skill_path_origin: &SkillPathOrigin::Local,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
@@ -599,6 +620,7 @@ fn transfer_control_tool_call_converts_to_action_message() {
|
||||
let task_id = TaskId::new("task".to_string());
|
||||
let reason = "Please finish the interactive flow".to_string();
|
||||
let message = api::Message {
|
||||
fetched_memories: vec![],
|
||||
id: "message".to_string(),
|
||||
task_id: "task".to_string(),
|
||||
server_message_data: String::new(),
|
||||
@@ -622,6 +644,7 @@ fn transfer_control_tool_call_converts_to_action_message() {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
skill_path_origin: &SkillPathOrigin::Local,
|
||||
})
|
||||
.expect("transfer-control conversion should succeed");
|
||||
|
||||
|
||||
@@ -5,14 +5,12 @@ use anyhow::anyhow;
|
||||
use chrono::{DateTime, Local, Timelike};
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use crate::ai::{
|
||||
agent::{
|
||||
AIAgentActionResult, AIAgentActionResultType, AIAgentAttachment, AIAgentContext,
|
||||
AIAgentInput, DriveObjectPayload, MCPContext, PassiveSuggestionResultType,
|
||||
PassiveSuggestionTrigger, RunningCommand, StaticQueryType, Suggestions, UserQueryMode,
|
||||
},
|
||||
block_context::BlockContext,
|
||||
use crate::ai::agent::{
|
||||
AIAgentActionResult, AIAgentActionResultType, AIAgentAttachment, AIAgentContext, AIAgentInput,
|
||||
DriveObjectPayload, MCPContext, PassiveSuggestionResultType, PassiveSuggestionTrigger,
|
||||
RunningCommand, StaticQueryType, Suggestions, UserQueryMode,
|
||||
};
|
||||
use crate::ai::block_context::BlockContext;
|
||||
|
||||
fn local_datetime_to_timestamp(timestamp: DateTime<Local>) -> prost_types::Timestamp {
|
||||
prost_types::Timestamp {
|
||||
@@ -46,11 +44,6 @@ impl TryFrom<StaticQueryType> for api::request::input::query_with_canned_respons
|
||||
api::request::input::query_with_canned_response::SomethingElse {},
|
||||
),
|
||||
),
|
||||
StaticQueryType::CustomOnboardingRequest => Ok(
|
||||
api::request::input::query_with_canned_response::Type::CustomOnboardingRequest(
|
||||
api::request::input::query_with_canned_response::CustomOnboardingRequest {},
|
||||
),
|
||||
),
|
||||
StaticQueryType::EvaluationSuite => {
|
||||
Err(anyhow::anyhow!("EvaluationSuite StaticQueryType not yet supported").into())
|
||||
}
|
||||
@@ -209,9 +202,9 @@ pub(super) fn convert_input(
|
||||
)),
|
||||
});
|
||||
}
|
||||
AIAgentInput::SummarizeConversation { prompt } => {
|
||||
AIAgentInput::SummarizeConversation { prompt, context } => {
|
||||
return Ok(api::request::Input {
|
||||
context: None,
|
||||
context: Some(convert_context(context.as_ref())),
|
||||
r#type: Some(api::request::input::Type::SummarizeConversation(
|
||||
api::request::input::SummarizeConversation {
|
||||
prompt: prompt.unwrap_or_default(),
|
||||
@@ -435,6 +428,19 @@ fn convert_input_to_user_input(
|
||||
),
|
||||
)
|
||||
}
|
||||
AIAgentInput::OrchestrationConfigUpdate {
|
||||
plan_id,
|
||||
config,
|
||||
status,
|
||||
} => Ok(
|
||||
api::request::input::user_inputs::user_input::Input::OrchestrationConfigUpdate(
|
||||
api::OrchestrationConfigUpdate {
|
||||
plan_id,
|
||||
config: Some(config.to_proto()),
|
||||
status: status.to_proto(),
|
||||
},
|
||||
),
|
||||
),
|
||||
AIAgentInput::ResumeConversation { .. } => Err(ConvertToAPITypeError::Ignore),
|
||||
AIAgentInput::InitProjectRules { .. } => Err(ConvertToAPITypeError::Ignore),
|
||||
AIAgentInput::CodeReview { .. } => Err(ConvertToAPITypeError::Ignore),
|
||||
@@ -692,6 +698,12 @@ impl TryFrom<AIAgentActionResult> for api::request::input::user_inputs::user_inp
|
||||
AIAgentActionResultType::AskUserQuestion(ask_user_question_result) => {
|
||||
Some(ask_user_question_result.into())
|
||||
}
|
||||
AIAgentActionResultType::RunAgents(orchestrate_result) => {
|
||||
Some(orchestrate_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::WaitForEvents(wait_for_events_result) => {
|
||||
Some(wait_for_events_result.try_into()?)
|
||||
}
|
||||
};
|
||||
Ok(
|
||||
api::request::input::user_inputs::user_input::Input::ToolCallResult(
|
||||
@@ -706,6 +718,7 @@ impl TryFrom<AIAgentActionResult> for api::request::input::user_inputs::user_inp
|
||||
|
||||
fn convert_context(context: &[AIAgentContext]) -> api::InputContext {
|
||||
let mut api_context = api::InputContext::default();
|
||||
let mut git_context = None;
|
||||
for context in context.iter().cloned() {
|
||||
match context {
|
||||
AIAgentContext::Block(block) => {
|
||||
@@ -789,11 +802,40 @@ fn convert_context(context: &[AIAgentContext]) -> api::InputContext {
|
||||
}
|
||||
}
|
||||
AIAgentContext::Git { head, branch } => {
|
||||
api_context.git = Some(api::input_context::Git {
|
||||
head,
|
||||
branch: branch.unwrap_or_default(),
|
||||
let api_git_context =
|
||||
git_context.get_or_insert_with(api::input_context::Git::default);
|
||||
api_git_context.head = head;
|
||||
api_git_context.branch = branch.unwrap_or_default();
|
||||
}
|
||||
AIAgentContext::Repository { name, owner } => {
|
||||
let api_git_context =
|
||||
git_context.get_or_insert_with(api::input_context::Git::default);
|
||||
api_git_context.repository = Some(api::input_context::git::Repository {
|
||||
name,
|
||||
owner: owner.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
AIAgentContext::PullRequest {
|
||||
number,
|
||||
state,
|
||||
draft,
|
||||
base_branch,
|
||||
} => {
|
||||
if number <= 0 {
|
||||
continue;
|
||||
}
|
||||
let Some(state) = api_pull_request_state(&state, draft) else {
|
||||
continue;
|
||||
};
|
||||
let pull_request = api::input_context::git::PullRequest {
|
||||
number,
|
||||
state: state as i32,
|
||||
base_branch,
|
||||
};
|
||||
let api_git_context =
|
||||
git_context.get_or_insert_with(api::input_context::Git::default);
|
||||
api_git_context.pull_request = Some(pull_request);
|
||||
}
|
||||
AIAgentContext::Skills { skills } => {
|
||||
api_context.updated_skills_context = Some(api::input_context::SkillsContext {
|
||||
available_skills: skills
|
||||
@@ -810,9 +852,34 @@ fn convert_context(context: &[AIAgentContext]) -> api::InputContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
api_context.git = git_context;
|
||||
api_context
|
||||
}
|
||||
|
||||
/// Maps a GitHub PR state plus draft flag to the proto `State` enum.
|
||||
///
|
||||
/// Returns `None` for unknown states so the caller can skip emitting a
|
||||
/// `pull_request` sub-message rather than sending `STATE_UNSPECIFIED` to the
|
||||
/// server.
|
||||
fn api_pull_request_state(
|
||||
state: &str,
|
||||
draft: bool,
|
||||
) -> Option<api::input_context::git::pull_request::State> {
|
||||
use api::input_context::git::pull_request::State;
|
||||
match state.to_ascii_uppercase().as_str() {
|
||||
"OPEN" => {
|
||||
if draft {
|
||||
Some(State::OpenDraft)
|
||||
} else {
|
||||
Some(State::Open)
|
||||
}
|
||||
}
|
||||
"CLOSED" => Some(State::Closed),
|
||||
"MERGED" => Some(State::Merged),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Suggestions> for api::Suggestions {
|
||||
fn from(value: Suggestions) -> Self {
|
||||
Self {
|
||||
@@ -940,12 +1007,13 @@ impl From<BlockContext> for api::ExecutedShellCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/// Trys to convert a [`serde_json::Value`] to a [`prost_types::Value`].
|
||||
/// Tries to convert a [`serde_json::Value`] to a [`prost_types::Value`].
|
||||
#[cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
fn serde_json_to_prost(value: serde_json::Value) -> Result<prost_types::Value, String> {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use prost_types::value::Kind::*;
|
||||
use serde_json::Value::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
Ok(prost_types::Value {
|
||||
kind: Some(match value {
|
||||
|
||||
@@ -1,11 +1,111 @@
|
||||
use crate::ai::agent::task::TaskId;
|
||||
use crate::ai::agent::{
|
||||
AIAgentActionResult, AIAgentActionResultType, TransferShellCommandControlToUserResult,
|
||||
};
|
||||
use crate::terminal::model::block::BlockId;
|
||||
use chrono::{DateTime, Utc};
|
||||
use galaxy_core::command::ExitCode;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use crate::ai::agent::task::TaskId;
|
||||
use crate::ai::agent::{
|
||||
AIAgentActionResult, AIAgentActionResultType, AIAgentContext,
|
||||
TransferShellCommandControlToUserResult,
|
||||
};
|
||||
use crate::terminal::model::block::BlockId;
|
||||
|
||||
#[test]
|
||||
fn git_context_converts_repository_and_pull_request_metadata() {
|
||||
let context = vec![
|
||||
AIAgentContext::Git {
|
||||
head: "abc123".to_string(),
|
||||
branch: Some("feature/repo-pr".to_string()),
|
||||
},
|
||||
AIAgentContext::Repository {
|
||||
name: "warp-internal".to_string(),
|
||||
owner: Some("warpdotdev".to_string()),
|
||||
},
|
||||
AIAgentContext::PullRequest {
|
||||
number: 42,
|
||||
state: "OPEN".to_string(),
|
||||
draft: true,
|
||||
base_branch: "main".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
let api_context = super::convert_context(&context);
|
||||
let git = api_context.git.expect("expected git context");
|
||||
assert_eq!(git.head, "abc123");
|
||||
assert_eq!(git.branch, "feature/repo-pr");
|
||||
|
||||
let repository = git.repository.expect("expected repository context");
|
||||
assert_eq!(repository.name, "warp-internal");
|
||||
assert_eq!(repository.owner, "warpdotdev");
|
||||
|
||||
let pull_request = git.pull_request.expect("expected pull request context");
|
||||
assert_eq!(pull_request.number, 42);
|
||||
assert_eq!(
|
||||
pull_request.state,
|
||||
api::input_context::git::pull_request::State::OpenDraft as i32
|
||||
);
|
||||
assert_eq!(pull_request.base_branch, "main");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_context_skips_pull_request_metadata_with_invalid_number() {
|
||||
for number in [0, -1] {
|
||||
let context = vec![
|
||||
AIAgentContext::Git {
|
||||
head: "abc123".to_string(),
|
||||
branch: Some("feature/repo-pr".to_string()),
|
||||
},
|
||||
AIAgentContext::PullRequest {
|
||||
number,
|
||||
state: "OPEN".to_string(),
|
||||
draft: false,
|
||||
base_branch: "main".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
let api_context = super::convert_context(&context);
|
||||
let git = api_context.git.expect("expected git context");
|
||||
assert_eq!(git.head, "abc123");
|
||||
assert_eq!(git.branch, "feature/repo-pr");
|
||||
assert_eq!(git.pull_request, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_context_skips_pull_request_metadata_with_unknown_state() {
|
||||
let context = vec![
|
||||
AIAgentContext::Git {
|
||||
head: "abc123".to_string(),
|
||||
branch: Some("feature/repo-pr".to_string()),
|
||||
},
|
||||
AIAgentContext::PullRequest {
|
||||
number: 42,
|
||||
state: "SOMETHING_ELSE".to_string(),
|
||||
draft: false,
|
||||
base_branch: "main".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
let api_context = super::convert_context(&context);
|
||||
let git = api_context.git.expect("expected git context");
|
||||
assert_eq!(git.pull_request, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_context_deserializes_legacy_string_pull_request_number() {
|
||||
let pull_request = serde_json::from_str::<AIAgentContext>(
|
||||
r#"{"PullRequest":{"number":"42","state":"OPEN","draft":false,"base_branch":"main"}}"#,
|
||||
)
|
||||
.expect("expected legacy serialized pull request context");
|
||||
|
||||
let api_context = super::convert_context(&[pull_request]);
|
||||
let pull_request = api_context
|
||||
.git
|
||||
.expect("expected git context")
|
||||
.pull_request
|
||||
.expect("expected pull request context");
|
||||
assert_eq!(pull_request.number, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_control_snapshot_result_converts_to_tool_call_result_input() {
|
||||
let block_id = BlockId::default();
|
||||
@@ -51,6 +151,8 @@ fn transfer_control_snapshot_result_converts_to_tool_call_result_input() {
|
||||
#[test]
|
||||
fn transfer_control_finished_result_converts_to_tool_call_result_input() {
|
||||
let block_id = BlockId::default();
|
||||
let start_ts = DateTime::from(Utc::now());
|
||||
let completed_ts = DateTime::from(Utc::now());
|
||||
let input =
|
||||
api::request::input::user_inputs::user_input::Input::try_from(AIAgentActionResult {
|
||||
id: "tool_call".to_string().into(),
|
||||
@@ -60,6 +162,8 @@ fn transfer_control_finished_result_converts_to_tool_call_result_input() {
|
||||
block_id: block_id.clone(),
|
||||
output: "done".to_string(),
|
||||
exit_code: ExitCode::from(17),
|
||||
start_ts: Some(start_ts),
|
||||
completed_ts: Some(completed_ts),
|
||||
},
|
||||
),
|
||||
})
|
||||
@@ -78,6 +182,8 @@ fn transfer_control_finished_result_converts_to_tool_call_result_input() {
|
||||
assert_eq!(finished.command_id, block_id.to_string());
|
||||
assert_eq!(finished.output, "done");
|
||||
assert_eq!(finished.exit_code, 17);
|
||||
assert_eq!(finished.start_ts, Some(super::local_datetime_to_timestamp(start_ts)));
|
||||
assert_eq!(finished.finish_ts, Some(super::local_datetime_to_timestamp(completed_ts)));
|
||||
}
|
||||
other => panic!("Expected command-finished result, got {other:?}"),
|
||||
},
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{ai::agent::redaction, terminal::model::session::SessionType};
|
||||
use futures_util::StreamExt;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use crate::ai::bedrock::translator::{self, TranslatorRequest};
|
||||
use crate::ai::openai::translator as openai_translator;
|
||||
use crate::ai::provider::ProviderConfig;
|
||||
|
||||
use super::{convert_to::convert_input, ConvertToAPITypeError, RequestParams, ResponseStream};
|
||||
use super::convert_to::convert_input;
|
||||
use super::{ConvertToAPITypeError, RequestParams, ResponseStream};
|
||||
use crate::ai::agent::redaction;
|
||||
use crate::server::server_api::{AIApiError, ServerApi};
|
||||
use crate::terminal::model::session::SessionType;
|
||||
|
||||
pub async fn generate_multi_agent_output(
|
||||
provider_config: ProviderConfig,
|
||||
@@ -53,10 +53,10 @@ pub async fn generate_multi_agent_output(
|
||||
redaction::redact_inputs(&mut params.input);
|
||||
}
|
||||
|
||||
let mut api_keys = params.api_keys;
|
||||
if let Some(api_keys) = &mut api_keys {
|
||||
api_keys.allow_use_of_warp_credits = params.allow_use_of_warp_credits_with_byok;
|
||||
}
|
||||
let api_keys = api_keys_with_warp_credit_fallback_setting(
|
||||
params.api_keys,
|
||||
params.allow_use_of_warp_credits,
|
||||
);
|
||||
|
||||
let mut request = api::Request {
|
||||
task_context: Some(api::request::TaskContext {
|
||||
@@ -68,6 +68,7 @@ pub async fn generate_multi_agent_output(
|
||||
base: params.model.into(),
|
||||
cli_agent: params.cli_agent_model.into(),
|
||||
computer_use_agent: params.computer_use_model.into(),
|
||||
base_model_context_window_limit: params.context_window_limit.unwrap_or(0),
|
||||
..Default::default()
|
||||
}),
|
||||
rules_enabled: params.is_memory_enabled,
|
||||
@@ -99,7 +100,11 @@ pub async fn generate_multi_agent_output(
|
||||
FeatureFlag::SummarizationViaMessageReplacement.is_enabled(),
|
||||
supports_bundled_skills: FeatureFlag::BundledSkills.is_enabled(),
|
||||
supports_research_agent: params.research_agent_enabled,
|
||||
supports_orchestration_v2: FeatureFlag::OrchestrationV2.is_enabled(),
|
||||
supports_orchestration_v2: supports_orchestration_v2(params.orchestration_enabled),
|
||||
supports_background_computer_use: FeatureFlag::BackgroundComputerUse.is_enabled()
|
||||
&& computer_use::background_supported(),
|
||||
custom_model_providers: params.custom_model_providers,
|
||||
custom_model_routers: params.custom_model_routers,
|
||||
}),
|
||||
metadata: Some(api::request::Metadata {
|
||||
logging: logging_metadata,
|
||||
@@ -113,6 +118,8 @@ pub async fn generate_multi_agent_output(
|
||||
.map(|id| id.to_string())
|
||||
.unwrap_or_default(),
|
||||
forked_from_conversation_id: if params.conversation_token.is_none() {
|
||||
// We only include this param on our initial request to the server
|
||||
// (when the forked conversation has not been assigned a new id yet).
|
||||
params
|
||||
.forked_from_conversation_token
|
||||
.map(|token| token.as_str().to_string())
|
||||
@@ -129,41 +136,19 @@ pub async fn generate_multi_agent_output(
|
||||
mcp_context: params.mcp_context.map(Into::into),
|
||||
};
|
||||
|
||||
let model_id = request
|
||||
.settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.model_config.as_ref())
|
||||
.map(|mc| mc.base.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
match provider_config {
|
||||
ProviderConfig::Bedrock(config) => {
|
||||
let translator_request = TranslatorRequest {
|
||||
config,
|
||||
model_id,
|
||||
root_task_id: params.root_task_id.clone(),
|
||||
bedrock_message_history: params.bedrock_message_history.clone(),
|
||||
bedrock_tool_result_archive: params.bedrock_tool_result_archive.clone(),
|
||||
bedrock_progressive_summary: params.bedrock_progressive_summary.clone(),
|
||||
bedrock_messages_sent: params.bedrock_messages_sent.clone(),
|
||||
};
|
||||
|
||||
match translator::execute(translator_request, &mut request).await {
|
||||
Ok(stream) => {
|
||||
let output_stream = stream.take_until(cancellation_rx);
|
||||
Ok(Box::pin(output_stream))
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("[bedrock] Translator error: {e}");
|
||||
let err = Arc::new(crate::server::server_api::AIApiError::Stream {
|
||||
stream_type: "bedrock_converse",
|
||||
source: anyhow::anyhow!("{e}"),
|
||||
});
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
let _ = tx.send(Err(err)).await;
|
||||
Ok(Box::pin(rx))
|
||||
}
|
||||
}
|
||||
let response_stream =
|
||||
warp_multi_agent_client::generate_multi_agent_output(server_api.as_ref(), &request).await;
|
||||
match response_stream {
|
||||
Ok(stream) => {
|
||||
let output_stream = stream
|
||||
.then(|result| async {
|
||||
match result {
|
||||
Ok(event) => Ok(event),
|
||||
Err(error) => Err(convert_multi_agent_client_error(error).await),
|
||||
}
|
||||
})
|
||||
.take_until(cancellation_rx);
|
||||
Ok(Box::pin(output_stream))
|
||||
}
|
||||
ProviderConfig::OpenAI(config) => {
|
||||
let translator_request = openai_translator::TranslatorRequest {
|
||||
@@ -202,12 +187,53 @@ pub async fn generate_multi_agent_output(
|
||||
),
|
||||
});
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
let _ = tx.send(Err(err)).await;
|
||||
let _ = tx
|
||||
.send(Err(convert_multi_agent_client_error(e).await))
|
||||
.await;
|
||||
Ok(Box::pin(rx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn convert_multi_agent_client_error(
|
||||
error: warp_multi_agent_client::Error,
|
||||
) -> Arc<AIApiError> {
|
||||
let error = match error {
|
||||
warp_multi_agent_client::Error::Authentication(error)
|
||||
| warp_multi_agent_client::Error::AmbientHeaders(error) => AIApiError::Other(error),
|
||||
warp_multi_agent_client::Error::Base64Decode(error) => {
|
||||
AIApiError::Other(anyhow::Error::from(error))
|
||||
}
|
||||
warp_multi_agent_client::Error::ProtobufDecode(error) => {
|
||||
AIApiError::Other(anyhow::Error::from(error))
|
||||
}
|
||||
warp_multi_agent_client::Error::EventSource(error) => {
|
||||
AIApiError::from_stream_error("GenerateMultiAgentOutput", *error).await
|
||||
}
|
||||
};
|
||||
Arc::new(error)
|
||||
}
|
||||
|
||||
fn api_keys_with_warp_credit_fallback_setting(
|
||||
api_keys: Option<api::request::settings::ApiKeys>,
|
||||
allow_use_of_warp_credits: bool,
|
||||
) -> Option<api::request::settings::ApiKeys> {
|
||||
match api_keys {
|
||||
Some(mut api_keys) => {
|
||||
api_keys.allow_use_of_warp_credits = allow_use_of_warp_credits;
|
||||
Some(api_keys)
|
||||
}
|
||||
None if allow_use_of_warp_credits => Some(api::request::settings::ApiKeys {
|
||||
allow_use_of_warp_credits: true,
|
||||
..Default::default()
|
||||
}),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_orchestration_v2(orchestration_enabled: bool) -> bool {
|
||||
orchestration_enabled
|
||||
}
|
||||
fn get_supported_tools(params: &RequestParams) -> Vec<api::ToolType> {
|
||||
let mut supported_tools = vec![
|
||||
api::ToolType::Grep,
|
||||
@@ -245,7 +271,14 @@ fn get_supported_tools(params: &RequestParams) -> Vec<api::ToolType> {
|
||||
}
|
||||
}
|
||||
Some(SessionType::WarpifiedRemote { host_id: Some(_) }) => {
|
||||
// Remote session with a known host — enable tools that route
|
||||
// through RemoteServerClient. The host_id is only populated
|
||||
// after a successful connection handshake, so its presence is a
|
||||
// sufficient proxy for client availability.
|
||||
supported_tools.extend(&[api::ToolType::ReadFiles, api::ToolType::ApplyFileDiffs]);
|
||||
if FeatureFlag::RemoteCodebaseIndexing.is_enabled() {
|
||||
supported_tools.push(api::ToolType::SearchCodebase);
|
||||
}
|
||||
}
|
||||
Some(SessionType::WarpifiedRemote { host_id: None }) => {}
|
||||
}
|
||||
@@ -264,12 +297,10 @@ fn get_supported_tools(params: &RequestParams) -> Vec<api::ToolType> {
|
||||
}
|
||||
|
||||
if params.orchestration_enabled {
|
||||
supported_tools.push(if FeatureFlag::OrchestrationV2.is_enabled() {
|
||||
api::ToolType::StartAgentV2
|
||||
} else {
|
||||
api::ToolType::StartAgent
|
||||
});
|
||||
supported_tools.push(api::ToolType::SendMessageToAgent);
|
||||
supported_tools.extend([api::ToolType::RunAgents, api::ToolType::SendMessageToAgent]);
|
||||
// Declare client-handled wait_for_events so the server doesn't
|
||||
// fall back to the legacy server-handled form.
|
||||
supported_tools.push(api::ToolType::WaitForEvents);
|
||||
}
|
||||
|
||||
if FeatureFlag::AskUserQuestion.is_enabled() && params.ask_user_question_enabled {
|
||||
@@ -299,6 +330,9 @@ fn get_supported_cli_agent_tools(params: &RequestParams) -> Vec<api::ToolType> {
|
||||
}
|
||||
Some(SessionType::WarpifiedRemote { host_id: Some(_) }) => {
|
||||
supported_cli_agent_tools.push(api::ToolType::ReadFiles);
|
||||
if FeatureFlag::RemoteCodebaseIndexing.is_enabled() {
|
||||
supported_cli_agent_tools.push(api::ToolType::SearchCodebase);
|
||||
}
|
||||
}
|
||||
Some(SessionType::WarpifiedRemote { host_id: None }) => {}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::HostId;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use super::{
|
||||
api_keys_with_warp_credit_fallback_setting, get_supported_cli_agent_tools, get_supported_tools,
|
||||
supports_orchestration_v2,
|
||||
};
|
||||
use crate::ai::agent::api::RequestParams;
|
||||
use crate::ai::blocklist::SessionContext;
|
||||
use crate::ai::llms::LLMId;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use super::get_supported_tools;
|
||||
use crate::terminal::model::session::SessionType;
|
||||
|
||||
fn request_params_with_ask_user_question_enabled(ask_user_question_enabled: bool) -> RequestParams {
|
||||
let model = LLMId::from("test-model");
|
||||
@@ -24,11 +29,14 @@ fn request_params_with_ask_user_question_enabled(ask_user_question_enabled: bool
|
||||
computer_use_model: model,
|
||||
is_memory_enabled: false,
|
||||
warp_drive_context_enabled: false,
|
||||
context_window_limit: None,
|
||||
mcp_context: None,
|
||||
planning_enabled: true,
|
||||
should_redact_secrets: false,
|
||||
api_keys: None,
|
||||
allow_use_of_warp_credits_with_byok: false,
|
||||
custom_model_providers: None,
|
||||
custom_model_routers: None,
|
||||
allow_use_of_warp_credits: false,
|
||||
autonomy_level: api::AutonomyLevel::Supervised,
|
||||
isolation_level: api::IsolationLevel::None,
|
||||
web_search_enabled: false,
|
||||
@@ -47,6 +55,85 @@ fn request_params_with_ask_user_question_enabled(ask_user_question_enabled: bool
|
||||
}
|
||||
}
|
||||
|
||||
fn request_params_for_remote(host_id: Option<HostId>) -> RequestParams {
|
||||
let mut params = request_params_with_ask_user_question_enabled(false);
|
||||
params.session_context =
|
||||
SessionContext::new_with_session_type_for_test(Some(SessionType::WarpifiedRemote {
|
||||
host_id,
|
||||
}));
|
||||
params
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_keys_with_warp_credit_fallback_setting_returns_none_without_keys_or_fallback() {
|
||||
let api_keys = api_keys_with_warp_credit_fallback_setting(None, false);
|
||||
|
||||
assert!(api_keys.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_keys_with_warp_credit_fallback_setting_creates_fallback_only_api_keys() {
|
||||
let api_keys = api_keys_with_warp_credit_fallback_setting(None, true)
|
||||
.expect("fallback setting should create ApiKeys");
|
||||
|
||||
assert!(api_keys.allow_use_of_warp_credits);
|
||||
assert!(api_keys.anthropic.is_empty());
|
||||
assert!(api_keys.openai.is_empty());
|
||||
assert!(api_keys.google.is_empty());
|
||||
assert!(api_keys.open_router.is_empty());
|
||||
assert!(api_keys.aws_credentials.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_keys_with_warp_credit_fallback_setting_preserves_existing_keys() {
|
||||
let api_keys = api_keys_with_warp_credit_fallback_setting(
|
||||
Some(api::request::settings::ApiKeys {
|
||||
anthropic: "anthropic-key".to_string(),
|
||||
openai: String::new(),
|
||||
google: String::new(),
|
||||
open_router: String::new(),
|
||||
grok_oauth_access_token: String::new(),
|
||||
allow_use_of_warp_credits: false,
|
||||
aws_credentials: None,
|
||||
google_cloud_credentials: None,
|
||||
}),
|
||||
true,
|
||||
)
|
||||
.expect("existing ApiKeys should be preserved");
|
||||
|
||||
assert_eq!(api_keys.anthropic, "anthropic-key");
|
||||
assert!(api_keys.allow_use_of_warp_credits);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_orchestration_v2_matches_request_orchestration_setting() {
|
||||
assert!(supports_orchestration_v2(true));
|
||||
assert!(!supports_orchestration_v2(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supported_tools_include_orchestration_tools_when_orchestration_enabled() {
|
||||
let mut params = request_params_with_ask_user_question_enabled(false);
|
||||
params.orchestration_enabled = true;
|
||||
|
||||
let supported_tools = get_supported_tools(¶ms);
|
||||
|
||||
assert!(supported_tools.contains(&api::ToolType::RunAgents));
|
||||
assert!(supported_tools.contains(&api::ToolType::SendMessageToAgent));
|
||||
assert!(!supported_tools.contains(&api::ToolType::StartAgent));
|
||||
assert!(!supported_tools.contains(&api::ToolType::StartAgentV2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supported_tools_omit_orchestration_tools_when_orchestration_disabled() {
|
||||
let params = request_params_with_ask_user_question_enabled(false);
|
||||
let supported_tools = get_supported_tools(¶ms);
|
||||
|
||||
assert!(!supported_tools.contains(&api::ToolType::RunAgents));
|
||||
assert!(!supported_tools.contains(&api::ToolType::SendMessageToAgent));
|
||||
assert!(!supported_tools.contains(&api::ToolType::StartAgent));
|
||||
assert!(!supported_tools.contains(&api::ToolType::StartAgentV2));
|
||||
}
|
||||
#[test]
|
||||
fn supported_tools_omits_ask_user_question_when_disabled() {
|
||||
let params = request_params_with_ask_user_question_enabled(false);
|
||||
@@ -84,3 +171,35 @@ fn supported_tools_omit_upload_artifact_when_feature_flag_is_disabled() {
|
||||
|
||||
assert!(!supported_tools.contains(&api::ToolType::UploadFileArtifact));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_supported_tools_include_search_codebase_when_connected_and_feature_flag_is_enabled() {
|
||||
let _flag = FeatureFlag::RemoteCodebaseIndexing.override_enabled(true);
|
||||
let params = request_params_for_remote(Some(HostId::new("host".to_string())));
|
||||
let supported_tools = get_supported_tools(¶ms);
|
||||
let supported_cli_agent_tools = get_supported_cli_agent_tools(¶ms);
|
||||
|
||||
assert!(supported_tools.contains(&api::ToolType::SearchCodebase));
|
||||
assert!(supported_cli_agent_tools.contains(&api::ToolType::SearchCodebase));
|
||||
}
|
||||
#[test]
|
||||
fn remote_supported_tools_omit_search_codebase_when_feature_flag_is_disabled() {
|
||||
let _flag = FeatureFlag::RemoteCodebaseIndexing.override_enabled(false);
|
||||
let params = request_params_for_remote(Some(HostId::new("host".to_string())));
|
||||
let supported_tools = get_supported_tools(¶ms);
|
||||
let supported_cli_agent_tools = get_supported_cli_agent_tools(¶ms);
|
||||
|
||||
assert!(!supported_tools.contains(&api::ToolType::SearchCodebase));
|
||||
assert!(!supported_cli_agent_tools.contains(&api::ToolType::SearchCodebase));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_supported_tools_omit_search_codebase_when_remote_is_not_connected() {
|
||||
let _flag = FeatureFlag::RemoteCodebaseIndexing.override_enabled(true);
|
||||
let params = request_params_for_remote(None);
|
||||
let supported_tools = get_supported_tools(¶ms);
|
||||
let supported_cli_agent_tools = get_supported_cli_agent_tools(¶ms);
|
||||
|
||||
assert!(!supported_tools.contains(&api::ToolType::SearchCodebase));
|
||||
assert!(!supported_cli_agent_tools.contains(&api::ToolType::SearchCodebase));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user