first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,26 +1,21 @@
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use itertools::Itertools as _;
|
||||
use uuid::Uuid;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use crate::{
|
||||
agent::{
|
||||
action::{
|
||||
AIAgentActionType, AIAgentPtyWriteMode, CommentSide, FileEdit, InsertReviewComment,
|
||||
InsertedCommentLine, InsertedCommentLocation, ReadFilesRequest, ReadSkillRequest,
|
||||
SearchCodebaseRequest, ShellCommandDelay, SuggestPromptRequest, UploadArtifactRequest,
|
||||
UseComputerRequest,
|
||||
},
|
||||
action_result::{AnyFileContent, FileContext},
|
||||
convert::ToolToAIAgentActionError,
|
||||
FileLocations,
|
||||
},
|
||||
diff_validation::{ParsedDiff, V4AHunk},
|
||||
document::AIDocumentId,
|
||||
skills::SkillReference,
|
||||
use crate::agent::action::{
|
||||
AIAgentActionType, AIAgentPtyWriteMode, CommentSide, FileEdit, InsertReviewComment,
|
||||
InsertedCommentLine, InsertedCommentLocation, ReadFilesRequest, SearchCodebaseRequest,
|
||||
ShellCommandDelay, SuggestPromptRequest, UploadArtifactRequest, UseComputerRequest,
|
||||
};
|
||||
use crate::agent::action_result::{AnyFileContent, FileContext};
|
||||
use crate::agent::convert::ToolToAIAgentActionError;
|
||||
use crate::agent::FileLocations;
|
||||
use crate::diff_validation::{ParsedDiff, V4AHunk};
|
||||
use crate::document::AIDocumentId;
|
||||
|
||||
impl From<api::message::tool_call::RunShellCommand> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::RunShellCommand) -> Self {
|
||||
@@ -430,10 +425,11 @@ impl TryFrom<api::message::tool_call::UseComputer> for AIAgentActionType {
|
||||
.actions
|
||||
.into_iter()
|
||||
.map(|action| {
|
||||
let target = convert_computer_use_target(action.target);
|
||||
let Some(action_type) = action.r#type else {
|
||||
return Err(ToolToAIAgentActionError::MissingComputerUseActionType);
|
||||
};
|
||||
match action_type {
|
||||
let action = match action_type {
|
||||
use_computer::action::Type::MouseMove(mouse_move) => {
|
||||
Ok(computer_use::Action::MouseMove {
|
||||
to: coordinates_to_vec(mouse_move.to.as_ref())?,
|
||||
@@ -481,7 +477,9 @@ impl TryFrom<api::message::tool_call::UseComputer> for AIAgentActionType {
|
||||
let key = convert_key(key_up.key)?;
|
||||
Ok(computer_use::Action::KeyUp { key })
|
||||
}
|
||||
}
|
||||
}?;
|
||||
log_window_target_coord(&action, target);
|
||||
Ok(computer_use::TargetedAction { action, target })
|
||||
})
|
||||
.try_collect()?;
|
||||
let screenshot_params = value
|
||||
@@ -505,19 +503,6 @@ impl From<api::message::tool_call::RequestComputerUse> for AIAgentActionType {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<api::message::tool_call::ReadSkill> for AIAgentActionType {
|
||||
type Error = ToolToAIAgentActionError;
|
||||
|
||||
fn try_from(value: api::message::tool_call::ReadSkill) -> Result<Self, Self::Error> {
|
||||
match value.skill_reference {
|
||||
Some(reference) => Ok(AIAgentActionType::ReadSkill(ReadSkillRequest {
|
||||
skill: SkillReference::from(reference),
|
||||
})),
|
||||
None => Err(ToolToAIAgentActionError::MissingSkillReference),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::FetchConversation> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::FetchConversation) -> Self {
|
||||
AIAgentActionType::FetchConversation {
|
||||
@@ -526,18 +511,6 @@ impl From<api::message::tool_call::FetchConversation> for AIAgentActionType {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::read_skill::SkillReference> for SkillReference {
|
||||
fn from(value: api::message::tool_call::read_skill::SkillReference) -> Self {
|
||||
use warp_multi_agent_api::message::tool_call::read_skill::SkillReference as ApiSkillReference;
|
||||
match value {
|
||||
ApiSkillReference::SkillPath(skill_path) => {
|
||||
SkillReference::Path(PathBuf::from(skill_path))
|
||||
}
|
||||
ApiSkillReference::BundledSkillId(id) => SkillReference::BundledSkillId(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts API ScreenshotParams to the internal computer_use type.
|
||||
fn convert_screenshot_params(
|
||||
params: api::message::tool_call::ScreenshotParams,
|
||||
@@ -556,6 +529,51 @@ fn convert_screenshot_params(
|
||||
max_long_edge_px: (params.max_long_edge_px > 0).then_some(params.max_long_edge_px as usize),
|
||||
max_total_px: (params.max_total_px > 0).then_some(params.max_total_px as usize),
|
||||
region,
|
||||
target: convert_computer_use_target(params.target),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an optional API `ComputerUseTarget` into the internal computer_use target. An absent
|
||||
/// or `Screen` target maps to the legacy whole-screen behavior.
|
||||
fn convert_computer_use_target(
|
||||
target: Option<api::message::tool_call::ComputerUseTarget>,
|
||||
) -> computer_use::Target {
|
||||
use api::message::tool_call::computer_use_target::Target as ApiTarget;
|
||||
match target.and_then(|t| t.target) {
|
||||
// The proto window id is an opaque string; on macOS it is a CGWindowID, so parse it back
|
||||
// to a u32. An unparseable id is treated as no valid window target and falls back to the
|
||||
// legacy whole-screen behavior rather than panicking.
|
||||
Some(ApiTarget::Window(window)) => match window.window_id.parse::<u32>() {
|
||||
Ok(window_id) => computer_use::Target::Window {
|
||||
window_id,
|
||||
pid: window.pid,
|
||||
},
|
||||
Err(_) => computer_use::Target::Screen,
|
||||
},
|
||||
Some(ApiTarget::Screen(_)) | None => computer_use::Target::Screen,
|
||||
}
|
||||
}
|
||||
|
||||
/// Logs the raw server-provided coordinates for a window-targeted computer-use action, so the
|
||||
/// agent-driven coordinate conversion can be compared against where the click actually lands.
|
||||
/// Gated on COMPUTER_USE_DEBUG and routed through `log` so it surfaces in the app's log file.
|
||||
fn log_window_target_coord(action: &computer_use::Action, target: computer_use::Target) {
|
||||
if std::env::var_os("COMPUTER_USE_DEBUG").is_none() {
|
||||
return;
|
||||
}
|
||||
let computer_use::Target::Window { window_id, pid } = target else {
|
||||
return;
|
||||
};
|
||||
let coord = match action {
|
||||
computer_use::Action::MouseMove { to } => Some(("mouse_move", to.x(), to.y())),
|
||||
computer_use::Action::MouseDown { at, .. } => Some(("mouse_down", at.x(), at.y())),
|
||||
computer_use::Action::MouseWheel { at, .. } => Some(("mouse_wheel", at.x(), at.y())),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((kind, x, y)) = coord {
|
||||
log::info!(
|
||||
"[computer_use] server->client {kind} window#={window_id} pid={pid} raw_coord=({x},{y})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
mod convert;
|
||||
|
||||
use std::{fmt::Display, ops::Range, path::PathBuf, time::Duration};
|
||||
use std::fmt::Display;
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use galaxy_terminal::model::BlockId;
|
||||
use itertools::Itertools as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumDiscriminants;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
agent::{
|
||||
action_result::{
|
||||
AIAgentActionResultType, AskUserQuestionResult, CallMCPToolResult,
|
||||
CreateDocumentsResult, EditDocumentsResult, FetchConversationResult, FileGlobResult,
|
||||
FileGlobV2Result, GrepResult, InsertReviewCommentsResult, ReadDocumentsResult,
|
||||
ReadFilesResult, ReadMCPResourceResult, ReadShellCommandOutputResult, ReadSkillResult,
|
||||
RequestCommandOutputResult, RequestComputerUseResult, RequestFileEditsResult,
|
||||
SearchCodebaseResult, SendMessageToAgentResult, StartAgentResult, StartAgentVersion,
|
||||
SuggestNewConversationResult, SuggestPromptResult,
|
||||
TransferShellCommandControlToUserResult, UploadArtifactResult, UseComputerResult,
|
||||
WriteToLongRunningShellCommandResult,
|
||||
},
|
||||
AIAgentCitation, FileLocations,
|
||||
},
|
||||
diff_validation::ParsedDiff,
|
||||
document::AIDocumentId,
|
||||
skills::SkillReference,
|
||||
};
|
||||
pub use warp_multi_agent_api::LifecycleEventType;
|
||||
|
||||
use crate::agent::action_result::{
|
||||
AIAgentActionResultType, AskUserQuestionResult, CallMCPToolResult, CreateDocumentsResult,
|
||||
EditDocumentsResult, FetchConversationResult, FileGlobResult, FileGlobV2Result, GrepResult,
|
||||
InsertReviewCommentsResult, ReadDocumentsResult, ReadFilesResult, ReadMCPResourceResult,
|
||||
ReadShellCommandOutputResult, ReadSkillResult, RequestCommandOutputResult,
|
||||
RequestComputerUseResult, RequestFileEditsResult, RunAgentsResult, SearchCodebaseResult,
|
||||
SendMessageToAgentResult, StartAgentResult, StartAgentVersion, SuggestNewConversationResult,
|
||||
SuggestPromptResult, TransferShellCommandControlToUserResult, UploadArtifactResult,
|
||||
UseComputerResult, WaitForEventsResult, WriteToLongRunningShellCommandResult,
|
||||
};
|
||||
use crate::agent::{AIAgentCitation, FileLocations};
|
||||
use crate::diff_validation::ParsedDiff;
|
||||
use crate::document::AIDocumentId;
|
||||
use crate::skills::SkillReference;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, EnumDiscriminants)]
|
||||
pub enum AIAgentActionType {
|
||||
/// The AI requested the output for a given command to be retrieved as context in responding to
|
||||
@@ -48,7 +46,7 @@ pub enum AIAgentActionType {
|
||||
/// result instead.
|
||||
wait_until_completion: bool,
|
||||
|
||||
/// [`Some(true)`] iff the LLM thinks that the `command` might invoke pager.
|
||||
/// [`Some(true)`] iff the LLM thinks that the `command` might invoke a pager.
|
||||
uses_pager: Option<bool>,
|
||||
|
||||
/// The AI's rationale for requesting a command.
|
||||
@@ -144,7 +142,9 @@ pub enum AIAgentActionType {
|
||||
FetchConversation {
|
||||
conversation_id: String,
|
||||
},
|
||||
|
||||
// TODO(QUALITY-788): Delete legacy start_agent/start_agent_v2 action support once
|
||||
// old preview orchestration history no longer needs parse/display/result compatibility.
|
||||
// Linear issue: QUALITY-788.
|
||||
StartAgent {
|
||||
version: StartAgentVersion,
|
||||
name: String,
|
||||
@@ -167,6 +167,68 @@ pub enum AIAgentActionType {
|
||||
AskUserQuestion {
|
||||
questions: Vec<AskUserQuestionItem>,
|
||||
},
|
||||
|
||||
/// AI requested batched orchestration of one-or-more child agents that
|
||||
/// share run-wide configuration (model, harness, execution mode).
|
||||
/// The full per-child prompt is computed at dispatch time as
|
||||
/// `base_prompt + "\n\n" + agent_run_configs[i].prompt` (or just
|
||||
/// `base_prompt` when the per-agent `prompt` is empty).
|
||||
RunAgents(RunAgentsRequest),
|
||||
|
||||
/// Synthesized from a server-emitted Message::ToolCall::WaitForEvents;
|
||||
/// dispatched by WaitForEventsExecutor.
|
||||
WaitForEvents {
|
||||
/// tool_call_id of the unresolved WaitForEvents call; used to
|
||||
/// match inbound resume signals.
|
||||
tool_call_id: String,
|
||||
/// 0 means "unset" (prost flat-scalar convention); the executor
|
||||
/// falls back to a default.
|
||||
idle_timeout_seconds: i32,
|
||||
},
|
||||
}
|
||||
|
||||
/// Run-wide + per-agent configuration for a `RunAgents` tool call.
|
||||
///
|
||||
/// Mirrors the proto `RunAgents` message. Server-resolved fields
|
||||
/// (`model_id`, `harness_type`, `execution_mode`'s remote details) are
|
||||
/// folded in by the server's final tool-call re-emission once the
|
||||
/// payload is complete; the client renders the full layout from a
|
||||
/// fully-resolved instance only.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct RunAgentsRequest {
|
||||
pub summary: String,
|
||||
pub base_prompt: String,
|
||||
pub skills: Vec<SkillReference>,
|
||||
pub model_id: String,
|
||||
pub harness_type: String,
|
||||
pub execution_mode: RunAgentsExecutionMode,
|
||||
pub agent_run_configs: Vec<RunAgentsAgentRunConfig>,
|
||||
pub plan_id: String,
|
||||
/// Resolved client-side at dispatch time; not serialized to the wire.
|
||||
pub harness_auth_secret_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum RunAgentsExecutionMode {
|
||||
Local,
|
||||
Remote {
|
||||
environment_id: String,
|
||||
worker_host: String,
|
||||
computer_use_enabled: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl RunAgentsExecutionMode {
|
||||
pub fn is_remote(&self) -> bool {
|
||||
matches!(self, Self::Remote { .. })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct RunAgentsAgentRunConfig {
|
||||
pub name: String,
|
||||
pub prompt: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
@@ -175,6 +237,11 @@ pub enum StartAgentExecutionMode {
|
||||
/// `None` selects the legacy embedded local child-agent flow.
|
||||
/// `Some(...)` selects a third-party CLI harness to launch locally.
|
||||
harness_type: Option<String>,
|
||||
/// `None` inherits the parent agent's preferred LLM (legacy behavior).
|
||||
/// `Some(_)` overrides the child's preferred LLM with the supplied
|
||||
/// model id (used by the orchestrate confirmation card so the user's
|
||||
/// model selection is honored on local launches).
|
||||
model_id: Option<String>,
|
||||
},
|
||||
Remote {
|
||||
environment_id: String,
|
||||
@@ -184,18 +251,27 @@ pub enum StartAgentExecutionMode {
|
||||
worker_host: String,
|
||||
harness_type: String,
|
||||
title: String,
|
||||
/// Name of a managed secret to forward as the authentication
|
||||
/// credential for the remote child when running a non-Oz harness.
|
||||
/// `None` means no client-side secret was selected — the remote
|
||||
/// environment falls back to its own ambient credentials.
|
||||
auth_secret_name: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl StartAgentExecutionMode {
|
||||
/// Constructs a local execution mode using the legacy v1 default harness.
|
||||
pub fn local_with_defaults() -> Self {
|
||||
Self::Local { harness_type: None }
|
||||
Self::Local {
|
||||
harness_type: None,
|
||||
model_id: None,
|
||||
}
|
||||
}
|
||||
/// Constructs a local execution mode for a specific third-party harness.
|
||||
pub fn local_harness(harness_type: String) -> Self {
|
||||
Self::Local {
|
||||
harness_type: Some(harness_type),
|
||||
model_id: None,
|
||||
}
|
||||
}
|
||||
/// Constructs a remote execution mode using the legacy v1 defaults for
|
||||
@@ -209,6 +285,7 @@ impl StartAgentExecutionMode {
|
||||
worker_host: String::new(),
|
||||
harness_type: String::new(),
|
||||
title: String::new(),
|
||||
auth_secret_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -317,6 +394,10 @@ impl AIAgentActionType {
|
||||
Self::AskUserQuestion { .. } => {
|
||||
AIAgentActionResultType::AskUserQuestion(AskUserQuestionResult::Cancelled)
|
||||
}
|
||||
Self::RunAgents(_) => AIAgentActionResultType::RunAgents(RunAgentsResult::Cancelled),
|
||||
Self::WaitForEvents { .. } => {
|
||||
AIAgentActionResultType::WaitForEvents(WaitForEventsResult::Cancelled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,6 +443,10 @@ impl AIAgentActionType {
|
||||
Self::AskUserQuestion { questions } => {
|
||||
format!("Ask user {} question(s)", questions.len())
|
||||
}
|
||||
Self::RunAgents(req) => {
|
||||
format!("Orchestrate {} agent(s)", req.agent_run_configs.len())
|
||||
}
|
||||
Self::WaitForEvents { .. } => "Wait for events".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,6 +619,24 @@ impl Display for AIAgentActionType {
|
||||
AIAgentActionType::AskUserQuestion { questions } => {
|
||||
write!(f, "AskUserQuestion: {} question(s)", questions.len())
|
||||
}
|
||||
AIAgentActionType::RunAgents(req) => {
|
||||
let names = req
|
||||
.agent_run_configs
|
||||
.iter()
|
||||
.map(|c| c.name.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
write!(f, "Orchestrate: summary='{}' agents=[{names}]", req.summary,)
|
||||
}
|
||||
AIAgentActionType::WaitForEvents {
|
||||
tool_call_id,
|
||||
idle_timeout_seconds,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"WaitForEvents: tool_call_id={tool_call_id} idle_timeout_seconds={idle_timeout_seconds}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -665,7 +768,8 @@ pub struct CreateDocumentsRequest {
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct UseComputerRequest {
|
||||
pub action_summary: String,
|
||||
pub actions: Vec<computer_use::Action>,
|
||||
/// Each action carries the surface (screen or a specific window) it targets.
|
||||
pub actions: Vec<computer_use::TargetedAction>,
|
||||
/// If set, a screenshot will be captured after the actions are executed.
|
||||
pub screenshot_params: Option<computer_use::ScreenshotParams>,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
use warp_multi_agent_api::{
|
||||
self as api,
|
||||
apply_file_diffs_result::success::UpdatedFileContent,
|
||||
ask_user_question_result::answer_item::{self, Answer as AskUserQuestionAnswer},
|
||||
use chrono::{DateTime, Local};
|
||||
use warp_multi_agent_api::apply_file_diffs_result::success::UpdatedFileContent;
|
||||
use warp_multi_agent_api::ask_user_question_result::answer_item::{
|
||||
self, Answer as AskUserQuestionAnswer,
|
||||
};
|
||||
|
||||
use crate::agent::{action_result::ShellCommandError, convert::ConvertToAPITypeError};
|
||||
use warp_multi_agent_api::{self as api};
|
||||
|
||||
use super::*;
|
||||
use crate::agent::action_result::ShellCommandError;
|
||||
use crate::agent::convert::ConvertToAPITypeError;
|
||||
|
||||
fn local_datetime_to_timestamp(timestamp: DateTime<Local>) -> prost_types::Timestamp {
|
||||
prost_types::Timestamp {
|
||||
seconds: timestamp.timestamp(),
|
||||
nanos: timestamp.timestamp_subsec_nanos() as i32,
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RequestCommandOutputResult> for api::request::input::tool_call_result::Result {
|
||||
type Error = ConvertToAPITypeError;
|
||||
@@ -18,7 +26,8 @@ impl TryFrom<RequestCommandOutputResult> for api::request::input::tool_call_resu
|
||||
block_id,
|
||||
output,
|
||||
exit_code,
|
||||
..
|
||||
start_ts,
|
||||
completed_ts,
|
||||
} => Ok(
|
||||
api::request::input::tool_call_result::Result::RunShellCommand(
|
||||
#[allow(deprecated)]
|
||||
@@ -31,6 +40,8 @@ impl TryFrom<RequestCommandOutputResult> for api::request::input::tool_call_resu
|
||||
command_id: block_id.to_string(),
|
||||
output,
|
||||
exit_code: exit_code.value(),
|
||||
start_ts: start_ts.map(local_datetime_to_timestamp),
|
||||
finish_ts: completed_ts.map(local_datetime_to_timestamp),
|
||||
},
|
||||
)),
|
||||
},
|
||||
@@ -127,7 +138,7 @@ impl TryFrom<WriteToLongRunningShellCommandResult>
|
||||
},
|
||||
),
|
||||
),
|
||||
WriteToLongRunningShellCommandResult::CommandFinished { block_id, output, exit_code, .. } => Ok(
|
||||
WriteToLongRunningShellCommandResult::CommandFinished { block_id, output, exit_code, start_ts, completed_ts } => Ok(
|
||||
api::request::input::tool_call_result::Result::WriteToLongRunningShellCommand(
|
||||
api::WriteToLongRunningShellCommandResult {
|
||||
result: Some(api::write_to_long_running_shell_command_result::Result::CommandFinished(
|
||||
@@ -135,6 +146,8 @@ impl TryFrom<WriteToLongRunningShellCommandResult>
|
||||
command_id: block_id.to_string(),
|
||||
output,
|
||||
exit_code: exit_code.value(),
|
||||
start_ts: start_ts.map(local_datetime_to_timestamp),
|
||||
finish_ts: completed_ts.map(local_datetime_to_timestamp),
|
||||
}
|
||||
))
|
||||
},
|
||||
@@ -797,7 +810,8 @@ impl TryFrom<ReadShellCommandOutputResult> for api::request::input::tool_call_re
|
||||
block_id,
|
||||
output,
|
||||
exit_code,
|
||||
..
|
||||
start_ts,
|
||||
completed_ts,
|
||||
} => Ok(
|
||||
api::request::input::tool_call_result::Result::ReadShellCommandOutput(
|
||||
api::ReadShellCommandOutputResult {
|
||||
@@ -807,6 +821,8 @@ impl TryFrom<ReadShellCommandOutputResult> for api::request::input::tool_call_re
|
||||
command_id: block_id.to_string(),
|
||||
output,
|
||||
exit_code: exit_code.value(),
|
||||
start_ts: start_ts.map(local_datetime_to_timestamp),
|
||||
finish_ts: completed_ts.map(local_datetime_to_timestamp),
|
||||
},
|
||||
)),
|
||||
},
|
||||
@@ -891,6 +907,8 @@ impl TryFrom<TransferShellCommandControlToUserResult>
|
||||
block_id,
|
||||
output,
|
||||
exit_code,
|
||||
start_ts,
|
||||
completed_ts,
|
||||
} => Ok(
|
||||
api::request::input::tool_call_result::Result::TransferShellCommandControlToUser(
|
||||
api::TransferShellCommandControlToUserResult {
|
||||
@@ -900,6 +918,8 @@ impl TryFrom<TransferShellCommandControlToUserResult>
|
||||
command_id: block_id.to_string(),
|
||||
output,
|
||||
exit_code: exit_code.value(),
|
||||
start_ts: start_ts.map(local_datetime_to_timestamp),
|
||||
finish_ts: completed_ts.map(local_datetime_to_timestamp),
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -1109,6 +1129,7 @@ impl TryFrom<RequestComputerUseResult> for api::request::input::tool_call_result
|
||||
RequestComputerUseResult::Approved {
|
||||
screenshot,
|
||||
platform,
|
||||
windows,
|
||||
} => Ok(
|
||||
api::request::input::tool_call_result::Result::RequestComputerUse(
|
||||
api::RequestComputerUseResult {
|
||||
@@ -1125,6 +1146,7 @@ impl TryFrom<RequestComputerUseResult> for api::request::input::tool_call_result
|
||||
height: screenshot.height as i32,
|
||||
}),
|
||||
platform: convert_platform(platform).into(),
|
||||
windows: windows.into_iter().map(convert_window_info).collect(),
|
||||
},
|
||||
)),
|
||||
},
|
||||
@@ -1158,6 +1180,9 @@ impl TryFrom<UseComputerResult> for api::request::input::tool_call_result::Resul
|
||||
fn try_from(result: UseComputerResult) -> Result<Self, Self::Error> {
|
||||
match result {
|
||||
UseComputerResult::Success(result) => {
|
||||
// Copy out the captured-window metadata (if any) before the owned fields of
|
||||
// `result` are moved into the message below.
|
||||
let captured = result.captured_window;
|
||||
Ok(api::request::input::tool_call_result::Result::UseComputer(
|
||||
api::UseComputerResult {
|
||||
result: Some(api::use_computer_result::Result::Success(
|
||||
@@ -1169,6 +1194,20 @@ impl TryFrom<UseComputerResult> for api::request::input::tool_call_result::Resul
|
||||
height: s.height as i32,
|
||||
}),
|
||||
cursor_position: result.cursor_position.map(vec_to_coordinates),
|
||||
windows: result
|
||||
.windows
|
||||
.into_iter()
|
||||
.map(convert_window_info)
|
||||
.collect(),
|
||||
// The window id is an opaque string on the wire; on macOS it is a
|
||||
// CGWindowID, so format the u32 back to a string at the boundary.
|
||||
captured_window: captured.map(|c| {
|
||||
api::use_computer_result::success::CapturedWindow {
|
||||
window_id: c.window_id.to_string(),
|
||||
width_px: c.width_px,
|
||||
height_px: c.height_px,
|
||||
}
|
||||
}),
|
||||
},
|
||||
)),
|
||||
},
|
||||
@@ -1205,6 +1244,18 @@ fn vec_to_coordinates(vec: computer_use::Vector2I) -> api::Coordinates {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a computer_use window record into the API `WindowInfo` message.
|
||||
fn convert_window_info(window: computer_use::WindowInfo) -> api::WindowInfo {
|
||||
api::WindowInfo {
|
||||
// The window id travels as an opaque string; on macOS it is a CGWindowID (u32).
|
||||
window_id: window.window_id.to_string(),
|
||||
pid: window.pid,
|
||||
app_name: window.app_name,
|
||||
title: window.title,
|
||||
layer: window.layer,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_platform(
|
||||
platform: computer_use::Platform,
|
||||
) -> api::request_computer_use_result::approved::Platform {
|
||||
@@ -1463,6 +1514,121 @@ impl From<AskUserQuestionResult> for api::request::input::tool_call_result::Resu
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RunAgentsLaunchedExecutionMode>
|
||||
for api::run_agents_result::launched::ResolvedExecutionMode
|
||||
{
|
||||
fn from(mode: RunAgentsLaunchedExecutionMode) -> Self {
|
||||
match mode {
|
||||
RunAgentsLaunchedExecutionMode::Local => {
|
||||
api::run_agents_result::launched::ResolvedExecutionMode::Local(
|
||||
api::run_agents::Local {},
|
||||
)
|
||||
}
|
||||
RunAgentsLaunchedExecutionMode::Remote {
|
||||
environment_id,
|
||||
worker_host,
|
||||
computer_use_enabled,
|
||||
} => api::run_agents_result::launched::ResolvedExecutionMode::Remote(
|
||||
api::run_agents::Remote {
|
||||
environment_id,
|
||||
worker_host,
|
||||
computer_use_enabled,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RunAgentsAgentOutcome> for api::run_agents_result::AgentOutcome {
|
||||
fn from(outcome: RunAgentsAgentOutcome) -> Self {
|
||||
let result = match outcome.kind {
|
||||
RunAgentsAgentOutcomeKind::Launched { agent_id } => {
|
||||
api::run_agents_result::agent_outcome::Result::Launched(
|
||||
api::run_agents_result::LaunchedAgent { agent_id },
|
||||
)
|
||||
}
|
||||
RunAgentsAgentOutcomeKind::Failed { error } => {
|
||||
api::run_agents_result::agent_outcome::Result::Failed(
|
||||
api::run_agents_result::FailedAgent { error },
|
||||
)
|
||||
}
|
||||
};
|
||||
api::run_agents_result::AgentOutcome {
|
||||
name: outcome.name,
|
||||
result: Some(result),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a client-side harness string identifier (e.g. "oz", "claude")
|
||||
/// to the new proto `Harness` oneof. Returns `None` for empty,
|
||||
/// unrecognized, or `"unknown"` strings; callers leave
|
||||
/// `resolved_harness` unset in that case.
|
||||
pub(super) fn build_api_harness(harness_type: &str) -> Option<api::Harness> {
|
||||
let normalized = harness_type.trim().to_ascii_lowercase().replace('_', "-");
|
||||
let variant = match normalized.as_str() {
|
||||
"oz" => api::harness::Variant::Oz(api::harness::Oz {}),
|
||||
"claude" | "claude-code" => api::harness::Variant::ClaudeCode(api::harness::ClaudeCode {}),
|
||||
"opencode" | "open-code" => api::harness::Variant::OpenCode(api::harness::OpenCode {}),
|
||||
"gemini" => api::harness::Variant::Gemini(api::harness::Gemini {}),
|
||||
"codex" => api::harness::Variant::Codex(api::harness::Codex {}),
|
||||
_ => return None,
|
||||
};
|
||||
Some(api::Harness {
|
||||
variant: Some(variant),
|
||||
})
|
||||
}
|
||||
|
||||
impl TryFrom<RunAgentsResult> for api::request::input::tool_call_result::Result {
|
||||
type Error = ConvertToAPITypeError;
|
||||
|
||||
fn try_from(result: RunAgentsResult) -> Result<Self, Self::Error> {
|
||||
match result {
|
||||
RunAgentsResult::Launched {
|
||||
model_id,
|
||||
harness_type,
|
||||
execution_mode,
|
||||
agents,
|
||||
} => Ok(
|
||||
api::request::input::tool_call_result::Result::RunAgentsResult(
|
||||
api::RunAgentsResult {
|
||||
outcome: Some(api::run_agents_result::Outcome::Launched(
|
||||
api::run_agents_result::Launched {
|
||||
resolved_model_id: model_id,
|
||||
resolved_harness: build_api_harness(&harness_type),
|
||||
resolved_execution_mode: Some(execution_mode.into()),
|
||||
agents: agents.into_iter().map(Into::into).collect(),
|
||||
},
|
||||
)),
|
||||
},
|
||||
),
|
||||
),
|
||||
RunAgentsResult::Denied { reason } => Ok(
|
||||
api::request::input::tool_call_result::Result::RunAgentsResult(
|
||||
api::RunAgentsResult {
|
||||
outcome: Some(api::run_agents_result::Outcome::Denied(
|
||||
api::run_agents_result::Denied { reason },
|
||||
)),
|
||||
},
|
||||
),
|
||||
),
|
||||
RunAgentsResult::Failure { error } => Ok(
|
||||
api::request::input::tool_call_result::Result::RunAgentsResult(
|
||||
api::RunAgentsResult {
|
||||
outcome: Some(api::run_agents_result::Outcome::Failure(
|
||||
api::run_agents_result::Failure { error },
|
||||
)),
|
||||
},
|
||||
),
|
||||
),
|
||||
// Reject is conveyed by the generic ToolCallResult.Cancel marker
|
||||
// synthesized server-side on the next user input; nothing for the
|
||||
// client to send on the wire here.
|
||||
RunAgentsResult::Cancelled => Err(ConvertToAPITypeError::Ignore),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<InsertReviewCommentsResult> for api::request::input::tool_call_result::Result {
|
||||
type Error = ConvertToAPITypeError;
|
||||
|
||||
@@ -1504,6 +1670,22 @@ impl TryFrom<InsertReviewCommentsResult> for api::request::input::tool_call_resu
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<WaitForEventsResult> for api::request::input::tool_call_result::Result {
|
||||
type Error = ConvertToAPITypeError;
|
||||
|
||||
/// Completed → wire form; Cancelled → drop (mirrors RunAgents).
|
||||
fn try_from(result: WaitForEventsResult) -> Result<Self, Self::Error> {
|
||||
match result {
|
||||
WaitForEventsResult::Completed => Ok(
|
||||
api::request::input::tool_call_result::Result::WaitForEvents(
|
||||
api::WaitForEventsResult {},
|
||||
),
|
||||
),
|
||||
WaitForEventsResult::Cancelled => Err(ConvertToAPITypeError::Ignore),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "convert_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
mod convert;
|
||||
|
||||
use std::{fmt::Display, ops::Range, time::SystemTime};
|
||||
use std::fmt::Display;
|
||||
use std::ops::Range;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use galaxy_core::command::ExitCode;
|
||||
use galaxy_terminal::model::BlockId;
|
||||
use chrono::{DateTime, Local};
|
||||
use itertools::Itertools as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use warp_multi_agent_api::apply_file_diffs_result::success::UpdatedFileContent;
|
||||
|
||||
use crate::{
|
||||
agent::FileLocations,
|
||||
document::{AIDocumentId, AIDocumentVersion},
|
||||
};
|
||||
use crate::agent::FileLocations;
|
||||
use crate::document::{AIDocumentId, AIDocumentVersion};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum AIAgentActionResultType {
|
||||
@@ -95,6 +94,14 @@ pub enum AIAgentActionResultType {
|
||||
TransferShellCommandControlToUser(TransferShellCommandControlToUserResult),
|
||||
/// The result of asking the user a question.
|
||||
AskUserQuestion(AskUserQuestionResult),
|
||||
|
||||
/// The result of an orchestrate tool call: launched (with per-agent
|
||||
/// outcomes), launch denied (Stage 2), failure, or cancelled.
|
||||
RunAgents(RunAgentsResult),
|
||||
|
||||
/// Result of the client-side wait_for_events watchdog or inbound
|
||||
/// resume.
|
||||
WaitForEvents(WaitForEventsResult),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
@@ -161,6 +168,8 @@ impl Display for AIAgentActionResultType {
|
||||
AIAgentActionResultType::SendMessageToAgent(result) => result.fmt(f),
|
||||
AIAgentActionResultType::TransferShellCommandControlToUser(result) => result.fmt(f),
|
||||
AIAgentActionResultType::AskUserQuestion(result) => result.fmt(f),
|
||||
AIAgentActionResultType::RunAgents(result) => result.fmt(f),
|
||||
AIAgentActionResultType::WaitForEvents(result) => result.fmt(f),
|
||||
AIAgentActionResultType::OpenCodeReview | AIAgentActionResultType::InitProject => {
|
||||
Ok(())
|
||||
}
|
||||
@@ -175,6 +184,8 @@ pub enum RequestCommandOutputResult {
|
||||
command: String,
|
||||
output: String,
|
||||
exit_code: ExitCode,
|
||||
start_ts: Option<DateTime<Local>>,
|
||||
completed_ts: Option<DateTime<Local>>,
|
||||
},
|
||||
LongRunningCommandSnapshot {
|
||||
block_id: BlockId,
|
||||
@@ -266,6 +277,8 @@ pub enum WriteToLongRunningShellCommandResult {
|
||||
block_id: BlockId,
|
||||
output: String,
|
||||
exit_code: ExitCode,
|
||||
start_ts: Option<DateTime<Local>>,
|
||||
completed_ts: Option<DateTime<Local>>,
|
||||
},
|
||||
Cancelled,
|
||||
Error(ShellCommandError),
|
||||
@@ -555,6 +568,8 @@ pub enum ReadShellCommandOutputResult {
|
||||
block_id: BlockId,
|
||||
output: String,
|
||||
exit_code: ExitCode,
|
||||
start_ts: Option<DateTime<Local>>,
|
||||
completed_ts: Option<DateTime<Local>>,
|
||||
},
|
||||
LongRunningCommandSnapshot {
|
||||
command: String,
|
||||
@@ -763,6 +778,12 @@ impl AIAgentActionResultType {
|
||||
AIAgentActionResultType::AskUserQuestion(_) => {
|
||||
"The user's answers to clarifying questions"
|
||||
}
|
||||
AIAgentActionResultType::RunAgents(_) => {
|
||||
"The result of an orchestrate batch of child agents"
|
||||
}
|
||||
AIAgentActionResultType::WaitForEvents(_) => {
|
||||
"The local watchdog timed out while waiting for inbound events"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,6 +821,8 @@ impl AIAgentActionResultType {
|
||||
| TransferShellCommandControlToUserResult::CommandFinished { .. },
|
||||
) => true,
|
||||
Self::AskUserQuestion(AskUserQuestionResult::Success { .. }) => true,
|
||||
Self::RunAgents(RunAgentsResult::Launched { .. }) => true,
|
||||
Self::WaitForEvents(WaitForEventsResult::Completed) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -828,7 +851,10 @@ impl AIAgentActionResultType {
|
||||
| Self::AskUserQuestion(AskUserQuestionResult::Error(_))
|
||||
| Self::TransferShellCommandControlToUser(
|
||||
TransferShellCommandControlToUserResult::Error(_),
|
||||
) => true,
|
||||
)
|
||||
| Self::RunAgents(RunAgentsResult::Failure { .. } | RunAgentsResult::Denied { .. }) => {
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -952,7 +978,9 @@ impl AIAgentActionResultType {
|
||||
| Self::StartAgent(StartAgentResult::Cancelled { .. })
|
||||
| Self::SendMessageToAgent(SendMessageToAgentResult::Cancelled)
|
||||
// SkippedByAutoApprove is intentionally excluded: the agent should continue.
|
||||
| Self::AskUserQuestion(AskUserQuestionResult::Cancelled) => true,
|
||||
| Self::AskUserQuestion(AskUserQuestionResult::Cancelled)
|
||||
| Self::RunAgents(RunAgentsResult::Cancelled)
|
||||
| Self::WaitForEvents(WaitForEventsResult::Cancelled) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -1234,6 +1262,8 @@ pub enum RequestComputerUseResult {
|
||||
Approved {
|
||||
screenshot: computer_use::Screenshot,
|
||||
platform: computer_use::Platform,
|
||||
/// The on-screen windows the agent may target.
|
||||
windows: Vec<computer_use::WindowInfo>,
|
||||
},
|
||||
/// Request errored.
|
||||
Error(String),
|
||||
@@ -1280,6 +1310,9 @@ impl Display for FetchConversationResult {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(QUALITY-788): Delete legacy start_agent/start_agent_v2 result support once
|
||||
// old preview orchestration history no longer needs parse/display/result compatibility.
|
||||
// Linear issue: QUALITY-788.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum StartAgentResult {
|
||||
Success {
|
||||
@@ -1321,6 +1354,83 @@ impl Display for StartAgentResult {
|
||||
}
|
||||
}
|
||||
|
||||
/// The terminal outcome of an orchestrate tool call.
|
||||
///
|
||||
/// Mirrors the proto `RunAgentsResult` oneof, with an additional
|
||||
/// `Cancelled` variant used internally by the action machinery when the
|
||||
/// user clicks Reject. The proto wire form for cancellation is the
|
||||
/// generic `ToolCallResult.Cancel` marker; the conversion code emits
|
||||
/// `ConvertToAPITypeError::Ignore` for `Cancelled` so the input
|
||||
/// interceptor can synthesize the marker on the next outbound input.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum RunAgentsResult {
|
||||
/// Orchestration launched. Carries the resolved configuration and one
|
||||
/// `AgentOutcome` per `agent_run_configs[]` entry, in input order.
|
||||
Launched {
|
||||
model_id: String,
|
||||
harness_type: String,
|
||||
execution_mode: RunAgentsLaunchedExecutionMode,
|
||||
agents: Vec<RunAgentsAgentOutcome>,
|
||||
},
|
||||
/// Declined for a non-error reason (currently disapproval).
|
||||
Denied { reason: String },
|
||||
/// Actual error path: server-side validation rejected the call, or the
|
||||
/// client could not begin the launch sequence at all.
|
||||
Failure { error: String },
|
||||
/// User rejected via the Reject button. Wire form is the generic
|
||||
/// `ToolCallResult.Cancel` marker, synthesized by the server's input
|
||||
/// interceptor on the next user input.
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum RunAgentsLaunchedExecutionMode {
|
||||
Local,
|
||||
Remote {
|
||||
environment_id: String,
|
||||
worker_host: String,
|
||||
computer_use_enabled: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Per-agent outcome reported in `RunAgentsResult::Launched.agents`.
|
||||
/// Order mirrors the input order of `RunAgents.agent_run_configs[]`,
|
||||
/// regardless of which `CreateAgentTask` call returned first.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RunAgentsAgentOutcome {
|
||||
pub name: String,
|
||||
pub kind: RunAgentsAgentOutcomeKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum RunAgentsAgentOutcomeKind {
|
||||
Launched { agent_id: String },
|
||||
Failed { error: String },
|
||||
}
|
||||
|
||||
impl Display for RunAgentsResult {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RunAgentsResult::Launched { agents, .. } => {
|
||||
let launched = agents
|
||||
.iter()
|
||||
.filter(|a| matches!(a.kind, RunAgentsAgentOutcomeKind::Launched { .. }))
|
||||
.count();
|
||||
write!(
|
||||
f,
|
||||
"Orchestrate launched ({launched}/{} agents started)",
|
||||
agents.len()
|
||||
)
|
||||
}
|
||||
RunAgentsResult::Denied { reason } => {
|
||||
write!(f, "Orchestrate launch denied: {reason}")
|
||||
}
|
||||
RunAgentsResult::Failure { error } => write!(f, "Orchestrate failure: {error}"),
|
||||
RunAgentsResult::Cancelled => write!(f, "Orchestrate cancelled"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum SendMessageToAgentResult {
|
||||
Success { message_id: String },
|
||||
@@ -1353,6 +1463,8 @@ pub enum TransferShellCommandControlToUserResult {
|
||||
block_id: BlockId,
|
||||
output: String,
|
||||
exit_code: ExitCode,
|
||||
start_ts: Option<DateTime<Local>>,
|
||||
completed_ts: Option<DateTime<Local>>,
|
||||
},
|
||||
Cancelled,
|
||||
Error(ShellCommandError),
|
||||
@@ -1447,3 +1559,24 @@ impl Display for AskUserQuestionResult {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of a client-side wait_for_events action.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum WaitForEventsResult {
|
||||
/// Watchdog fired or an inbound resume signal closed the wait. The
|
||||
/// agent's next turn observes an empty WaitForEvents result on the
|
||||
/// wire and decides how to proceed.
|
||||
Completed,
|
||||
/// User cancelled the conversation while waiting. Mirrors
|
||||
/// RunAgents::Cancelled: no tool-call result is sent on the wire.
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl Display for WaitForEventsResult {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Completed => write!(f, "Wait for events completed"),
|
||||
Self::Cancelled => write!(f, "Wait for events cancelled"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,22 @@ use warp_multi_agent_api as api;
|
||||
/// A citation listed in an AI response.
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||
pub enum AIAgentCitation {
|
||||
WarpDriveObject { uid: String },
|
||||
WarpDocumentation { path: String },
|
||||
WebPage { url: String },
|
||||
WarpDriveObject {
|
||||
uid: String,
|
||||
},
|
||||
WarpDocumentation {
|
||||
path: String,
|
||||
},
|
||||
WebPage {
|
||||
url: String,
|
||||
},
|
||||
/// A memory from an attached memory store. `content` is the raw memory
|
||||
/// text shown as a preview in the chip.
|
||||
AgentMemory {
|
||||
memory_store_id: String,
|
||||
memory_id: String,
|
||||
content: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for AIAgentCitation {
|
||||
@@ -22,6 +35,13 @@ impl Display for AIAgentCitation {
|
||||
AIAgentCitation::WebPage { url } => {
|
||||
write!(f, "Web Page: {url}")
|
||||
}
|
||||
AIAgentCitation::AgentMemory {
|
||||
memory_store_id,
|
||||
memory_id,
|
||||
..
|
||||
} => {
|
||||
write!(f, "Agent Memory: {memory_store_id}/{memory_id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ use galaxy_terminal::shell::ShellLaunchData;
|
||||
use itertools::Itertools as _;
|
||||
|
||||
use crate::agent::action_result::FileContext;
|
||||
use crate::{index::locations::CodeContextLocation, paths::shell_native_absolute_path};
|
||||
use crate::index::locations::CodeContextLocation;
|
||||
use crate::paths::shell_native_absolute_path;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct FileLocations {
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod action_result;
|
||||
mod citation;
|
||||
pub mod convert;
|
||||
pub mod file_locations;
|
||||
pub mod orchestration_config;
|
||||
|
||||
pub use citation::{AIAgentCitation, UnknownCitationTypeError};
|
||||
pub use file_locations::{group_file_contexts_for_display, FileLocations};
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use super::action::RunAgentsRequest;
|
||||
|
||||
/// Client-side representation of the orchestration config attached to a
|
||||
/// conversation via `OrchestrationConfigSnapshot`.
|
||||
///
|
||||
/// Mirrors the proto `OrchestrationConfig` but uses Rust-native types
|
||||
/// to keep view / model code free of proto imports.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct OrchestrationConfig {
|
||||
pub model_id: String,
|
||||
pub harness_type: String,
|
||||
pub execution_mode: OrchestrationExecutionMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum OrchestrationExecutionMode {
|
||||
Local,
|
||||
Remote {
|
||||
environment_id: String,
|
||||
worker_host: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl OrchestrationExecutionMode {
|
||||
pub fn is_remote(&self) -> bool {
|
||||
matches!(self, Self::Remote { .. })
|
||||
}
|
||||
}
|
||||
|
||||
/// User's approval state for orchestration on the active config.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
|
||||
pub enum OrchestrationConfigStatus {
|
||||
/// No `OrchestrationConfigSnapshot` has been seen yet.
|
||||
#[default]
|
||||
None,
|
||||
Approved,
|
||||
Disapproved,
|
||||
}
|
||||
|
||||
impl OrchestrationConfigStatus {
|
||||
pub fn is_approved(&self) -> bool {
|
||||
matches!(self, Self::Approved)
|
||||
}
|
||||
|
||||
pub fn is_disapproved(&self) -> bool {
|
||||
matches!(self, Self::Disapproved)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Match check — determines whether a `run_agents` call auto-launches.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Returns `true` when the `run_agents` call's run-wide fields match
|
||||
/// the active approved `OrchestrationConfig`, meaning the confirmation
|
||||
/// card can be skipped (auto-launch).
|
||||
///
|
||||
/// Empty/unset fields on the call are treated as inheriting from the
|
||||
/// config (and therefore matching). Fields not in the config
|
||||
/// (`computer_use_enabled`, `skills`, `base_prompt`, `agent_run_configs`,
|
||||
/// per-agent `title`) are excluded from the check.
|
||||
pub fn matches_active_config(request: &RunAgentsRequest, config: &OrchestrationConfig) -> bool {
|
||||
// model_id — empty on the call means "inherit from config" → matches.
|
||||
if !request.model_id.is_empty() && request.model_id != config.model_id {
|
||||
return false;
|
||||
}
|
||||
|
||||
// harness_type
|
||||
if !request.harness_type.is_empty() && request.harness_type != config.harness_type {
|
||||
return false;
|
||||
}
|
||||
|
||||
// execution_mode variant must agree.
|
||||
match (&request.execution_mode, &config.execution_mode) {
|
||||
(super::action::RunAgentsExecutionMode::Local, OrchestrationExecutionMode::Local) => true,
|
||||
(
|
||||
super::action::RunAgentsExecutionMode::Remote {
|
||||
environment_id,
|
||||
worker_host,
|
||||
..
|
||||
},
|
||||
OrchestrationExecutionMode::Remote {
|
||||
environment_id: cfg_env,
|
||||
worker_host: cfg_host,
|
||||
},
|
||||
) => {
|
||||
let env_matches = environment_id.is_empty() || environment_id == cfg_env;
|
||||
let host_matches = worker_host.is_empty() || worker_host == cfg_host;
|
||||
env_matches && host_matches
|
||||
}
|
||||
// Variant mismatch (Local vs Remote).
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Proto ↔ native conversions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
impl OrchestrationConfig {
|
||||
/// Converts from the proto `OrchestrationConfig` message.
|
||||
pub fn from_proto(proto: &api::OrchestrationConfig) -> Self {
|
||||
let execution_mode = match &proto.execution_mode {
|
||||
Some(api::orchestration_config::ExecutionMode::Remote(remote)) => {
|
||||
OrchestrationExecutionMode::Remote {
|
||||
environment_id: remote.environment_id.clone(),
|
||||
worker_host: remote.worker_host.clone(),
|
||||
}
|
||||
}
|
||||
Some(api::orchestration_config::ExecutionMode::Local(_)) | None => {
|
||||
OrchestrationExecutionMode::Local
|
||||
}
|
||||
};
|
||||
|
||||
let harness_type = harness_proto_to_string(proto.harness.as_ref()).unwrap_or_default();
|
||||
|
||||
Self {
|
||||
model_id: proto.model_id.clone(),
|
||||
harness_type,
|
||||
execution_mode,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts to the proto `OrchestrationConfig` message.
|
||||
pub fn to_proto(&self) -> api::OrchestrationConfig {
|
||||
let execution_mode = match &self.execution_mode {
|
||||
OrchestrationExecutionMode::Local => {
|
||||
Some(api::orchestration_config::ExecutionMode::Local(
|
||||
api::orchestration_config::Local {},
|
||||
))
|
||||
}
|
||||
OrchestrationExecutionMode::Remote {
|
||||
environment_id,
|
||||
worker_host,
|
||||
} => Some(api::orchestration_config::ExecutionMode::Remote(
|
||||
api::orchestration_config::Remote {
|
||||
environment_id: environment_id.clone(),
|
||||
worker_host: worker_host.clone(),
|
||||
},
|
||||
)),
|
||||
};
|
||||
|
||||
api::OrchestrationConfig {
|
||||
model_id: self.model_id.clone(),
|
||||
harness: harness_type_to_proto(&self.harness_type),
|
||||
execution_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OrchestrationConfigStatus {
|
||||
/// Converts from the proto `OrchestrationStatus` message.
|
||||
pub fn from_proto(proto: Option<&api::OrchestrationStatus>) -> Self {
|
||||
let Some(status) = proto else {
|
||||
return Self::None;
|
||||
};
|
||||
match &status.status {
|
||||
Some(api::orchestration_status::Status::Approved(_)) => Self::Approved,
|
||||
Some(api::orchestration_status::Status::Disapproved(_)) => Self::Disapproved,
|
||||
None => Self::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts to the proto `OrchestrationStatus` message.
|
||||
pub fn to_proto(&self) -> Option<api::OrchestrationStatus> {
|
||||
match self {
|
||||
Self::None => None,
|
||||
Self::Approved => Some(api::OrchestrationStatus {
|
||||
status: Some(api::orchestration_status::Status::Approved(
|
||||
api::orchestration_status::Approved {},
|
||||
)),
|
||||
}),
|
||||
Self::Disapproved => Some(api::OrchestrationStatus {
|
||||
status: Some(api::orchestration_status::Status::Disapproved(
|
||||
api::orchestration_status::Disapproved {},
|
||||
)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps the proto `Harness` oneof to a client-side string identifier.
|
||||
/// Returns `None` for an unset variant.
|
||||
fn harness_proto_to_string(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(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Converts a client-side harness string identifier to the proto `Harness`
|
||||
/// oneof variant. Returns `None` for empty or unknown strings.
|
||||
fn harness_type_to_proto(harness_type: &str) -> Option<api::Harness> {
|
||||
let variant = match harness_type {
|
||||
"oz" => api::harness::Variant::Oz(api::harness::Oz {}),
|
||||
"claude" => api::harness::Variant::ClaudeCode(api::harness::ClaudeCode {}),
|
||||
"opencode" => api::harness::Variant::OpenCode(api::harness::OpenCode {}),
|
||||
"gemini" => api::harness::Variant::Gemini(api::harness::Gemini {}),
|
||||
"codex" => api::harness::Variant::Codex(api::harness::Codex {}),
|
||||
_ => return None,
|
||||
};
|
||||
Some(api::Harness {
|
||||
variant: Some(variant),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "orchestration_config_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,180 @@
|
||||
use super::*;
|
||||
use crate::agent::action::{RunAgentsAgentRunConfig, RunAgentsExecutionMode, RunAgentsRequest};
|
||||
|
||||
fn make_config(model: &str, harness: &str, remote: bool) -> OrchestrationConfig {
|
||||
OrchestrationConfig {
|
||||
model_id: model.to_string(),
|
||||
harness_type: harness.to_string(),
|
||||
execution_mode: if remote {
|
||||
OrchestrationExecutionMode::Remote {
|
||||
environment_id: "env-1".to_string(),
|
||||
worker_host: "warp".to_string(),
|
||||
}
|
||||
} else {
|
||||
OrchestrationExecutionMode::Local
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn make_request(model: &str, harness: &str, remote: bool) -> RunAgentsRequest {
|
||||
RunAgentsRequest {
|
||||
summary: "test".to_string(),
|
||||
base_prompt: "prompt".to_string(),
|
||||
skills: vec![],
|
||||
model_id: model.to_string(),
|
||||
harness_type: harness.to_string(),
|
||||
execution_mode: if remote {
|
||||
RunAgentsExecutionMode::Remote {
|
||||
environment_id: "env-1".to_string(),
|
||||
worker_host: "warp".to_string(),
|
||||
computer_use_enabled: false,
|
||||
}
|
||||
} else {
|
||||
RunAgentsExecutionMode::Local
|
||||
},
|
||||
agent_run_configs: vec![RunAgentsAgentRunConfig {
|
||||
name: "a".to_string(),
|
||||
prompt: String::new(),
|
||||
title: String::new(),
|
||||
}],
|
||||
plan_id: String::new(),
|
||||
harness_auth_secret_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_match_local() {
|
||||
let config = make_config("auto", "oz", false);
|
||||
let request = make_request("auto", "oz", false);
|
||||
assert!(matches_active_config(&request, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_match_remote() {
|
||||
let config = make_config("auto", "oz", true);
|
||||
let request = make_request("auto", "oz", true);
|
||||
assert!(matches_active_config(&request, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_model_inherits_and_matches() {
|
||||
let config = make_config("auto", "oz", false);
|
||||
let request = make_request("", "oz", false);
|
||||
assert!(matches_active_config(&request, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_harness_inherits_and_matches() {
|
||||
let config = make_config("auto", "oz", false);
|
||||
let request = make_request("auto", "", false);
|
||||
assert!(matches_active_config(&request, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_model_mismatches() {
|
||||
let config = make_config("auto", "oz", false);
|
||||
let request = make_request("claude-4-6-opus-high", "oz", false);
|
||||
assert!(!matches_active_config(&request, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_harness_mismatches() {
|
||||
let config = make_config("auto", "oz", false);
|
||||
let request = make_request("auto", "claude", false);
|
||||
assert!(!matches_active_config(&request, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_mode_variant_mismatch() {
|
||||
let config = make_config("auto", "oz", false);
|
||||
let request = make_request("auto", "oz", true);
|
||||
assert!(!matches_active_config(&request, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_different_environment_mismatches() {
|
||||
let config = make_config("auto", "oz", true);
|
||||
let mut request = make_request("auto", "oz", true);
|
||||
if let RunAgentsExecutionMode::Remote {
|
||||
ref mut environment_id,
|
||||
..
|
||||
} = request.execution_mode
|
||||
{
|
||||
*environment_id = "env-other".to_string();
|
||||
}
|
||||
assert!(!matches_active_config(&request, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_empty_env_inherits_and_matches() {
|
||||
let config = make_config("auto", "oz", true);
|
||||
let mut request = make_request("auto", "oz", true);
|
||||
if let RunAgentsExecutionMode::Remote {
|
||||
ref mut environment_id,
|
||||
..
|
||||
} = request.execution_mode
|
||||
{
|
||||
*environment_id = String::new();
|
||||
}
|
||||
assert!(matches_active_config(&request, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn computer_use_not_in_match_check() {
|
||||
let config = make_config("auto", "oz", true);
|
||||
let mut request = make_request("auto", "oz", true);
|
||||
if let RunAgentsExecutionMode::Remote {
|
||||
ref mut computer_use_enabled,
|
||||
..
|
||||
} = request.execution_mode
|
||||
{
|
||||
*computer_use_enabled = true;
|
||||
}
|
||||
// computer_use_enabled differs but should still match
|
||||
assert!(matches_active_config(&request, &config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_default_is_none() {
|
||||
assert_eq!(
|
||||
OrchestrationConfigStatus::default(),
|
||||
OrchestrationConfigStatus::None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_predicates() {
|
||||
assert!(OrchestrationConfigStatus::Approved.is_approved());
|
||||
assert!(!OrchestrationConfigStatus::Approved.is_disapproved());
|
||||
assert!(OrchestrationConfigStatus::Disapproved.is_disapproved());
|
||||
assert!(!OrchestrationConfigStatus::None.is_approved());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proto_round_trip_config_local() {
|
||||
let config = make_config("auto", "oz", false);
|
||||
let proto = config.to_proto();
|
||||
let round_tripped = OrchestrationConfig::from_proto(&proto);
|
||||
assert_eq!(config, round_tripped);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proto_round_trip_config_remote() {
|
||||
let config = make_config("auto", "claude", true);
|
||||
let proto = config.to_proto();
|
||||
let round_tripped = OrchestrationConfig::from_proto(&proto);
|
||||
assert_eq!(config, round_tripped);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proto_round_trip_status() {
|
||||
for status in [
|
||||
OrchestrationConfigStatus::None,
|
||||
OrchestrationConfigStatus::Approved,
|
||||
OrchestrationConfigStatus::Disapproved,
|
||||
] {
|
||||
let proto = status.to_proto();
|
||||
let round_tripped = OrchestrationConfigStatus::from_proto(proto.as_ref());
|
||||
assert_eq!(status, round_tripped);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user