Fix Local Agent Execution And Auth Checks
- Gate server requests on available credentials - Run local child agents directly without a parent run ID - Include command IDs in Bedrock context and recognize transfer tools
This commit is contained in:
@@ -90,6 +90,12 @@ pub fn extract_new_input_messages(request: &api::Request) -> Vec<ConversationMes
|
|||||||
let mut context =
|
let mut context =
|
||||||
format!("[Running command: {}]\n", running_cmd.command);
|
format!("[Running command: {}]\n", running_cmd.command);
|
||||||
if let Some(snapshot) = &running_cmd.snapshot {
|
if let Some(snapshot) = &running_cmd.snapshot {
|
||||||
|
if !snapshot.command_id.is_empty() {
|
||||||
|
context.push_str(&format!(
|
||||||
|
"[Command ID: {}]\n",
|
||||||
|
snapshot.command_id
|
||||||
|
));
|
||||||
|
}
|
||||||
if !snapshot.output.is_empty() {
|
if !snapshot.output.is_empty() {
|
||||||
context.push_str(&format!(
|
context.push_str(&format!(
|
||||||
"[Terminal output:\n{}\n]\n",
|
"[Terminal output:\n{}\n]\n",
|
||||||
@@ -588,16 +594,15 @@ fn extract_input_messages(request: &api::Request) -> Vec<api::Message> {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
let message_user_query =
|
let message_user_query =
|
||||||
invoke_skill.user_query.as_ref().map(|input_query| {
|
invoke_skill
|
||||||
api::message::UserQuery {
|
.user_query
|
||||||
|
.as_ref()
|
||||||
|
.map(|input_query| api::message::UserQuery {
|
||||||
query: input_query.query.clone(),
|
query: input_query.query.clone(),
|
||||||
context: None,
|
context: None,
|
||||||
referenced_attachments: input_query
|
referenced_attachments: input_query.referenced_attachments.clone(),
|
||||||
.referenced_attachments
|
|
||||||
.clone(),
|
|
||||||
mode: input_query.mode,
|
mode: input_query.mode,
|
||||||
intended_agent: input_query.intended_agent,
|
intended_agent: input_query.intended_agent,
|
||||||
}
|
|
||||||
});
|
});
|
||||||
results.push(api::Message {
|
results.push(api::Message {
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
@@ -1062,10 +1067,83 @@ fn collect_tool_result_ids(content: &MessageContent, ids: &mut std::collections:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_system_prompt(request: &api::Request, global_rules: &[(String, String)]) -> Option<String> {
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
let mut prompt = String::with_capacity(2048);
|
enum AgentMode {
|
||||||
|
Normal,
|
||||||
|
Plan,
|
||||||
|
Orchestrate,
|
||||||
|
Cli,
|
||||||
|
}
|
||||||
|
|
||||||
prompt.push_str("You are Galaxy, an AI coding assistant embedded in a terminal application. You help users with software engineering tasks including writing code, debugging, explaining concepts, and navigating codebases.\n\n");
|
fn request_agent_mode(request: &api::Request) -> AgentMode {
|
||||||
|
let Some(api::request::Input {
|
||||||
|
r#type: Some(api::request::input::Type::UserInputs(user_inputs)),
|
||||||
|
..
|
||||||
|
}) = &request.input
|
||||||
|
else {
|
||||||
|
return AgentMode::Normal;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut mode = AgentMode::Normal;
|
||||||
|
for user_input in &user_inputs.inputs {
|
||||||
|
match &user_input.input {
|
||||||
|
Some(api::request::input::user_inputs::user_input::Input::CliAgentUserQuery(_)) => {
|
||||||
|
return AgentMode::Cli;
|
||||||
|
}
|
||||||
|
Some(api::request::input::user_inputs::user_input::Input::UserQuery(query)) => {
|
||||||
|
match query.mode.as_ref().and_then(|mode| mode.r#type.as_ref()) {
|
||||||
|
Some(api::user_query_mode::Type::Plan(())) => mode = AgentMode::Plan,
|
||||||
|
Some(api::user_query_mode::Type::Orchestrate(())) => {
|
||||||
|
mode = AgentMode::Orchestrate
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(api::request::input::user_inputs::user_input::Input::ToolCallResult(result))
|
||||||
|
if tool_result_is_cli_command(result) =>
|
||||||
|
{
|
||||||
|
return AgentMode::Cli
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mode
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_result_is_cli_command(result: &api::request::input::ToolCallResult) -> bool {
|
||||||
|
use api::request::input::tool_call_result::Result;
|
||||||
|
|
||||||
|
match &result.result {
|
||||||
|
Some(Result::RunShellCommand(result)) => matches!(
|
||||||
|
result.result,
|
||||||
|
Some(api::run_shell_command_result::Result::LongRunningCommandSnapshot(_))
|
||||||
|
),
|
||||||
|
Some(
|
||||||
|
Result::WriteToLongRunningShellCommand(_)
|
||||||
|
| Result::ReadShellCommandOutput(_)
|
||||||
|
| Result::TransferShellCommandControlToUser(_),
|
||||||
|
) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_system_prompt(
|
||||||
|
request: &api::Request,
|
||||||
|
global_rules: &[(String, String)],
|
||||||
|
) -> Option<String> {
|
||||||
|
let mode = request_agent_mode(request);
|
||||||
|
let tool_names = extract_tools(request)
|
||||||
|
.into_iter()
|
||||||
|
.map(|tool| tool.name)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut prompt = String::with_capacity(4096);
|
||||||
|
|
||||||
|
prompt.push_str(
|
||||||
|
"You are Galaxy, an AI software-engineering and terminal agent embedded in the user's \
|
||||||
|
terminal. Your job is to complete the user's task, not merely describe how they could \
|
||||||
|
complete it. You are especially capable at inspecting codebases, editing files, running \
|
||||||
|
commands, diagnosing failures, and validating changes.\n\n",
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(input) = &request.input {
|
if let Some(input) = &request.input {
|
||||||
if let Some(context) = &input.context {
|
if let Some(context) = &input.context {
|
||||||
@@ -1120,7 +1198,9 @@ pub fn extract_system_prompt(request: &api::Request, global_rules: &[(String, St
|
|||||||
// Inject global rules from the local CloudModel (stored as AIFact/AIMemory)
|
// Inject global rules from the local CloudModel (stored as AIFact/AIMemory)
|
||||||
if !global_rules.is_empty() {
|
if !global_rules.is_empty() {
|
||||||
prompt.push_str("## Global Rules\n");
|
prompt.push_str("## Global Rules\n");
|
||||||
prompt.push_str("The following rules have been configured by the user and should be followed:\n\n");
|
prompt.push_str(
|
||||||
|
"The following rules have been configured by the user and should be followed:\n\n",
|
||||||
|
);
|
||||||
for (name, content) in global_rules {
|
for (name, content) in global_rules {
|
||||||
if !name.is_empty() {
|
if !name.is_empty() {
|
||||||
prompt.push_str(&format!("### {}\n", name));
|
prompt.push_str(&format!("### {}\n", name));
|
||||||
@@ -1130,28 +1210,119 @@ pub fn extract_system_prompt(request: &api::Request, global_rules: &[(String, St
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt.push_str("## Tool Usage\n");
|
prompt.push_str("## Operating Contract\n");
|
||||||
prompt.push_str("You have been given every tool you need to complete your tasks. Use them to achieve results with as few calls and as little back-and-forth as possible.\n\n");
|
|
||||||
prompt.push_str("**How to choose tools:**\n");
|
|
||||||
prompt.push_str("- For reading, writing, searching, and navigating files on the local filesystem, use your filesystem tools (`read_files`, `file_glob`, `grep`, `apply_file_diffs`).\n");
|
|
||||||
prompt.push_str("- For running commands, installing packages, building, testing, and any shell operation, use `run_shell_command`.\n");
|
|
||||||
prompt.push_str("- For tasks that require interacting with external services, web UIs, or capabilities not covered by your filesystem and shell tools, use your MCP tools.\n");
|
|
||||||
prompt.push_str("- For complex multi-step tasks where a single script would replace many tool calls, write code (Python, Node, bash) via `run_shell_command` to reduce round-trips. But never use scripts for simple operations that a single command handles.\n\n");
|
|
||||||
prompt.push_str("**Critical rules:**\n");
|
|
||||||
prompt.push_str(
|
prompt.push_str(
|
||||||
"- Use ONLY the tools in your tool configuration. Never invent or guess tool names.\n",
|
"- If the user asks for a change, carry it through inspection, implementation, and \
|
||||||
|
proportionate validation. Do not stop after proposing a plan unless the request is in \
|
||||||
|
plan mode or user input is genuinely required.\n",
|
||||||
);
|
);
|
||||||
prompt.push_str("- ALWAYS pass `--no-pager` (or equivalent) flags to CLI tools like git, less, man, etc. Tools that lock stdin will freeze the session.\n");
|
prompt.push_str(
|
||||||
prompt.push_str("- Output text directly in your response instead of using `echo` — echo requires user approval and adds unnecessary friction.\n");
|
"- Inspect relevant files and current state before making claims. Preserve unrelated user \
|
||||||
prompt.push_str("- Use absolute paths based on the working directory shown above.\n");
|
changes and make the smallest coherent change that solves the problem.\n",
|
||||||
prompt.push_str("- Be logical in your tool choices. Read files before making claims about code. List files before assuming project structure.\n");
|
);
|
||||||
prompt.push_str("- Be concise and direct.\n");
|
prompt.push_str(
|
||||||
|
"- Prefer non-interactive commands. Disable pagers (`--no-pager`, `PAGER=cat`, or the \
|
||||||
|
tool-specific equivalent) and avoid commands that wait for an editor or prompt unless \
|
||||||
|
interaction is intentional.\n",
|
||||||
|
);
|
||||||
|
prompt.push_str(
|
||||||
|
"- Treat tool results as evidence. Check exit codes and output, fix failures when they are \
|
||||||
|
in scope, and never claim a build, test, command, or edit succeeded without a successful \
|
||||||
|
result.\n",
|
||||||
|
);
|
||||||
|
prompt.push_str(
|
||||||
|
"- Ask the user only when a missing choice materially changes the result or when an action \
|
||||||
|
requires authority you do not have. Otherwise make a reasonable, scoped assumption and \
|
||||||
|
continue.\n",
|
||||||
|
);
|
||||||
|
prompt.push_str(
|
||||||
|
"- Avoid destructive or irreversible commands unless they are clearly requested and their \
|
||||||
|
target is verified. Never overwrite unrelated work.\n",
|
||||||
|
);
|
||||||
|
prompt.push_str("- Keep progress and final responses concise, concrete, and honest.\n\n");
|
||||||
|
|
||||||
|
match mode {
|
||||||
|
AgentMode::Normal => {}
|
||||||
|
AgentMode::Plan => {
|
||||||
|
prompt.push_str("## Plan Mode\n");
|
||||||
|
prompt.push_str(
|
||||||
|
"Analyze and produce an implementation-ready plan. You may inspect files and run \
|
||||||
|
read-only commands, but do not edit files, install dependencies, or run commands \
|
||||||
|
that change external state. Resolve as much uncertainty as possible through \
|
||||||
|
inspection before presenting the plan.\n\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
AgentMode::Orchestrate => {
|
||||||
|
prompt.push_str("## Orchestration Mode\n");
|
||||||
|
prompt.push_str(
|
||||||
|
"Coordinate independent work when delegation materially reduces latency or improves \
|
||||||
|
coverage. Give each child agent a bounded task and synthesize its result. Do not \
|
||||||
|
delegate trivial work or work that depends on unfinished local context.\n\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
AgentMode::Cli => {
|
||||||
|
prompt.push_str("## Running Command Monitor\n");
|
||||||
|
prompt.push_str(
|
||||||
|
"This turn concerns a running or just-finished shell command. Act as its dedicated \
|
||||||
|
monitor while still following the user's steering messages. Use the command ID from \
|
||||||
|
the running-command context or tool result for every read/write operation. If the \
|
||||||
|
result says the command finished, report its outcome and stop polling. Otherwise, \
|
||||||
|
poll with `read_shell_command_output`; use a short delay for active progress and \
|
||||||
|
`wait_until_complete` only when no intervention is expected. Use \
|
||||||
|
`write_to_long_running_shell_command` only when the process needs input. Never start \
|
||||||
|
a duplicate command merely to check its state, and never report completion while a \
|
||||||
|
result says it is still running. If user interaction is the right next step and the \
|
||||||
|
transfer tool is available, transfer control with a clear reason.\n\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt.push_str("## Available Tools\n");
|
||||||
|
if tool_names.is_empty() {
|
||||||
|
prompt.push_str("No tools are available for this request. Do not invent tool calls.\n\n");
|
||||||
|
} else {
|
||||||
|
prompt.push_str(&format!(
|
||||||
|
"Use only these tools: {}.\n\n",
|
||||||
|
tool_names.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let has_tool = |name: &str| tool_names.iter().any(|tool_name| tool_name == name);
|
||||||
|
if [
|
||||||
|
"read_files",
|
||||||
|
"apply_file_diffs",
|
||||||
|
"grep",
|
||||||
|
"file_glob",
|
||||||
|
"search_codebase",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.any(has_tool)
|
||||||
|
{
|
||||||
|
prompt.push_str(
|
||||||
|
"- Use filesystem/search tools to understand the codebase and focused diff tools to edit \
|
||||||
|
it. Use paths rooted in the working directory and absolute paths when a schema requires \
|
||||||
|
them.\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if has_tool("run_shell_command") {
|
||||||
|
prompt.push_str(
|
||||||
|
"- Use `run_shell_command` for builds, tests, package managers, git, and terminal work. \
|
||||||
|
Set `is_read_only` accurately, set `uses_pager=false`, and set \
|
||||||
|
`wait_until_complete=false` for commands that may run longer than a few seconds so they \
|
||||||
|
can be monitored asynchronously.\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !tool_names.is_empty() {
|
||||||
|
prompt.push_str(
|
||||||
|
"- Batch independent reads and searches when the tool schema allows it, but do not hide \
|
||||||
|
important intermediate failures inside a large opaque script.\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
prompt.push_str("- Never invent tool names or parameters.\n");
|
||||||
|
|
||||||
Some(prompt)
|
Some(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_tools(request: &api::Request) -> Vec<ToolDefinition> {
|
pub fn extract_tools(request: &api::Request) -> Vec<ToolDefinition> {
|
||||||
// Always start with the full set of default tools.
|
|
||||||
let mut tools = default_tool_definitions();
|
let mut tools = default_tool_definitions();
|
||||||
let mut seen_names: std::collections::HashSet<String> =
|
let mut seen_names: std::collections::HashSet<String> =
|
||||||
tools.iter().map(|t| t.name.clone()).collect();
|
tools.iter().map(|t| t.name.clone()).collect();
|
||||||
@@ -1202,30 +1373,72 @@ pub fn extract_tools(request: &api::Request) -> Vec<ToolDefinition> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out suggest_next_prompt — its action executor waits on a oneshot
|
// Only advertise tools the client reported for this request. CLI-agent turns
|
||||||
// channel for UI interaction that never fires in the Bedrock path, causing
|
// use their narrower capability list so the command monitor stays focused.
|
||||||
// the conversation to stay InProgress forever.
|
if let Some(supported_tools) = supported_tool_types(request) {
|
||||||
// Filter out start_agent/send_message_to_agent — sub-agents are disabled.
|
tools.retain(|tool| tool_name_is_supported(&tool.name, &supported_tools));
|
||||||
tools.retain(|t| {
|
}
|
||||||
t.name != "suggest_next_prompt"
|
|
||||||
&& t.name != "start_agent"
|
|
||||||
&& t.name != "send_message_to_agent"
|
|
||||||
});
|
|
||||||
|
|
||||||
tools
|
tools
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn supported_tool_types(request: &api::Request) -> Option<HashSet<api::ToolType>> {
|
||||||
|
let settings = request.settings.as_ref()?;
|
||||||
|
let raw_tools = if request_agent_mode(request) == AgentMode::Cli {
|
||||||
|
&settings.supported_cli_agent_tools
|
||||||
|
} else {
|
||||||
|
&settings.supported_tools
|
||||||
|
};
|
||||||
|
Some(
|
||||||
|
raw_tools
|
||||||
|
.iter()
|
||||||
|
.filter_map(|value| api::ToolType::try_from(*value).ok())
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_name_is_supported(name: &str, supported: &HashSet<api::ToolType>) -> bool {
|
||||||
|
use api::ToolType;
|
||||||
|
|
||||||
|
let has = |tool| supported.contains(&tool);
|
||||||
|
match name {
|
||||||
|
"run_shell_command" => has(ToolType::RunShellCommand),
|
||||||
|
"read_files" => has(ToolType::ReadFiles),
|
||||||
|
"apply_file_diffs" => has(ToolType::ApplyFileDiffs),
|
||||||
|
"grep" => has(ToolType::Grep),
|
||||||
|
"file_glob" => has(ToolType::FileGlob) || has(ToolType::FileGlobV2),
|
||||||
|
"search_codebase" => has(ToolType::SearchCodebase),
|
||||||
|
"write_to_long_running_shell_command" => has(ToolType::WriteToLongRunningShellCommand),
|
||||||
|
"read_shell_command_output" => has(ToolType::ReadShellCommandOutput),
|
||||||
|
"transfer_shell_command_control_to_user" => {
|
||||||
|
has(ToolType::TransferShellCommandControlToUser)
|
||||||
|
}
|
||||||
|
"read_mcp_resource" => has(ToolType::ReadMcpResource),
|
||||||
|
name if name.starts_with("mcp__") => has(ToolType::CallMcpTool),
|
||||||
|
"read_plan" | "read_notebook" => has(ToolType::ReadDocuments),
|
||||||
|
"create_plan" | "create_notebook" => has(ToolType::CreateDocuments),
|
||||||
|
"edit_plan" | "edit_notebook" => has(ToolType::EditDocuments),
|
||||||
|
"start_agent" => has(ToolType::Subagent) || has(ToolType::StartAgentV2),
|
||||||
|
"ask_user_question" => has(ToolType::AskUserQuestion),
|
||||||
|
"read_skill" => has(ToolType::ReadSkill),
|
||||||
|
"fetch_conversation" => has(ToolType::FetchConversation),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_tool_definitions() -> Vec<ToolDefinition> {
|
pub fn default_tool_definitions() -> Vec<ToolDefinition> {
|
||||||
vec![
|
vec![
|
||||||
ToolDefinition {
|
ToolDefinition {
|
||||||
name: "run_shell_command".to_string(),
|
name: "run_shell_command".to_string(),
|
||||||
description: "Execute a shell command in the user's terminal and return its output. Use for running builds, tests, git operations, installing packages, or any shell operation. Commands run in the user's actual shell with their environment. Set is_read_only=true for read-only commands (ls, cat, git status) to enable auto-execution. Always use --no-pager for git commands.".to_string(),
|
description: "Execute a shell command in the user's terminal and return its output. Use for builds, tests, git, package managers, and other shell work. Commands run in the user's actual shell and environment. Set wait_until_complete=false for commands that might run longer than a few seconds; Galaxy will return a command_id that can be monitored with read_shell_command_output.".to_string(),
|
||||||
input_schema: serde_json::json!({
|
input_schema: serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": { "type": "string", "description": "The shell command to execute" },
|
"command": { "type": "string", "description": "The shell command to execute" },
|
||||||
"is_read_only": { "type": "boolean", "description": "True if command only reads data and makes no changes" },
|
"is_read_only": { "type": "boolean", "description": "True if command only reads data and makes no changes" },
|
||||||
"is_risky": { "type": "boolean", "description": "True if command is destructive or irreversible (rm -rf, git push --force)" }
|
"is_risky": { "type": "boolean", "description": "True if command is destructive or irreversible (rm -rf, git push --force)" },
|
||||||
|
"uses_pager": { "type": "boolean", "default": false, "description": "True only when the command intentionally launches a pager. Prefer false." },
|
||||||
|
"wait_until_complete": { "type": "boolean", "default": false, "description": "Whether to wait for the process to exit. Defaults to false so builds, servers, watchers, and other potentially long-running commands can be monitored asynchronously." }
|
||||||
},
|
},
|
||||||
"required": ["command"]
|
"required": ["command"]
|
||||||
}),
|
}),
|
||||||
@@ -1291,22 +1504,39 @@ pub fn default_tool_definitions() -> Vec<ToolDefinition> {
|
|||||||
},
|
},
|
||||||
ToolDefinition {
|
ToolDefinition {
|
||||||
name: "write_to_long_running_shell_command".to_string(),
|
name: "write_to_long_running_shell_command".to_string(),
|
||||||
description: "Send input (stdin) to a currently running shell command. Use this to interact with commands that are waiting for input, like interactive prompts, REPLs, or commands that accept piped input.".to_string(),
|
description: "Send input to a currently running shell command identified by command_id. Use only when the command is waiting for input, such as an interactive prompt or REPL.".to_string(),
|
||||||
input_schema: serde_json::json!({
|
input_schema: serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"input": { "type": "string", "description": "Text to send as stdin to the running command" }
|
"command_id": { "type": "string", "description": "Command ID returned by a long-running command result" },
|
||||||
|
"input": { "type": "string", "description": "Text to send to the running command" },
|
||||||
|
"mode": { "type": "string", "enum": ["raw", "line", "block"], "default": "raw", "description": "raw sends exact bytes; line submits one line with Enter; block pastes multiline input" }
|
||||||
},
|
},
|
||||||
"required": ["input"]
|
"required": ["command_id", "input"]
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
ToolDefinition {
|
ToolDefinition {
|
||||||
name: "read_shell_command_output".to_string(),
|
name: "read_shell_command_output".to_string(),
|
||||||
description: "Read the latest output from a previously started long-running shell command. Use to check progress or get results from commands that are still running.".to_string(),
|
description: "Read output from a previously started long-running shell command identified by command_id. Use wait_seconds for a timed poll, or wait_until_complete=true only when no intervention is expected.".to_string(),
|
||||||
input_schema: serde_json::json!({
|
input_schema: serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {},
|
"properties": {
|
||||||
"required": []
|
"command_id": { "type": "string", "description": "Command ID returned by a long-running command result" },
|
||||||
|
"wait_seconds": { "type": "integer", "minimum": 0, "maximum": 120, "default": 2, "description": "Seconds to wait before returning a fresh snapshot; defaults to 2" },
|
||||||
|
"wait_until_complete": { "type": "boolean", "description": "Wait until the command exits instead of returning a timed snapshot" }
|
||||||
|
},
|
||||||
|
"required": ["command_id"]
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
ToolDefinition {
|
||||||
|
name: "transfer_shell_command_control_to_user".to_string(),
|
||||||
|
description: "Transfer control of the current long-running shell command to the user when manual interaction is needed. Explain why control is being transferred.".to_string(),
|
||||||
|
input_schema: serde_json::json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"reason": { "type": "string", "description": "Concise explanation of what the user needs to do" }
|
||||||
|
},
|
||||||
|
"required": ["reason"]
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
ToolDefinition {
|
ToolDefinition {
|
||||||
@@ -1450,17 +1680,11 @@ fn extract_tool_result_content(result: &api::request::input::ToolCallResult) ->
|
|||||||
api::request::input::tool_call_result::Result::RunShellCommand(cmd_result) => {
|
api::request::input::tool_call_result::Result::RunShellCommand(cmd_result) => {
|
||||||
match &cmd_result.result {
|
match &cmd_result.result {
|
||||||
Some(api::run_shell_command_result::Result::CommandFinished(finished)) => {
|
Some(api::run_shell_command_result::Result::CommandFinished(finished)) => {
|
||||||
let content = if finished.output.is_empty() {
|
command_finished_content(finished)
|
||||||
format!("Exit code: {}\n(no output)", finished.exit_code)
|
|
||||||
} else {
|
|
||||||
format!("Exit code: {}\n{}", finished.exit_code, finished.output)
|
|
||||||
};
|
|
||||||
let is_error = finished.exit_code != 0;
|
|
||||||
(content, is_error)
|
|
||||||
}
|
}
|
||||||
Some(api::run_shell_command_result::Result::LongRunningCommandSnapshot(
|
Some(api::run_shell_command_result::Result::LongRunningCommandSnapshot(
|
||||||
snapshot,
|
snapshot,
|
||||||
)) => (snapshot.output.clone(), false),
|
)) => (long_running_command_content(snapshot), false),
|
||||||
Some(api::run_shell_command_result::Result::PermissionDenied(denied)) => {
|
Some(api::run_shell_command_result::Result::PermissionDenied(denied)) => {
|
||||||
let reason = match &denied.reason {
|
let reason = match &denied.reason {
|
||||||
Some(api::permission_denied::Reason::DenylistedCommand(())) => {
|
Some(api::permission_denied::Reason::DenylistedCommand(())) => {
|
||||||
@@ -1643,24 +1867,58 @@ fn extract_tool_result_content(result: &api::request::input::ToolCallResult) ->
|
|||||||
api::write_to_long_running_shell_command_result::Result::LongRunningCommandSnapshot(
|
api::write_to_long_running_shell_command_result::Result::LongRunningCommandSnapshot(
|
||||||
snapshot,
|
snapshot,
|
||||||
),
|
),
|
||||||
) => (snapshot.output.clone(), false),
|
) => (long_running_command_content(snapshot), false),
|
||||||
Some(
|
Some(
|
||||||
api::write_to_long_running_shell_command_result::Result::CommandFinished(
|
api::write_to_long_running_shell_command_result::Result::CommandFinished(
|
||||||
finished,
|
finished,
|
||||||
),
|
),
|
||||||
) => {
|
) => command_finished_content(finished),
|
||||||
let content = format!(
|
|
||||||
"Exit code: {}\n{}",
|
|
||||||
finished.exit_code, finished.output
|
|
||||||
);
|
|
||||||
let is_error = finished.exit_code != 0;
|
|
||||||
(content, is_error)
|
|
||||||
}
|
|
||||||
Some(api::write_to_long_running_shell_command_result::Result::Error(_)) => {
|
Some(api::write_to_long_running_shell_command_result::Result::Error(_)) => {
|
||||||
("Error: shell command not found.".to_string(), true)
|
("Error: shell command not found.".to_string(), true)
|
||||||
}
|
}
|
||||||
None => ("Write to shell command completed.".to_string(), false),
|
None => ("Write to shell command completed.".to_string(), false),
|
||||||
},
|
},
|
||||||
|
api::request::input::tool_call_result::Result::ReadShellCommandOutput(read_result) => {
|
||||||
|
match &read_result.result {
|
||||||
|
Some(
|
||||||
|
api::read_shell_command_output_result::Result::LongRunningCommandSnapshot(
|
||||||
|
snapshot,
|
||||||
|
),
|
||||||
|
) => (long_running_command_content(snapshot), false),
|
||||||
|
Some(api::read_shell_command_output_result::Result::CommandFinished(
|
||||||
|
finished,
|
||||||
|
)) => command_finished_content(finished),
|
||||||
|
Some(api::read_shell_command_output_result::Result::Error(_)) => {
|
||||||
|
("Error: shell command not found.".to_string(), true)
|
||||||
|
}
|
||||||
|
None => ("Read shell command output completed.".to_string(), false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
api::request::input::tool_call_result::Result::TransferShellCommandControlToUser(
|
||||||
|
transfer_result,
|
||||||
|
) => match &transfer_result.result {
|
||||||
|
Some(
|
||||||
|
api::transfer_shell_command_control_to_user_result::Result::LongRunningCommandSnapshot(
|
||||||
|
snapshot,
|
||||||
|
),
|
||||||
|
) => {
|
||||||
|
let mut content = long_running_command_content(snapshot);
|
||||||
|
content.push_str(
|
||||||
|
"\nControl has been transferred to the user. Do not write to the command \
|
||||||
|
until control is returned.",
|
||||||
|
);
|
||||||
|
(content, false)
|
||||||
|
}
|
||||||
|
Some(
|
||||||
|
api::transfer_shell_command_control_to_user_result::Result::CommandFinished(
|
||||||
|
finished,
|
||||||
|
),
|
||||||
|
) => command_finished_content(finished),
|
||||||
|
Some(api::transfer_shell_command_control_to_user_result::Result::Error(_)) => {
|
||||||
|
("Error: shell command not found.".to_string(), true)
|
||||||
|
}
|
||||||
|
None => ("Shell command control transferred to the user.".to_string(), false),
|
||||||
|
},
|
||||||
api::request::input::tool_call_result::Result::StartAgent(start_agent_result) => {
|
api::request::input::tool_call_result::Result::StartAgent(start_agent_result) => {
|
||||||
match &start_agent_result.result {
|
match &start_agent_result.result {
|
||||||
Some(api::start_agent_result::Result::Success(success)) => {
|
Some(api::start_agent_result::Result::Success(success)) => {
|
||||||
@@ -1788,6 +2046,38 @@ fn extract_tool_result_content(result: &api::request::input::ToolCallResult) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn command_finished_content(finished: &api::ShellCommandFinished) -> (String, bool) {
|
||||||
|
let mut content = String::new();
|
||||||
|
if !finished.command_id.is_empty() {
|
||||||
|
content.push_str(&format!("Command ID: {}\n", finished.command_id));
|
||||||
|
}
|
||||||
|
content.push_str(&format!(
|
||||||
|
"Command finished with exit code {}.",
|
||||||
|
finished.exit_code
|
||||||
|
));
|
||||||
|
if finished.output.is_empty() {
|
||||||
|
content.push_str("\n(no output)");
|
||||||
|
} else {
|
||||||
|
content.push_str(&format!("\nOutput:\n{}", finished.output));
|
||||||
|
}
|
||||||
|
(content, finished.exit_code != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn long_running_command_content(snapshot: &api::LongRunningShellCommandSnapshot) -> String {
|
||||||
|
let output = if snapshot.output.is_empty() {
|
||||||
|
"(no output yet)"
|
||||||
|
} else {
|
||||||
|
&snapshot.output
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"Command is still running.\nCommand ID: {}\nCurrent terminal output:\n{}\n\
|
||||||
|
Continue monitoring with `read_shell_command_output` using command_id `{}`. \
|
||||||
|
Use `write_to_long_running_shell_command` with the same command_id only if input is \
|
||||||
|
required. Do not report the command as complete while it is still running.",
|
||||||
|
snapshot.command_id, output, snapshot.command_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn extract_messages_from_request(request: &api::Request) -> Vec<ConversationMessage> {
|
pub fn extract_messages_from_request(request: &api::Request) -> Vec<ConversationMessage> {
|
||||||
let mut messages = Vec::new();
|
let mut messages = Vec::new();
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use warp_multi_agent_api as api;
|
||||||
|
|
||||||
use super::sanitize_messages_for_bedrock;
|
use super::{
|
||||||
|
extract_new_input_messages, extract_system_prompt, extract_tools, sanitize_messages_for_bedrock,
|
||||||
|
};
|
||||||
use crate::ai::bedrock::convert::{ContentPart, ConversationMessage, MessageContent, MessageRole};
|
use crate::ai::bedrock::convert::{ContentPart, ConversationMessage, MessageContent, MessageRole};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -45,3 +48,168 @@ fn test_sanitize_messages_prepends_synthetic_tool_result_before_existing_user_te
|
|||||||
ContentPart::Text(text) if text == &existing_user_text
|
ContentPart::Text(text) if text == &existing_user_text
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn advertised_tools_follow_client_capabilities_and_include_local_subagents() {
|
||||||
|
let request = api::Request {
|
||||||
|
settings: Some(api::request::Settings {
|
||||||
|
supported_tools: vec![
|
||||||
|
api::ToolType::RunShellCommand.into(),
|
||||||
|
api::ToolType::ReadFiles.into(),
|
||||||
|
api::ToolType::Subagent.into(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let names = extract_tools(&request)
|
||||||
|
.into_iter()
|
||||||
|
.map(|tool| tool.name)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
names,
|
||||||
|
vec!["run_shell_command", "read_files", "start_agent"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plan_mode_prompt_prohibits_mutation() {
|
||||||
|
let request = api::Request {
|
||||||
|
input: Some(api::request::Input {
|
||||||
|
r#type: Some(api::request::input::Type::UserInputs(
|
||||||
|
api::request::input::UserInputs {
|
||||||
|
inputs: vec![api::request::input::user_inputs::UserInput {
|
||||||
|
input: Some(
|
||||||
|
api::request::input::user_inputs::user_input::Input::UserQuery(
|
||||||
|
api::request::input::UserQuery {
|
||||||
|
query: "plan the change".to_string(),
|
||||||
|
mode: Some(api::UserQueryMode {
|
||||||
|
r#type: Some(api::user_query_mode::Type::Plan(())),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let prompt = extract_system_prompt(&request, &[]).unwrap();
|
||||||
|
assert!(prompt.contains("## Plan Mode"));
|
||||||
|
assert!(prompt.contains("do not edit files"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn running_command_turn_gets_monitor_prompt_and_cli_tools() {
|
||||||
|
let request = api::Request {
|
||||||
|
input: Some(api::request::Input {
|
||||||
|
r#type: Some(api::request::input::Type::UserInputs(
|
||||||
|
api::request::input::UserInputs {
|
||||||
|
inputs: vec![api::request::input::user_inputs::UserInput {
|
||||||
|
input: Some(
|
||||||
|
api::request::input::user_inputs::user_input::Input::CliAgentUserQuery(
|
||||||
|
api::request::input::CliAgentUserQuery {
|
||||||
|
user_query: Some(api::request::input::UserQuery {
|
||||||
|
query: "monitor this".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
running_command: Some(api::RunningShellCommand {
|
||||||
|
command: "cargo test".to_string(),
|
||||||
|
snapshot: Some(api::LongRunningShellCommandSnapshot {
|
||||||
|
command_id: "block-123".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
settings: Some(api::request::Settings {
|
||||||
|
supported_tools: vec![api::ToolType::RunShellCommand.into()],
|
||||||
|
supported_cli_agent_tools: vec![
|
||||||
|
api::ToolType::WriteToLongRunningShellCommand.into(),
|
||||||
|
api::ToolType::ReadShellCommandOutput.into(),
|
||||||
|
api::ToolType::TransferShellCommandControlToUser.into(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let names = extract_tools(&request)
|
||||||
|
.into_iter()
|
||||||
|
.map(|tool| tool.name)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
names,
|
||||||
|
vec![
|
||||||
|
"write_to_long_running_shell_command",
|
||||||
|
"read_shell_command_output",
|
||||||
|
"transfer_shell_command_control_to_user",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let prompt = extract_system_prompt(&request, &[]).unwrap();
|
||||||
|
assert!(prompt.contains("## Running Command Monitor"));
|
||||||
|
assert!(prompt.contains("command ID"));
|
||||||
|
assert!(prompt.contains("read_shell_command_output"));
|
||||||
|
assert!(!prompt.contains("- Use `run_shell_command`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn long_running_tool_result_preserves_command_id() {
|
||||||
|
let request = api::Request {
|
||||||
|
input: Some(api::request::Input {
|
||||||
|
r#type: Some(api::request::input::Type::UserInputs(
|
||||||
|
api::request::input::UserInputs {
|
||||||
|
inputs: vec![api::request::input::user_inputs::UserInput {
|
||||||
|
input: Some(
|
||||||
|
api::request::input::user_inputs::user_input::Input::ToolCallResult(
|
||||||
|
api::request::input::ToolCallResult {
|
||||||
|
tool_call_id: "tool-1".to_string(),
|
||||||
|
result: Some(
|
||||||
|
api::request::input::tool_call_result::Result::RunShellCommand(
|
||||||
|
api::RunShellCommandResult {
|
||||||
|
command: "cargo test".to_string(),
|
||||||
|
result: Some(
|
||||||
|
api::run_shell_command_result::Result::LongRunningCommandSnapshot(
|
||||||
|
api::LongRunningShellCommandSnapshot {
|
||||||
|
command_id: "block-456".to_string(),
|
||||||
|
output: "running 42 tests".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let messages = extract_new_input_messages(&request);
|
||||||
|
let MessageContent::ToolResult { content, .. } = &messages[0].content else {
|
||||||
|
panic!("expected tool result");
|
||||||
|
};
|
||||||
|
assert!(content.contains("Command ID: block-456"));
|
||||||
|
assert!(content.contains("running 42 tests"));
|
||||||
|
assert!(content.contains("read_shell_command_output"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ pub fn bedrock_stream_to_response_events(
|
|||||||
current_tool_name, current_tool_use_id
|
current_tool_name, current_tool_use_id
|
||||||
);
|
);
|
||||||
let error_text = format!(
|
let error_text = format!(
|
||||||
"Error: '{}' is not a valid tool. Available tools are: run_shell_command, read_files, apply_file_diffs, grep, file_glob. Please use one of these tools instead.",
|
"Error: '{}' is not a valid tool for this request. Use one of the tools in the current tool configuration; do not invent tool names.",
|
||||||
current_tool_name
|
current_tool_name
|
||||||
);
|
);
|
||||||
let input_json: serde_json::Value = serde_json::from_str(¤t_tool_input_json)
|
let input_json: serde_json::Value = serde_json::from_str(¤t_tool_input_json)
|
||||||
@@ -846,15 +846,36 @@ pub fn build_tool_call_message(
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
let is_read_only = input
|
||||||
|
.get("is_read_only")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let uses_pager = input
|
||||||
|
.get("uses_pager")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let is_risky = input
|
||||||
|
.get("is_risky")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let wait_until_complete = input
|
||||||
|
.get("wait_until_complete")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let wait_until_complete_value = Some(
|
||||||
|
api::message::tool_call::run_shell_command::WaitUntilCompleteValue::WaitUntilComplete(
|
||||||
|
wait_until_complete,
|
||||||
|
),
|
||||||
|
);
|
||||||
Some(api::message::tool_call::Tool::RunShellCommand(
|
Some(api::message::tool_call::Tool::RunShellCommand(
|
||||||
api::message::tool_call::RunShellCommand {
|
api::message::tool_call::RunShellCommand {
|
||||||
command,
|
command,
|
||||||
is_read_only: false,
|
is_read_only,
|
||||||
uses_pager: true,
|
uses_pager,
|
||||||
citations: vec![],
|
citations: vec![],
|
||||||
is_risky: false,
|
is_risky,
|
||||||
risk_category: 0,
|
risk_category: 0,
|
||||||
wait_until_complete_value: None,
|
wait_until_complete_value,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -944,12 +965,14 @@ pub fn build_tool_call_message(
|
|||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let path = input
|
||||||
|
.get("path")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
Some(api::message::tool_call::Tool::FileGlob(
|
Some(api::message::tool_call::Tool::FileGlob(
|
||||||
api::message::tool_call::FileGlob {
|
api::message::tool_call::FileGlob { patterns, path },
|
||||||
patterns,
|
|
||||||
path: String::new(),
|
|
||||||
},
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
"search_codebase" => {
|
"search_codebase" => {
|
||||||
@@ -972,27 +995,80 @@ pub fn build_tool_call_message(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
"write_to_long_running_shell_command" => {
|
"write_to_long_running_shell_command" => {
|
||||||
|
use api::message::tool_call::write_to_long_running_shell_command::mode::Mode;
|
||||||
|
|
||||||
let text_input = input
|
let text_input = input
|
||||||
.get("input")
|
.get("input")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
let command_id = input
|
||||||
|
.get("command_id")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let mode = input.get("mode").and_then(|v| v.as_str()).map(|mode| {
|
||||||
|
api::message::tool_call::write_to_long_running_shell_command::Mode {
|
||||||
|
mode: Some(match mode {
|
||||||
|
"line" => Mode::Line(()),
|
||||||
|
"block" => Mode::Block(()),
|
||||||
|
_ => Mode::Raw(()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
Some(
|
Some(
|
||||||
api::message::tool_call::Tool::WriteToLongRunningShellCommand(
|
api::message::tool_call::Tool::WriteToLongRunningShellCommand(
|
||||||
api::message::tool_call::WriteToLongRunningShellCommand {
|
api::message::tool_call::WriteToLongRunningShellCommand {
|
||||||
input: text_input.into_bytes(),
|
input: text_input.into_bytes(),
|
||||||
mode: None,
|
mode,
|
||||||
command_id: String::new(),
|
command_id,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"read_shell_command_output" => Some(api::message::tool_call::Tool::ReadShellCommandOutput(
|
"read_shell_command_output" => {
|
||||||
api::message::tool_call::ReadShellCommandOutput {
|
let command_id = input
|
||||||
command_id: String::new(),
|
.get("command_id")
|
||||||
delay: None,
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let delay = if input
|
||||||
|
.get("wait_until_complete")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
Some(api::message::tool_call::read_shell_command_output::Delay::OnCompletion(()))
|
||||||
|
} else {
|
||||||
|
let seconds = input
|
||||||
|
.get("wait_seconds")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.unwrap_or(2)
|
||||||
|
.min(120);
|
||||||
|
Some(
|
||||||
|
api::message::tool_call::read_shell_command_output::Delay::Duration(
|
||||||
|
prost_types::Duration {
|
||||||
|
seconds: seconds as i64,
|
||||||
|
nanos: 0,
|
||||||
},
|
},
|
||||||
)),
|
),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
Some(api::message::tool_call::Tool::ReadShellCommandOutput(
|
||||||
|
api::message::tool_call::ReadShellCommandOutput { command_id, delay },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"transfer_shell_command_control_to_user" => {
|
||||||
|
let reason = input
|
||||||
|
.get("reason")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
Some(
|
||||||
|
api::message::tool_call::Tool::TransferShellCommandControlToUser(
|
||||||
|
api::message::tool_call::TransferShellCommandControlToUser { reason },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
"read_mcp_resource" => {
|
"read_mcp_resource" => {
|
||||||
let server_id = input
|
let server_id = input
|
||||||
.get("server_id")
|
.get("server_id")
|
||||||
@@ -1294,6 +1370,7 @@ const KNOWN_TOOLS: &[&str] = &[
|
|||||||
"search_codebase",
|
"search_codebase",
|
||||||
"write_to_long_running_shell_command",
|
"write_to_long_running_shell_command",
|
||||||
"read_shell_command_output",
|
"read_shell_command_output",
|
||||||
|
"transfer_shell_command_control_to_user",
|
||||||
"read_mcp_resource",
|
"read_mcp_resource",
|
||||||
"read_plan",
|
"read_plan",
|
||||||
"create_plan",
|
"create_plan",
|
||||||
|
|||||||
@@ -3,6 +3,21 @@ use warp_multi_agent_api::{self as api};
|
|||||||
|
|
||||||
use super::response_translator::*;
|
use super::response_translator::*;
|
||||||
|
|
||||||
|
fn tool_from_event(event: api::ResponseEvent) -> api::message::tool_call::Tool {
|
||||||
|
let Some(api::response_event::Type::ClientActions(actions)) = event.r#type else {
|
||||||
|
panic!("expected client actions");
|
||||||
|
};
|
||||||
|
let Some(api::client_action::Action::AddMessagesToTask(add_messages)) =
|
||||||
|
&actions.actions[0].action
|
||||||
|
else {
|
||||||
|
panic!("expected AddMessagesToTask");
|
||||||
|
};
|
||||||
|
let Some(api::message::Message::ToolCall(tool_call)) = &add_messages.messages[0].message else {
|
||||||
|
panic!("expected tool call message");
|
||||||
|
};
|
||||||
|
tool_call.tool.clone().expect("expected concrete tool")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_stream_init_has_valid_ids() {
|
fn test_build_stream_init_has_valid_ids() {
|
||||||
let event = build_stream_init("req-123", "conv-456");
|
let event = build_stream_init("req-123", "conv-456");
|
||||||
@@ -128,6 +143,76 @@ fn test_build_create_task_has_no_parent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_tool_call_defaults_to_async_and_preserves_controls() {
|
||||||
|
let tool = tool_from_event(build_tool_call_message(
|
||||||
|
"task-1",
|
||||||
|
"tool-1",
|
||||||
|
"run_shell_command",
|
||||||
|
r#"{
|
||||||
|
"command": "cargo test",
|
||||||
|
"is_read_only": true,
|
||||||
|
"is_risky": true,
|
||||||
|
"uses_pager": false
|
||||||
|
}"#,
|
||||||
|
));
|
||||||
|
|
||||||
|
let api::message::tool_call::Tool::RunShellCommand(command) = tool else {
|
||||||
|
panic!("expected run_shell_command");
|
||||||
|
};
|
||||||
|
assert!(command.is_read_only);
|
||||||
|
assert!(command.is_risky);
|
||||||
|
assert!(!command.uses_pager);
|
||||||
|
assert!(matches!(
|
||||||
|
command.wait_until_complete_value,
|
||||||
|
Some(
|
||||||
|
api::message::tool_call::run_shell_command::WaitUntilCompleteValue::WaitUntilComplete(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn long_running_tool_calls_preserve_command_id_and_delay() {
|
||||||
|
let write_tool = tool_from_event(build_tool_call_message(
|
||||||
|
"task-1",
|
||||||
|
"tool-1",
|
||||||
|
"write_to_long_running_shell_command",
|
||||||
|
r#"{"command_id":"block-123","input":"yes","mode":"line"}"#,
|
||||||
|
));
|
||||||
|
let api::message::tool_call::Tool::WriteToLongRunningShellCommand(write) = write_tool else {
|
||||||
|
panic!("expected write_to_long_running_shell_command");
|
||||||
|
};
|
||||||
|
assert_eq!(write.command_id, "block-123");
|
||||||
|
assert!(matches!(
|
||||||
|
write.mode.and_then(|mode| mode.mode),
|
||||||
|
Some(api::message::tool_call::write_to_long_running_shell_command::mode::Mode::Line(()))
|
||||||
|
));
|
||||||
|
|
||||||
|
let read_tool = tool_from_event(build_tool_call_message(
|
||||||
|
"task-1",
|
||||||
|
"tool-2",
|
||||||
|
"read_shell_command_output",
|
||||||
|
r#"{"command_id":"block-123","wait_seconds":7}"#,
|
||||||
|
));
|
||||||
|
let api::message::tool_call::Tool::ReadShellCommandOutput(read) = read_tool else {
|
||||||
|
panic!("expected read_shell_command_output");
|
||||||
|
};
|
||||||
|
assert_eq!(read.command_id, "block-123");
|
||||||
|
assert!(matches!(
|
||||||
|
read.delay,
|
||||||
|
Some(
|
||||||
|
api::message::tool_call::read_shell_command_output::Delay::Duration(
|
||||||
|
prost_types::Duration {
|
||||||
|
seconds: 7,
|
||||||
|
nanos: 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_context_window_for_model_1m_marker() {
|
fn test_context_window_for_model_1m_marker() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -299,6 +299,10 @@ impl RunAgentsExecutor {
|
|||||||
Ok(StartAgentOutcome::Started { agent_id }),
|
Ok(StartAgentOutcome::Started { agent_id }),
|
||||||
_,
|
_,
|
||||||
)) => RunAgentsAgentOutcomeKind::Launched { agent_id },
|
)) => RunAgentsAgentOutcomeKind::Launched { agent_id },
|
||||||
|
futures::future::Either::Left((
|
||||||
|
Ok(StartAgentOutcome::Completed { agent_id, .. }),
|
||||||
|
_,
|
||||||
|
)) => RunAgentsAgentOutcomeKind::Launched { agent_id },
|
||||||
futures::future::Either::Left((
|
futures::future::Either::Left((
|
||||||
Ok(StartAgentOutcome::Error(error)),
|
Ok(StartAgentOutcome::Error(error)),
|
||||||
_,
|
_,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use galaxyui::{Entity, ModelContext, ModelHandle, SingletonEntity};
|
|||||||
use shell_words::split as split_shell_words;
|
use shell_words::split as split_shell_words;
|
||||||
|
|
||||||
use super::{ActionExecution, AnyActionExecution, ExecuteActionInput, PreprocessActionInput};
|
use super::{ActionExecution, AnyActionExecution, ExecuteActionInput, PreprocessActionInput};
|
||||||
use crate::ai::agent::conversation::{AIConversationId, ConversationStatus};
|
use crate::ai::agent::conversation::{AIConversation, AIConversationId, ConversationStatus};
|
||||||
use crate::ai::agent::{
|
use crate::ai::agent::{
|
||||||
AIAgentAction, AIAgentActionResultType, AIAgentActionType, LifecycleEventType,
|
AIAgentAction, AIAgentActionResultType, AIAgentActionType, LifecycleEventType,
|
||||||
StartAgentExecutionMode, StartAgentResult,
|
StartAgentExecutionMode, StartAgentResult,
|
||||||
@@ -22,6 +22,11 @@ pub enum StartAgentOutcome {
|
|||||||
Started {
|
Started {
|
||||||
agent_id: String,
|
agent_id: String,
|
||||||
},
|
},
|
||||||
|
/// A direct-provider child completed and returned its output inline.
|
||||||
|
Completed {
|
||||||
|
agent_id: String,
|
||||||
|
output: String,
|
||||||
|
},
|
||||||
/// An error occurred while starting the agent.
|
/// An error occurred while starting the agent.
|
||||||
Error(String),
|
Error(String),
|
||||||
}
|
}
|
||||||
@@ -114,6 +119,10 @@ struct PendingStartAgent {
|
|||||||
/// Set once the child conversation is synchronously created.
|
/// Set once the child conversation is synchronously created.
|
||||||
child_conversation_id: Option<AIConversationId>,
|
child_conversation_id: Option<AIConversationId>,
|
||||||
sender: async_channel::Sender<StartAgentOutcome>,
|
sender: async_channel::Sender<StartAgentOutcome>,
|
||||||
|
/// Direct Bedrock/OpenAI parents do not have a server run id or an
|
||||||
|
/// orchestration event stream. Keep the tool call open until their local
|
||||||
|
/// child finishes, then return the child's output inline.
|
||||||
|
wait_for_completion: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StartAgentExecutor {
|
pub struct StartAgentExecutor {
|
||||||
@@ -194,6 +203,35 @@ impl StartAgentExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn complete_pending_as_completed(
|
||||||
|
&mut self,
|
||||||
|
request_id: StartAgentRequestId,
|
||||||
|
child_conversation_id: AIConversationId,
|
||||||
|
ctx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
let Some(conversation) =
|
||||||
|
BlocklistAIHistoryModel::as_ref(ctx).conversation(&child_conversation_id)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let agent_id = conversation
|
||||||
|
.orchestration_agent_id()
|
||||||
|
.or_else(|| {
|
||||||
|
conversation
|
||||||
|
.server_conversation_token()
|
||||||
|
.map(|token| token.as_str().to_string())
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| child_conversation_id.to_string());
|
||||||
|
let output = extract_child_output(conversation);
|
||||||
|
|
||||||
|
let Some(pending) = self.pending.remove(&request_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let _ = pending
|
||||||
|
.sender
|
||||||
|
.try_send(StartAgentOutcome::Completed { agent_id, output });
|
||||||
|
}
|
||||||
|
|
||||||
fn complete_pending_as_error(
|
fn complete_pending_as_error(
|
||||||
&mut self,
|
&mut self,
|
||||||
request_id: StartAgentRequestId,
|
request_id: StartAgentRequestId,
|
||||||
@@ -238,6 +276,14 @@ impl StartAgentExecutor {
|
|||||||
self.complete_pending_as_error(request_id, child_conversation_id, error_msg, ctx);
|
self.complete_pending_as_error(request_id, child_conversation_id, error_msg, ctx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let wait_for_completion = self
|
||||||
|
.pending
|
||||||
|
.get(&request_id)
|
||||||
|
.is_some_and(|pending| pending.wait_for_completion);
|
||||||
|
if wait_for_completion && matches!(conversation.status(), ConversationStatus::Success) {
|
||||||
|
self.complete_pending_as_completed(request_id, child_conversation_id, ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if conversation.orchestration_agent_id().is_some() {
|
if conversation.orchestration_agent_id().is_some() {
|
||||||
self.complete_pending_as_started(request_id, child_conversation_id, ctx);
|
self.complete_pending_as_started(request_id, child_conversation_id, ctx);
|
||||||
}
|
}
|
||||||
@@ -256,7 +302,7 @@ impl StartAgentExecutor {
|
|||||||
let Some(request_id) = self.find_pending_by_child(conversation_id) else {
|
let Some(request_id) = self.find_pending_by_child(conversation_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.complete_pending_as_started(request_id, *conversation_id, ctx);
|
self.maybe_complete_pending_for_child_state(request_id, *conversation_id, ctx);
|
||||||
}
|
}
|
||||||
BlocklistAIHistoryEvent::UpdatedConversationStatus {
|
BlocklistAIHistoryEvent::UpdatedConversationStatus {
|
||||||
conversation_id, ..
|
conversation_id, ..
|
||||||
@@ -264,17 +310,7 @@ impl StartAgentExecutor {
|
|||||||
let Some(request_id) = self.find_pending_by_child(conversation_id) else {
|
let Some(request_id) = self.find_pending_by_child(conversation_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let history = BlocklistAIHistoryModel::as_ref(ctx);
|
self.maybe_complete_pending_for_child_state(request_id, *conversation_id, ctx);
|
||||||
let Some(conversation) = history.conversation(conversation_id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let error_msg = start_agent_error_message_for_status(
|
|
||||||
conversation.status(),
|
|
||||||
conversation.status_error_message().as_deref(),
|
|
||||||
);
|
|
||||||
if let Some(error_msg) = error_msg {
|
|
||||||
self.complete_pending_as_error(request_id, *conversation_id, error_msg, ctx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
BlocklistAIHistoryEvent::NewConversationRequestComplete {
|
BlocklistAIHistoryEvent::NewConversationRequestComplete {
|
||||||
request_id,
|
request_id,
|
||||||
@@ -346,36 +382,19 @@ impl StartAgentExecutor {
|
|||||||
harness_type: None,
|
harness_type: None,
|
||||||
model_id,
|
model_id,
|
||||||
} => {
|
} => {
|
||||||
// Oz local children resolve their parent's run id from the
|
// Server-backed parents launch an Oz child and return its run
|
||||||
// parent conversation. This mirrors the third-party-harness
|
// id immediately. Direct Bedrock/OpenAI parents have no run
|
||||||
// and remote-child branches below; the child task row is
|
// id; terminal_pane creates a local hidden child instead and
|
||||||
// created eagerly at dispatch (see
|
// this executor waits for its final output.
|
||||||
// `launch_local_no_harness_child`) using this value as the
|
|
||||||
// `parent_run_id` on `CreateAgentTask`. Bail out if the
|
|
||||||
// parent has no `run_id` yet — the eager-create path has no
|
|
||||||
// late-binding fallback (the pre-change lazy path would have
|
|
||||||
// linked via `Request.metadata.parent_agent_id` later), so
|
|
||||||
// proceeding would mint an orphan child with no server-side
|
|
||||||
// parent linkage.
|
|
||||||
let parent_run_id = BlocklistAIHistoryModel::as_ref(ctx)
|
let parent_run_id = BlocklistAIHistoryModel::as_ref(ctx)
|
||||||
.conversation(&parent_conversation_id)
|
.conversation(&parent_conversation_id)
|
||||||
.and_then(|conversation| conversation.run_id());
|
.and_then(|conversation| conversation.run_id());
|
||||||
let Some(parent_run_id) = parent_run_id else {
|
|
||||||
return ActionExecution::Sync(AIAgentActionResultType::StartAgent(
|
|
||||||
StartAgentResult::Error {
|
|
||||||
error:
|
|
||||||
"Local Oz child agents require the parent run_id to be available."
|
|
||||||
.to_string(),
|
|
||||||
version,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
};
|
|
||||||
(
|
(
|
||||||
StartAgentExecutionMode::Local {
|
StartAgentExecutionMode::Local {
|
||||||
harness_type: None,
|
harness_type: None,
|
||||||
model_id,
|
model_id,
|
||||||
},
|
},
|
||||||
Some(parent_run_id),
|
parent_run_id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
StartAgentExecutionMode::Local {
|
StartAgentExecutionMode::Local {
|
||||||
@@ -485,7 +504,7 @@ impl StartAgentExecutor {
|
|||||||
|
|
||||||
// In local mode (no parent_run_id), block until the child finishes
|
// In local mode (no parent_run_id), block until the child finishes
|
||||||
// so the parent model receives the child's output as the tool result.
|
// so the parent model receives the child's output as the tool result.
|
||||||
let _wait_for_completion = parent_run_id.is_none();
|
let wait_for_completion = parent_run_id.is_none();
|
||||||
|
|
||||||
let (sender, receiver) = async_channel::bounded(1);
|
let (sender, receiver) = async_channel::bounded(1);
|
||||||
let request_id = self.next_request_id();
|
let request_id = self.next_request_id();
|
||||||
@@ -495,6 +514,7 @@ impl StartAgentExecutor {
|
|||||||
parent_conversation_id,
|
parent_conversation_id,
|
||||||
child_conversation_id: None,
|
child_conversation_id: None,
|
||||||
sender,
|
sender,
|
||||||
|
wait_for_completion,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -518,6 +538,12 @@ impl StartAgentExecutor {
|
|||||||
version,
|
version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Ok(StartAgentOutcome::Completed { agent_id, output }) => {
|
||||||
|
AIAgentActionResultType::StartAgent(StartAgentResult::Success {
|
||||||
|
agent_id: format!("{agent_id}\n\nAgent output:\n{output}"),
|
||||||
|
version,
|
||||||
|
})
|
||||||
|
}
|
||||||
Ok(StartAgentOutcome::Error(error)) => {
|
Ok(StartAgentOutcome::Error(error)) => {
|
||||||
AIAgentActionResultType::StartAgent(StartAgentResult::Error { error, version })
|
AIAgentActionResultType::StartAgent(StartAgentResult::Error { error, version })
|
||||||
}
|
}
|
||||||
@@ -551,6 +577,7 @@ impl StartAgentExecutor {
|
|||||||
parent_conversation_id,
|
parent_conversation_id,
|
||||||
child_conversation_id: None,
|
child_conversation_id: None,
|
||||||
sender,
|
sender,
|
||||||
|
wait_for_completion: parent_run_id.is_none(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ctx.emit(StartAgentExecutorEvent::CreateAgent(Box::new(
|
ctx.emit(StartAgentExecutorEvent::CreateAgent(Box::new(
|
||||||
@@ -576,6 +603,22 @@ impl StartAgentExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extracts the text output from every completed exchange in a local child.
|
||||||
|
fn extract_child_output(conversation: &AIConversation) -> String {
|
||||||
|
let output_parts = conversation
|
||||||
|
.all_exchanges()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|exchange| exchange.output_status.output())
|
||||||
|
.map(|output| output.get().format_for_copy(None))
|
||||||
|
.filter(|text| !text.is_empty())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if output_parts.is_empty() {
|
||||||
|
"Agent completed but produced no text output.".to_string()
|
||||||
|
} else {
|
||||||
|
output_parts.join("\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether a child that failed before launch should have its hidden pane and
|
/// Whether a child that failed before launch should have its hidden pane and
|
||||||
/// conversation cleaned up. Only terminal launch failures qualify; recoverable
|
/// conversation cleaned up. Only terminal launch failures qualify; recoverable
|
||||||
/// `Blocked` startup states (e.g. awaiting GitHub auth) and non-terminal
|
/// `Blocked` startup states (e.g. awaiting GitHub auth) and non-terminal
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ use crate::test_util::settings::initialize_history_persistence_for_tests;
|
|||||||
const FIRST_REQUEST_ID: StartAgentRequestId = StartAgentRequestId::from_raw_for_test(0);
|
const FIRST_REQUEST_ID: StartAgentRequestId = StartAgentRequestId::from_raw_for_test(0);
|
||||||
|
|
||||||
/// Stable placeholder run_id assigned to the parent conversation in tests
|
/// Stable placeholder run_id assigned to the parent conversation in tests
|
||||||
/// that dispatch an Oz local child. The Oz `Local` arm of
|
/// that exercise the server-backed Oz child path. Tests without this id
|
||||||
/// `StartAgentExecutor::execute` bails out synchronously if the parent has
|
/// exercise the direct-provider local child path instead.
|
||||||
/// no `run_id`, so every `local_with_defaults` test needs to assign one.
|
|
||||||
const PARENT_RUN_ID: &str = "00000000-0000-0000-0000-000000000001";
|
const PARENT_RUN_ID: &str = "00000000-0000-0000-0000-000000000001";
|
||||||
|
|
||||||
fn build_start_agent_action(
|
fn build_start_agent_action(
|
||||||
@@ -375,6 +374,89 @@ fn execute_resolves_success_when_request_linkage_happens_after_child_already_sta
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn execute_waits_for_direct_provider_child_and_returns_its_output() {
|
||||||
|
App::test((), |mut app| async move {
|
||||||
|
initialize_history_persistence_for_tests(&mut app);
|
||||||
|
let terminal_view_id = EntityId::new();
|
||||||
|
let history_model = app.add_singleton_model(|_| BlocklistAIHistoryModel::new_for_test());
|
||||||
|
let executor = app.add_model(StartAgentExecutor::new);
|
||||||
|
let parent_conversation_id = history_model.update(&mut app, |history_model, ctx| {
|
||||||
|
history_model.start_new_conversation(terminal_view_id, false, false, false, ctx)
|
||||||
|
});
|
||||||
|
let action = build_start_agent_action(
|
||||||
|
StartAgentVersion::V1,
|
||||||
|
StartAgentExecutionMode::local_with_defaults(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let execution = executor.update(&mut app, |executor, ctx| {
|
||||||
|
let input = ExecuteActionInput {
|
||||||
|
action: &action,
|
||||||
|
conversation_id: parent_conversation_id,
|
||||||
|
};
|
||||||
|
let result: AnyActionExecution = executor.execute(input, ctx).into();
|
||||||
|
result
|
||||||
|
});
|
||||||
|
let AnyActionExecution::Async {
|
||||||
|
execute_future,
|
||||||
|
on_complete,
|
||||||
|
} = execution
|
||||||
|
else {
|
||||||
|
panic!("expected async execution");
|
||||||
|
};
|
||||||
|
|
||||||
|
let child_conversation_id = history_model.update(&mut app, |history_model, ctx| {
|
||||||
|
history_model.start_new_child_conversation(
|
||||||
|
terminal_view_id,
|
||||||
|
"Agent 1".to_string(),
|
||||||
|
parent_conversation_id,
|
||||||
|
None,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
history_model.update(&mut app, |model, ctx| {
|
||||||
|
model.record_new_conversation_request_complete(
|
||||||
|
FIRST_REQUEST_ID,
|
||||||
|
child_conversation_id,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
executor.read(&app, |executor, _| {
|
||||||
|
let pending = executor
|
||||||
|
.pending
|
||||||
|
.get(&FIRST_REQUEST_ID)
|
||||||
|
.expect("direct child should remain pending until completion");
|
||||||
|
assert!(pending.wait_for_completion);
|
||||||
|
});
|
||||||
|
|
||||||
|
history_model.update(&mut app, |history_model, ctx| {
|
||||||
|
history_model.update_conversation_status(
|
||||||
|
terminal_view_id,
|
||||||
|
child_conversation_id,
|
||||||
|
ConversationStatus::Success,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let async_result = execute_future.await;
|
||||||
|
let result = app.update(|ctx| on_complete(async_result, ctx));
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
AIAgentActionResultType::StartAgent(StartAgentResult::Success {
|
||||||
|
agent_id,
|
||||||
|
version,
|
||||||
|
}) if agent_id.contains(&child_conversation_id.to_string())
|
||||||
|
&& agent_id.contains("Agent output:")
|
||||||
|
&& agent_id.contains("Agent completed but produced no text output.")
|
||||||
|
&& version == StartAgentVersion::V1
|
||||||
|
));
|
||||||
|
executor.read(&app, |executor, _| {
|
||||||
|
assert!(executor.pending.is_empty());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn execute_returns_detailed_error_when_child_startup_fails_before_initialization() {
|
fn execute_returns_detailed_error_when_child_startup_fails_before_initialization() {
|
||||||
App::test((), |mut app| async move {
|
App::test((), |mut app| async move {
|
||||||
|
|||||||
@@ -1514,9 +1514,7 @@ impl View for RequestedCommandView {
|
|||||||
let has_citations_footer =
|
let has_citations_footer =
|
||||||
!self.derived_from_citations.is_empty() && !self.block_model.status(app).is_streaming();
|
!self.derived_from_citations.is_empty() && !self.block_model.status(app).is_streaming();
|
||||||
let header_element = self.render_header(
|
let header_element = self.render_header(
|
||||||
!should_render_editor
|
!should_render_editor && !should_render_mcp_content && !has_citations_footer,
|
||||||
&& !should_render_mcp_content
|
|
||||||
&& !has_citations_footer,
|
|
||||||
app,
|
app,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1640,13 +1638,11 @@ impl View for RequestedCommandView {
|
|||||||
&& !is_input_pinned_to_top);
|
&& !is_input_pinned_to_top);
|
||||||
|
|
||||||
let container = Container::new(content.finish())
|
let container = Container::new(content.finish())
|
||||||
.with_margin_left(
|
.with_margin_left(if action_status.is_some_and(|status| status.is_blocked()) {
|
||||||
if action_status.is_some_and(|status| status.is_blocked()) {
|
|
||||||
CONTENT_HORIZONTAL_PADDING
|
CONTENT_HORIZONTAL_PADDING
|
||||||
} else {
|
} else {
|
||||||
CONTENT_HORIZONTAL_PADDING + icon_size(app) + 16.
|
CONTENT_HORIZONTAL_PADDING + icon_size(app) + 16.
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.with_margin_right(CONTENT_HORIZONTAL_PADDING)
|
.with_margin_right(CONTENT_HORIZONTAL_PADDING)
|
||||||
.with_margin_bottom(if should_remove_bottom_margin {
|
.with_margin_bottom(if should_remove_bottom_margin {
|
||||||
0.
|
0.
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ impl ConnectedSelfHostedWorkersModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh(&mut self, ctx: &mut ModelContext<Self>) {
|
pub fn refresh(&mut self, ctx: &mut ModelContext<Self>) {
|
||||||
if !AuthStateProvider::as_ref(ctx).get().is_logged_in() {
|
if !AuthStateProvider::as_ref(ctx)
|
||||||
|
.get()
|
||||||
|
.has_server_credentials()
|
||||||
|
{
|
||||||
self.clear_workers(ctx);
|
self.clear_workers(ctx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,10 +260,7 @@ impl CrosscheckReviewer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Use a non-streaming request by calling the same endpoint but with stream: false
|
// Use a non-streaming request by calling the same endpoint but with stream: false
|
||||||
let url = format!(
|
let url = format!("{}/chat/completions", config.base_url.trim_end_matches('/'));
|
||||||
"{}/chat/completions",
|
|
||||||
config.base_url.trim_end_matches('/')
|
|
||||||
);
|
|
||||||
|
|
||||||
let http = reqwest::Client::new();
|
let http = reqwest::Client::new();
|
||||||
let mut request_builder = http.post(&url).json(&request_body);
|
let mut request_builder = http.post(&url).json(&request_body);
|
||||||
|
|||||||
@@ -199,7 +199,10 @@ impl HarnessAvailabilityModel {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if !AuthStateProvider::as_ref(ctx).get().is_logged_in() {
|
if !AuthStateProvider::as_ref(ctx)
|
||||||
|
.get()
|
||||||
|
.has_server_credentials()
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +343,10 @@ impl HarnessAvailabilityModel {
|
|||||||
|
|
||||||
pub fn refresh(&self, ctx: &mut ModelContext<Self>) {
|
pub fn refresh(&self, ctx: &mut ModelContext<Self>) {
|
||||||
// The endpoint queries `user`, which requires auth.
|
// The endpoint queries `user`, which requires auth.
|
||||||
if !AuthStateProvider::as_ref(ctx).get().is_logged_in() {
|
if !AuthStateProvider::as_ref(ctx)
|
||||||
|
.get()
|
||||||
|
.has_server_credentials()
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,6 @@ pub mod bedrock;
|
|||||||
pub(crate) mod bedrock_credentials;
|
pub(crate) mod bedrock_credentials;
|
||||||
pub(crate) mod block_context;
|
pub(crate) mod block_context;
|
||||||
pub(crate) mod blocklist;
|
pub(crate) mod blocklist;
|
||||||
pub(crate) mod crosscheck;
|
|
||||||
#[cfg(any(feature = "local_fs", not(target_family = "wasm")))]
|
#[cfg(any(feature = "local_fs", not(target_family = "wasm")))]
|
||||||
pub(crate) mod codebase_auto_indexing;
|
pub(crate) mod codebase_auto_indexing;
|
||||||
pub mod control_code_parser;
|
pub mod control_code_parser;
|
||||||
@@ -31,6 +30,7 @@ pub(crate) mod conversation_navigation;
|
|||||||
pub(crate) mod conversation_rename;
|
pub(crate) mod conversation_rename;
|
||||||
pub(crate) mod conversation_status_ui;
|
pub(crate) mod conversation_status_ui;
|
||||||
pub(crate) mod conversation_utils;
|
pub(crate) mod conversation_utils;
|
||||||
|
pub(crate) mod crosscheck;
|
||||||
pub(crate) mod custom_model_router_editor;
|
pub(crate) mod custom_model_router_editor;
|
||||||
pub(crate) mod custom_model_routers;
|
pub(crate) mod custom_model_routers;
|
||||||
pub(crate) mod document;
|
pub(crate) mod document;
|
||||||
|
|||||||
@@ -572,6 +572,7 @@ const KNOWN_TOOLS: &[&str] = &[
|
|||||||
"search_codebase",
|
"search_codebase",
|
||||||
"write_to_long_running_shell_command",
|
"write_to_long_running_shell_command",
|
||||||
"read_shell_command_output",
|
"read_shell_command_output",
|
||||||
|
"transfer_shell_command_control_to_user",
|
||||||
"read_mcp_resource",
|
"read_mcp_resource",
|
||||||
"read_plan",
|
"read_plan",
|
||||||
"create_plan",
|
"create_plan",
|
||||||
|
|||||||
@@ -247,7 +247,10 @@ impl AIRequestUsageModel {
|
|||||||
|
|
||||||
/// Spawns a task to refresh the latest AI request usage and bonus grants, fetching from the server.
|
/// Spawns a task to refresh the latest AI request usage and bonus grants, fetching from the server.
|
||||||
pub fn refresh_request_usage_async(&mut self, ctx: &mut ModelContext<Self>) {
|
pub fn refresh_request_usage_async(&mut self, ctx: &mut ModelContext<Self>) {
|
||||||
if !AuthStateProvider::as_ref(ctx).get().is_logged_in() {
|
if !AuthStateProvider::as_ref(ctx)
|
||||||
|
.get()
|
||||||
|
.has_server_credentials()
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,9 @@ impl Requests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if cached_request_limit_info.is_none()
|
if cached_request_limit_info.is_none()
|
||||||
&& AuthStateProvider::as_ref(ctx).get().is_logged_in()
|
&& AuthStateProvider::as_ref(ctx)
|
||||||
|
.get()
|
||||||
|
.has_server_credentials()
|
||||||
{
|
{
|
||||||
let ai_client = requests.ai_client.clone();
|
let ai_client = requests.ai_client.clone();
|
||||||
let _ = ctx.spawn(
|
let _ = ctx.spawn(
|
||||||
|
|||||||
@@ -7483,7 +7483,10 @@ impl EditorView {
|
|||||||
|
|
||||||
fn focused_in_active_window(&self, ctx: &AppContext) -> bool {
|
fn focused_in_active_window(&self, ctx: &AppContext) -> bool {
|
||||||
let manager = self.windowing_state_handle.as_ref(ctx);
|
let manager = self.windowing_state_handle.as_ref(ctx);
|
||||||
let active = manager.state().active_window.or_else(|| manager.active_window());
|
let active = manager
|
||||||
|
.state()
|
||||||
|
.active_window
|
||||||
|
.or_else(|| manager.active_window());
|
||||||
Some(self.window_id) == active && self.focused
|
Some(self.window_id) == active && self.focused
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -336,7 +336,6 @@ pub trait Experiment<T: Experiment<T>>: FromStr {
|
|||||||
// the work above and do any one-time accounting.
|
// the work above and do any one-time accounting.
|
||||||
if let Some(group) = assigned_group.as_ref() {
|
if let Some(group) = assigned_group.as_ref() {
|
||||||
GROUP_ASSIGNMENTS.insert(Self::name(), group.variant());
|
GROUP_ASSIGNMENTS.insert(Self::name(), group.variant());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assigned_group
|
assigned_group
|
||||||
|
|||||||
@@ -8066,8 +8066,7 @@ impl View for PaneGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Render agent-assisted environment modal at tab level when open.
|
// Render agent-assisted environment modal at tab level when open.
|
||||||
if let Some(_pane_id) = self.pane_with_open_agent_assisted_environment_modal {
|
if let Some(_pane_id) = self.pane_with_open_agent_assisted_environment_modal {}
|
||||||
}
|
|
||||||
|
|
||||||
stack.finish()
|
stack.finish()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,9 +214,7 @@ impl PaneId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a [`PaneId`] from a [`ViewContext<PaneView<SettingsView>>`] (environment management stub)
|
/// Creates a [`PaneId`] from a [`ViewContext<PaneView<SettingsView>>`] (environment management stub)
|
||||||
pub fn from_environment_management_pane_ctx(
|
pub fn from_environment_management_pane_ctx(ctx: &ViewContext<PaneView<SettingsView>>) -> Self {
|
||||||
ctx: &ViewContext<PaneView<SettingsView>>,
|
|
||||||
) -> Self {
|
|
||||||
Self::new_from_ctx(IPaneType::EnvironmentManagement, ctx)
|
Self::new_from_ctx(IPaneType::EnvironmentManagement, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1647,6 +1647,11 @@ fn launch_local_no_harness_child(
|
|||||||
model_id: Option<String>,
|
model_id: Option<String>,
|
||||||
ctx: &mut ViewContext<PaneGroup>,
|
ctx: &mut ViewContext<PaneGroup>,
|
||||||
) {
|
) {
|
||||||
|
if request.parent_run_id.is_none() {
|
||||||
|
launch_direct_provider_child(group, parent_pane_id, request, model_id, ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let ai_client = ServerApiProvider::handle(ctx).as_ref(ctx).get_ai_client();
|
let ai_client = ServerApiProvider::handle(ctx).as_ref(ctx).get_ai_client();
|
||||||
let request_id = request.id;
|
let request_id = request.id;
|
||||||
let agent_name = normalize_orchestrator_agent_name(&request.name);
|
let agent_name = normalize_orchestrator_agent_name(&request.name);
|
||||||
@@ -1776,6 +1781,78 @@ fn launch_local_no_harness_child(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Launches a child conversation entirely inside Galaxy for direct Bedrock
|
||||||
|
/// and OpenAI-compatible providers. These parents have no Warp server run id,
|
||||||
|
/// so creating an Oz task would either fail or produce an orphan. The
|
||||||
|
/// StartAgentExecutor links this hidden conversation to the pending tool call
|
||||||
|
/// and returns its output after the child reaches Success.
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
fn launch_direct_provider_child(
|
||||||
|
group: &mut PaneGroup,
|
||||||
|
parent_pane_id: PaneId,
|
||||||
|
request: StartAgentRequest,
|
||||||
|
model_id: Option<String>,
|
||||||
|
ctx: &mut ViewContext<PaneGroup>,
|
||||||
|
) {
|
||||||
|
let request_id = request.id;
|
||||||
|
let request_name = normalize_orchestrator_agent_name(&request.name).unwrap_or_default();
|
||||||
|
let parent_conversation_id = request.parent_conversation_id;
|
||||||
|
let prompt = request.prompt;
|
||||||
|
|
||||||
|
let Some(HiddenChildAgentConversation {
|
||||||
|
terminal_view,
|
||||||
|
terminal_view_id,
|
||||||
|
conversation_id,
|
||||||
|
}) = create_hidden_child_agent_conversation(
|
||||||
|
group,
|
||||||
|
HiddenChildAgentConversationRequest {
|
||||||
|
parent_pane_id,
|
||||||
|
name: request_name.clone(),
|
||||||
|
parent_conversation_id,
|
||||||
|
orchestration_harness: None,
|
||||||
|
env_vars: HashMap::new(),
|
||||||
|
task_context: None,
|
||||||
|
is_shared_session_creator: IsSharedSessionCreator::No,
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
else {
|
||||||
|
let _ = create_error_child_agent_conversation(
|
||||||
|
group,
|
||||||
|
ErrorChildAgentConversationRequest {
|
||||||
|
parent_pane_id,
|
||||||
|
name: request_name,
|
||||||
|
parent_conversation_id,
|
||||||
|
request_id: Some(request_id),
|
||||||
|
orchestration_harness: None,
|
||||||
|
error_message: "Failed to create a hidden pane for the local child agent."
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
apply_child_model_id_override(terminal_view_id, model_id.as_deref(), ctx);
|
||||||
|
BlocklistAIHistoryModel::handle(ctx).update(ctx, |model, ctx| {
|
||||||
|
model.record_new_conversation_request_complete(request_id, conversation_id, ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
terminal_view.update(ctx, |terminal_view, ctx| {
|
||||||
|
terminal_view
|
||||||
|
.ai_controller()
|
||||||
|
.update(ctx, |controller, ctx| {
|
||||||
|
controller.send_agent_query_in_conversation(prompt, conversation_id, ctx);
|
||||||
|
});
|
||||||
|
terminal_view.enter_agent_view(
|
||||||
|
None,
|
||||||
|
Some(conversation_id),
|
||||||
|
AgentViewEntryOrigin::ChildAgent,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Asynchronously prepares a local harness launch, then creates the
|
/// Asynchronously prepares a local harness launch, then creates the
|
||||||
/// hidden child pane and executes the launch command.
|
/// hidden child pane and executes the launch command.
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
|||||||
@@ -71,7 +71,10 @@ impl ReferralThemeStatus {
|
|||||||
referrals_client: Arc<dyn ReferralsClient>,
|
referrals_client: Arc<dyn ReferralsClient>,
|
||||||
ctx: &mut ModelContext<Self>,
|
ctx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
if !AuthStateProvider::as_ref(ctx).get().is_logged_in() {
|
if !AuthStateProvider::as_ref(ctx)
|
||||||
|
.get()
|
||||||
|
.has_server_credentials()
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1647,16 +1647,14 @@ pub fn init_actions_from_parent_view<T: Action + Clone>(
|
|||||||
context: &ContextPredicate,
|
context: &ContextPredicate,
|
||||||
builder: fn(SettingsAction) -> T,
|
builder: fn(SettingsAction) -> T,
|
||||||
) {
|
) {
|
||||||
let mut toggle_binding_pairs = vec![
|
let mut toggle_binding_pairs = vec![ToggleSettingActionPair::new(
|
||||||
ToggleSettingActionPair::new(
|
|
||||||
"app analytics",
|
"app analytics",
|
||||||
builder(SettingsAction::PrivacyPageToggle(
|
builder(SettingsAction::PrivacyPageToggle(
|
||||||
PrivacyPageAction::ToggleTelemetry,
|
PrivacyPageAction::ToggleTelemetry,
|
||||||
)),
|
)),
|
||||||
context,
|
context,
|
||||||
flags::TELEMETRY_FLAG,
|
flags::TELEMETRY_FLAG,
|
||||||
),
|
)];
|
||||||
];
|
|
||||||
|
|
||||||
toggle_binding_pairs.push(ToggleSettingActionPair::new(
|
toggle_binding_pairs.push(ToggleSettingActionPair::new(
|
||||||
"secret redaction",
|
"secret redaction",
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ use galaxy_graphql::billing::AddonCreditsOption;
|
|||||||
use galaxy_graphql::error::BudgetExceededError;
|
use galaxy_graphql::error::BudgetExceededError;
|
||||||
use galaxyui::elements::{
|
use galaxyui::elements::{
|
||||||
Align, Border, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius,
|
Align, Border, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius,
|
||||||
CrossAxisAlignment, DropShadow, Empty, Expanded, Flex, FormattedTextElement, HighlightedHyperlink,
|
CrossAxisAlignment, DropShadow, Empty, Expanded, Flex, FormattedTextElement,
|
||||||
Hoverable, Icon as WarpUiIcon, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
HighlightedHyperlink, Hoverable, Icon as WarpUiIcon, MainAxisAlignment, MainAxisSize,
|
||||||
OffsetPositioning, ParentAnchor, ParentElement as _, ParentOffsetBounds, Radius, Shrinkable,
|
MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement as _, ParentOffsetBounds,
|
||||||
SizeConstraintCondition, SizeConstraintSwitch, Stack, Text,
|
Radius, Shrinkable, SizeConstraintCondition, SizeConstraintSwitch, Stack, Text,
|
||||||
};
|
};
|
||||||
use galaxyui::fonts::Weight;
|
use galaxyui::fonts::Weight;
|
||||||
use galaxyui::ui_components::button::ButtonVariant;
|
use galaxyui::ui_components::button::ButtonVariant;
|
||||||
@@ -339,7 +339,8 @@ impl BuyCreditsBanner {
|
|||||||
};
|
};
|
||||||
if discount_percent > 0 {
|
if discount_percent > 0 {
|
||||||
MenuItemFields::new_with_custom_label(
|
MenuItemFields::new_with_custom_label(
|
||||||
Arc::new(enclose!((primary_text) move |is_selected, is_hovered, appearance, _| {
|
Arc::new(
|
||||||
|
enclose!((primary_text) move |is_selected, is_hovered, appearance, _| {
|
||||||
let text_color = appearance.theme().main_text_color(
|
let text_color = appearance.theme().main_text_color(
|
||||||
if is_selected || is_hovered {
|
if is_selected || is_hovered {
|
||||||
appearance.theme().accent()
|
appearance.theme().accent()
|
||||||
@@ -364,8 +365,9 @@ impl BuyCreditsBanner {
|
|||||||
.with_child(main_text)
|
.with_child(main_text)
|
||||||
.with_child(discount_badge)
|
.with_child(discount_badge)
|
||||||
.finish()
|
.finish()
|
||||||
})),
|
}),
|
||||||
Some(primary_text)
|
),
|
||||||
|
Some(primary_text),
|
||||||
)
|
)
|
||||||
.with_on_select_action(DropdownAction::select_action_and_close(
|
.with_on_select_action(DropdownAction::select_action_and_close(
|
||||||
Action::SelectDenomination(index),
|
Action::SelectDenomination(index),
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ impl EnableAutoReloadModalBody {
|
|||||||
};
|
};
|
||||||
if discount_percent > 0 {
|
if discount_percent > 0 {
|
||||||
MenuItemFields::new_with_custom_label(
|
MenuItemFields::new_with_custom_label(
|
||||||
Arc::new(enclose!((primary_text) move |is_selected, is_hovered, appearance, _| {
|
Arc::new(
|
||||||
|
enclose!((primary_text) move |is_selected, is_hovered, appearance, _| {
|
||||||
let text_color = appearance.theme().main_text_color(
|
let text_color = appearance.theme().main_text_color(
|
||||||
if is_selected || is_hovered {
|
if is_selected || is_hovered {
|
||||||
appearance.theme().accent()
|
appearance.theme().accent()
|
||||||
@@ -190,7 +191,8 @@ impl EnableAutoReloadModalBody {
|
|||||||
.with_child(main_text)
|
.with_child(main_text)
|
||||||
.with_child(discount_badge)
|
.with_child(discount_badge)
|
||||||
.finish()
|
.finish()
|
||||||
})),
|
}),
|
||||||
|
),
|
||||||
Some(primary_text),
|
Some(primary_text),
|
||||||
)
|
)
|
||||||
.with_on_select_action(DropdownAction::select_action_and_close(
|
.with_on_select_action(DropdownAction::select_action_and_close(
|
||||||
|
|||||||
@@ -1217,9 +1217,7 @@ impl Input {
|
|||||||
"ToolResult(id={tool_use_id}, err={is_error}): {truncated}"
|
"ToolResult(id={tool_use_id}, err={is_error}): {truncated}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
crate::ai::bedrock::convert::MessageContent::MultiPart(
|
crate::ai::bedrock::convert::MessageContent::MultiPart(parts) => {
|
||||||
parts,
|
|
||||||
) => {
|
|
||||||
format!("MultiPart({} parts)", parts.len())
|
format!("MultiPart({} parts)", parts.len())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1270,16 +1268,13 @@ impl Input {
|
|||||||
}
|
}
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
|
|
||||||
output.push_str(&format!(
|
output.push_str(&format!("=== MESSAGE HISTORY ({msg_count} messages) ===\n"));
|
||||||
"=== MESSAGE HISTORY ({msg_count} messages) ===\n"
|
|
||||||
));
|
|
||||||
for line in &messages {
|
for line in &messages {
|
||||||
output.push_str(line);
|
output.push_str(line);
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.clipboard()
|
ctx.clipboard().write(ClipboardContent::plain_text(output));
|
||||||
.write(ClipboardContent::plain_text(output));
|
|
||||||
|
|
||||||
let window_id = ctx.window_id();
|
let window_id = ctx.window_id();
|
||||||
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
||||||
|
|||||||
@@ -219,8 +219,7 @@ impl PtySpawner {
|
|||||||
>,
|
>,
|
||||||
is_crash_reporting_enabled: bool,
|
is_crash_reporting_enabled: bool,
|
||||||
) -> Result<(PtySpawnResult, Box<dyn PtyHandle>)> {
|
) -> Result<(PtySpawnResult, Box<dyn PtyHandle>)> {
|
||||||
let pty_spawn_info =
|
let pty_spawn_info = invoke_without_crash_reporting(move || {
|
||||||
invoke_without_crash_reporting(move || {
|
|
||||||
local_tty::spawn(
|
local_tty::spawn(
|
||||||
options,
|
options,
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|||||||
@@ -318,4 +318,3 @@ pub fn init_logging() {
|
|||||||
.try_init();
|
.try_init();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ impl ChannelState {
|
|||||||
CHANNEL_STATE.lock().config.telemetry_config.is_some()
|
CHANNEL_STATE.lock().config.telemetry_config.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn releases_base_url() -> Cow<'static, str> {
|
pub fn releases_base_url() -> Cow<'static, str> {
|
||||||
CHANNEL_STATE
|
CHANNEL_STATE
|
||||||
.lock()
|
.lock()
|
||||||
|
|||||||
@@ -108,8 +108,7 @@ pub trait ErrorExt: RegisteredError + std::error::Error {
|
|||||||
/// engineering team.
|
/// engineering team.
|
||||||
fn is_actionable(&self) -> bool;
|
fn is_actionable(&self) -> bool;
|
||||||
|
|
||||||
fn report_error(&self) {
|
fn report_error(&self) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -24,6 +24,5 @@ impl AnyhowErrorExt for anyhow::Error {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn report_error(&self) {
|
fn report_error(&self) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -554,10 +554,7 @@ pub fn test_restore_snapshot_with_settings_page() -> Builder {
|
|||||||
|
|
||||||
let settings_view = settings_views.first().expect("Settings view must exist");
|
let settings_view = settings_views.first().expect("Settings view must exist");
|
||||||
settings_view.read(app, |view, _| {
|
settings_view.read(app, |view, _| {
|
||||||
async_assert_eq!(
|
async_assert_eq!(view.current_settings_section(), SettingsSection::About)
|
||||||
view.current_settings_section(),
|
|
||||||
SettingsSection::About
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -79,7 +79,14 @@ impl TypeScriptLanguageServerCandidate {
|
|||||||
if let Some(path_env) = path_env_var {
|
if let Some(path_env) = path_env_var {
|
||||||
let mut cmd = command::r#async::Command::new("npx");
|
let mut cmd = command::r#async::Command::new("npx");
|
||||||
cmd.env("PATH", path_env);
|
cmd.env("PATH", path_env);
|
||||||
cmd.args(["--yes", "--package", "typescript", "node", "-e", "console.log(require('typescript').sys.getExecutingFilePath())"]);
|
cmd.args([
|
||||||
|
"--yes",
|
||||||
|
"--package",
|
||||||
|
"typescript",
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"console.log(require('typescript').sys.getExecutingFilePath())",
|
||||||
|
]);
|
||||||
// Set cwd to workspace so npx resolves in the right context
|
// Set cwd to workspace so npx resolves in the right context
|
||||||
cmd.current_dir(workspace_root);
|
cmd.current_dir(workspace_root);
|
||||||
if let Ok(output) = cmd.output().await {
|
if let Ok(output) = cmd.output().await {
|
||||||
|
|||||||
@@ -268,6 +268,15 @@ impl AuthState {
|
|||||||
self.credentials.read().clone()
|
self.credentials.read().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether requests to authenticated server endpoints can be made.
|
||||||
|
///
|
||||||
|
/// Galaxy's local-first experience treats the user as logged in even when no
|
||||||
|
/// Warp server credentials are configured, so `is_logged_in` is not sufficient
|
||||||
|
/// for guarding authenticated network requests.
|
||||||
|
pub fn has_server_credentials(&self) -> bool {
|
||||||
|
self.credentials.read().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the credentials. Should only be called within the auth module.
|
/// Sets the credentials. Should only be called within the auth module.
|
||||||
pub fn set_credentials(&self, credentials: Option<Credentials>) {
|
pub fn set_credentials(&self, credentials: Option<Credentials>) {
|
||||||
*self.credentials.write() = credentials;
|
*self.credentials.write() = credentials;
|
||||||
|
|||||||
Reference in New Issue
Block a user