Improve agent prompting and classifier latency; clean up build warnings

This commit is contained in:
2026-09-15 14:25:00 -05:00
parent 66b73b1aac
commit 76d492aa75
69 changed files with 368 additions and 205 deletions
@@ -75,6 +75,7 @@ pub fn convert_conversation_data_to_ai_conversation(
agent_backend: Default::default(),
active_provider_run_json: None,
local_agent_groups: Vec::new(),
tool_titles: Default::default(),
server_conversation_token: None,
conversation_usage_metadata: usage_metadata,
reverted_action_ids: None,
@@ -100,6 +101,7 @@ pub fn convert_conversation_data_to_ai_conversation(
agent_backend: Default::default(),
active_provider_run_json: None,
local_agent_groups: Vec::new(),
tool_titles: Default::default(),
server_conversation_token: Some(
metadata.server_conversation_token.as_str().to_string(),
),
+16
View File
@@ -234,6 +234,7 @@ pub struct AIConversation {
/// Opaque, versioned snapshot of the active direct-provider run.
active_provider_run_json: Option<String>,
local_agent_groups: Vec<LocalAgentGroup>,
tool_titles: HashMap<String, String>,
/// The server-generated unique "token" for this conversation.
///
@@ -389,6 +390,7 @@ impl AIConversation {
agent_backend,
active_provider_run_json: None,
local_agent_groups: Vec::new(),
tool_titles: HashMap::new(),
server_conversation_token: None,
task_id: None,
forked_from_server_conversation_token: None,
@@ -547,6 +549,10 @@ impl AIConversation {
(task_store, todo_lists, status)
};
let tool_titles = conversation_data
.as_ref()
.map(|data| data.tool_titles.clone())
.unwrap_or_default();
let local_agent_groups = conversation_data
.as_ref()
.map(|data| data.local_agent_groups.clone())
@@ -682,6 +688,7 @@ impl AIConversation {
agent_backend,
active_provider_run_json,
local_agent_groups,
tool_titles,
server_conversation_token,
task_id: run_id.as_deref().and_then(|id| id.parse().ok()),
forked_from_server_conversation_token,
@@ -725,6 +732,14 @@ impl AIConversation {
&self.agent_backend
}
pub(crate) fn tool_titles(&self) -> &HashMap<String, String> {
&self.tool_titles
}
pub(crate) fn set_tool_title(&mut self, action_id: &AIAgentActionId, title: String) {
self.tool_titles.insert(action_id.to_string(), title);
}
pub(crate) fn local_agent_groups(&self) -> &[LocalAgentGroup] {
&self.local_agent_groups
}
@@ -4050,6 +4065,7 @@ impl AIConversation {
agent_backend: self.agent_backend.clone(),
active_provider_run_json: self.active_provider_run_json.clone(),
local_agent_groups: self.local_agent_groups.clone(),
tool_titles: self.tool_titles.clone(),
server_conversation_token: self
.server_conversation_token
.clone()
+1 -1
View File
@@ -80,7 +80,7 @@ pub fn compute_active_task_ids<'a>(
/// missing task are assigned depth 0.
pub fn compute_task_depths(tasks: &HashMap<String, api::Task>) -> HashMap<&str, usize> {
let mut depths = HashMap::new();
for (task_id, _) in tasks.iter() {
for task_id in tasks.keys() {
let mut depth = 0;
let mut current_id: &str = task_id;
let mut visited = HashSet::new();
+1 -4
View File
@@ -1211,10 +1211,7 @@ impl<'a> std::fmt::Display for MarkdownActionResult<'a> {
match self.0 {
AIAgentActionResultType::RequestCommandOutput(result) => match result {
RequestCommandOutputResult::Completed {
command,
output,
exit_code: _,
..
command, output, ..
} => {
write!(
f,
@@ -504,7 +504,7 @@ impl SecretRedactionState {
// the case where a secret is split across streaming chunks.
let combined_text = format!(
"{}{}",
&self.last_word_to_rescan_for_redaction,
self.last_word_to_rescan_for_redaction,
&text[self.last_scanned_secret_redaction_byte_index..]
);
let secret_ranges_with_levels =
+2 -8
View File
@@ -729,14 +729,8 @@ impl BlocklistAIContextModel {
) -> Option<&'a AIAgentTodoList> {
self.selected_conversation(ctx)
.and_then(|c| c.active_todo_list())
.and_then(|todo_list| {
// Don't show todo list if it's empty or finished
if todo_list.is_empty() || todo_list.is_finished() {
None
} else {
Some(todo_list)
}
})
// Don't show todo list if it's empty or finished
.filter(|&todo_list| !(todo_list.is_empty() || todo_list.is_finished()))
}
pub fn pending_query_autoexecute_override(
+18 -1
View File
@@ -122,7 +122,8 @@ const PROGRESSIVE_SUMMARY_PROMPT: &str = "Create a concise, factual task-state c
- Decisions made and the rationale needed to continue\n\
- Files changed and the material result of each change\n\
- Significant tool outcomes, errors, identifiers, and unresolved blockers\n\
- Current progress and the next concrete steps\n\n\
- Current progress and the next concrete steps; distinguish completed work from intentions\n\
- Delegated scope, concrete findings with file paths or tool-result IDs, and the exact remaining uncertainty\n\n\
Omit greetings, agreement, praise, reassurance, filler, routine status narration, repeated content, and stylistic assistant phrasing. Summarize completed tool work by outcome instead of listing every call. Do not quote prior assistant wording unless its exact text is task-critical. Return only the checkpoint, structured for another agent to resume the work.";
#[derive(Clone)]
@@ -7634,6 +7635,15 @@ impl BlocklistAIController {
else {
return;
};
let tool_titles = batch
.calls
.iter()
.filter_map(|pending| {
run.action_context
.tool_call_title(&pending.call)
.map(|title| (pending.call.id.clone(), title))
})
.collect::<HashMap<_, _>>();
let (mut converted_actions, invalid_results) =
convert_provider_tool_batch(&run.action_context, &batch);
for result in &invalid_results {
@@ -7742,6 +7752,13 @@ impl BlocklistAIController {
} else {
let apply_result =
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, ctx| {
if let Some(title) = tool_titles.get(&action.id.to_string()) {
if let Some(conversation) =
history_model.conversation_mut(&conversation_id)
{
conversation.set_tool_title(&action.id, title.clone());
}
}
history_model.apply_domain_tool_proposal(
&stream_id,
conversation_id,
+3
View File
@@ -1705,6 +1705,7 @@ impl BlocklistAIHistoryModel {
agent_backend: source_conversation.agent_backend().for_fork(),
active_provider_run_json: None,
local_agent_groups: Vec::new(),
tool_titles: source_conversation.tool_titles().clone(),
server_conversation_token: None,
conversation_usage_metadata: Some(source_conversation.usage_metadata()),
reverted_action_ids,
@@ -1871,6 +1872,7 @@ impl BlocklistAIHistoryModel {
agent_backend: conversation.agent_backend().for_fork(),
active_provider_run_json: None,
local_agent_groups: Vec::new(),
tool_titles: conversation.tool_titles().clone(),
server_conversation_token: None,
conversation_usage_metadata: None,
reverted_action_ids,
@@ -2882,6 +2884,7 @@ fn merged_remote_child_placeholder_conversation_data(
// Active process-local provider runs cannot be merged from a cloud transcript.
active_provider_run_json: None,
local_agent_groups: Vec::new(),
tool_titles: Default::default(),
// Cloud authoritative.
server_conversation_token: cloud_conversation
@@ -2118,15 +2118,15 @@ impl CodeDiffView {
}
fn position_id_for_file(&self, i: usize) -> String {
format!("CodeDiffView-{}-tab-{i}", &self.position_id_prefix)
format!("CodeDiffView-{}-tab-{i}", self.position_id_prefix)
}
fn position_id_for_inline_editor(&self) -> String {
format!("CodeDiffView-inline-editor-{}", &self.position_id_prefix)
format!("CodeDiffView-inline-editor-{}", self.position_id_prefix)
}
fn position_id_for_inline_speedbump(&self) -> String {
format!("CodeDiffView-inline-speedbump-{}", &self.position_id_prefix)
format!("CodeDiffView-inline-speedbump-{}", self.position_id_prefix)
}
fn position_id_for_accept_split_button(&self) -> String {
@@ -139,7 +139,7 @@ impl SuggestedUnitTestsView {
fn position_id_for_speedbump(&self) -> String {
format!(
"SuggestedUnitTestsView-speedbump-{}",
&self.position_id_prefix
self.position_id_prefix
)
}
+1 -1
View File
@@ -799,7 +799,7 @@ impl BlocklistAIInputModel {
if use_local_model {
// A cancelled debounce never starts inference. The local worker
// also cancels obsolete requests already queued or generating.
warpui::r#async::Timer::after(std::time::Duration::from_millis(300)).await;
warpui::r#async::Timer::after(std::time::Duration::from_millis(100)).await;
let result = crate::local_inference::classify(
buffer_cloned.to_string(),
history_entries.as_deref().unwrap_or_default(),
@@ -365,8 +365,7 @@ impl PassiveSuggestionsModel {
#[cfg(feature = "local_fs")]
if is_passive_code_diffs_enabled {
if let Some(current_working_directory) = block_completed.serialized_block.pwd.clone() {
let block_contents =
format!("{}\n{}", &block_context.command, &block_context.output);
let block_contents = format!("{}\n{}", block_context.command, block_context.output);
let shell = self.active_session.as_ref(ctx).shell_launch_data(ctx);
let shell_for_detection = shell.clone();
let current_working_directory_for_detection = current_working_directory.clone();
+1 -1
View File
@@ -1429,7 +1429,7 @@ impl AIExecutionProfilesModel {
// We don't want stale client ids in our map. We won't be able to find the backing cloud object when
// an edit occurs.
pub fn replace_client_id_with_server_id(&mut self, server_id: SyncId, client_id: SyncId) {
for (_, sync_id) in self.profile_id_to_sync_id.iter_mut() {
for sync_id in self.profile_id_to_sync_id.values_mut() {
if *sync_id == client_id {
*sync_id = server_id;
log::info!("Updated profile id mapping after creating a new execution profile");
+20
View File
@@ -63,6 +63,26 @@ impl ProviderActionContext {
}
}
pub(crate) fn tool_call_title(&self, call: &ToolCall) -> Option<String> {
let parameter = match self.mcp_tool_aliases.get(&call.name) {
Some(target) => target.title_parameter.as_deref()?,
None => super::rig_request::TOOL_TITLE_PARAMETER,
};
call.arguments
.get(parameter)
.and_then(serde_json::Value::as_str)
.map(|title| {
title
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.chars()
.take(120)
.collect::<String>()
})
.filter(|title| !title.is_empty())
}
pub(crate) fn action_from_tool_call(&self, call: &ToolCall) -> Result<AIAgentAction, String> {
action_from_tool_call(
&self.task_id,
+39 -2
View File
@@ -123,6 +123,8 @@ pub(crate) fn add_orchestration_model_options(
pub(crate) struct MCPToolTarget {
pub server_id: Option<Uuid>,
pub name: String,
#[serde(default)]
pub title_parameter: Option<String>,
}
pub(crate) fn prepare_rig_turn(
@@ -257,6 +259,12 @@ fn prepare_rig_turn_for_provider(
}
RigRequestMode::Normal | RigRequestMode::Plan | RigRequestMode::Orchestrate => {}
}
for tool in &mut tools {
let parameter = add_tool_title_parameter(&mut tool.input_schema);
if let Some(target) = mcp_tool_aliases.get_mut(&tool.name) {
target.title_parameter = Some(parameter);
}
}
let system_prompt = build_system_prompt(&input, &tools, &global_rules, mode);
let mut new_messages = input_messages(input, tool_results);
@@ -631,6 +639,7 @@ fn tool_definitions(
MCPToolTarget {
server_id: Uuid::parse_str(&server.id).ok(),
name: tool.name.to_string(),
title_parameter: None,
},
);
tools.push(ToolDefinition {
@@ -654,6 +663,7 @@ fn tool_definitions(
MCPToolTarget {
server_id: None,
name: tool.name.to_string(),
title_parameter: None,
},
);
tools.push(ToolDefinition {
@@ -670,6 +680,33 @@ fn tool_definitions(
(tools, mcp_tool_aliases)
}
pub(crate) const TOOL_TITLE_PARAMETER: &str = "_galaxy_tool_title";
fn add_tool_title_parameter(schema: &mut serde_json::Value) -> String {
let properties = schema["properties"].as_object_mut();
let mut parameter = TOOL_TITLE_PARAMETER.to_string();
if let Some(properties) = properties {
while properties.contains_key(&parameter) {
parameter.push('_');
}
} else {
schema["properties"] = serde_json::json!({});
}
schema["properties"][&parameter] = serde_json::json!({
"type": "string",
"description": "Short pane title stating this call's specific purpose, e.g. Find cache invalidation paths. Use 3–10 words; no greetings, status filler, or secrets.",
"maxLength": 120
});
if !schema["required"].is_array() {
schema["required"] = serde_json::json!([]);
}
schema["required"]
.as_array_mut()
.unwrap()
.push(parameter.clone().into());
parameter
}
fn tools_are_inline_only(tools: &[ToolDefinition]) -> bool {
tools.iter().all(|tool| tool.name == "recall_tool_history")
}
@@ -726,10 +763,10 @@ fn build_system_prompt(
"You are Galaxy, a local-first software-engineering and terminal agent. Complete the user's task through inspection, implementation, and proportionate validation. Galaxy owns tool permissions and execution; use only the tools advertised in this request and treat every result as authoritative evidence.\n\n",
);
prompt.push_str(
"## Communication Style\nSpeak naturally, warmly, and directly, like a thoughtful collaborator working alongside the user. Default to short responses and expand only when complexity or the user's request warrants it. Acknowledge a correction once when useful; do not repeatedly agree that the user is right, praise them, or add generic reassurance. For nontrivial work, briefly tell the user what you are checking before the first tool call. Between dependent tool calls, add a concise update only when a result materially changes what you learned or what you will do next; ground it in specifics instead of generic activity narration. Do not narrate every routine read, repeat the plan, or end a turn with only a progress update when useful work can continue. In the final response, lead with the outcome and keep the handoff compact.\n\n",
"## Communication Style\nSpeak naturally, warmly, and directly, like a thoughtful collaborator working alongside the user. Default to short responses and expand only when complexity or the user's request warrants it. Acknowledge a correction once when useful; do not repeatedly agree that the user is right, praise them, or add generic reassurance. For nontrivial work, briefly tell the user what you are checking once before the first tool call of a new user request. Tool results and compaction checkpoints continue the same request: resume the work without another greeting, acknowledgement, or restatement of the plan. Supply the short purpose title requested by each tool schema. Between dependent tool calls, add a concise update only when a result materially changes what you learned or what you will do next; ground it in specifics instead of generic activity narration. Do not narrate every routine read, repeat the plan, or end a turn with only a progress update when useful work can continue. In the final response, lead with the outcome and keep the handoff compact.\n\n",
);
prompt.push_str(
"## Execution Contract\nContinue until the user's requested outcome is complete and validated. Do not stop at an intermediate analysis, plan, status update, or promise of future work, and do not ask the user to say \"continue\". After each tool result, choose and perform the next necessary step. Stop only when the request is fulfilled or a concrete blocker requires user input; identify that blocker explicitly.\n\n",
"## Execution Contract\nBefore another read or search, identify the missing fact it will resolve. Reuse findings already present in the conversation. Once enough evidence exists for the requested change, implement it and validate; additional inspection should resolve a specific uncertainty. Batch independent reads within the tool limits and wait for results before dependent work. Continue until the user's requested outcome is complete and validated. Do not stop at an intermediate analysis, plan, status update, or promise of future work, and do not ask the user to say \"continue\". After each tool result, choose and perform the next necessary step. Stop only when the request is fulfilled or a concrete blocker requires user input; identify that blocker explicitly.\n\n",
);
prompt.push_str(
"## Task Tracking\nUse task tracking only for substantial work with multiple concrete steps, after planning has established those steps or when the user provides an actionable plan. Skip it for simple requests, questions, and exploratory planning. Each task must name a specific change or deliverable with a clear completion condition. Do not create generic workflow items such as 'research the request', 'critique the plan', 'execute the plan', or 'verify the result'. Update tasks only when their actual work is complete; a tool call or model turn alone is not evidence of completion.\n\n",
+22
View File
@@ -840,3 +840,25 @@ fn progressive_summary_is_provider_context_not_persistent_history() {
MessageContent::Text(text) if text == "Continue"
));
}
#[test]
fn tool_title_schema_preserves_existing_parameters() {
let mut schema = serde_json::json!({
"type": "object",
"properties": {"_galaxy_tool_title": {"type": "integer"}},
"required": ["_galaxy_tool_title"],
"additionalProperties": false
});
let parameter = super::add_tool_title_parameter(&mut schema);
assert_eq!(parameter, "_galaxy_tool_title_");
assert_eq!(
schema["properties"]["_galaxy_tool_title"]["type"],
"integer"
);
assert_eq!(schema["properties"][&parameter]["type"], "string");
assert_eq!(
schema["required"],
serde_json::json!(["_galaxy_tool_title", "_galaxy_tool_title_"])
);
assert_eq!(schema["additionalProperties"], false);
}
+23 -1
View File
@@ -1,4 +1,6 @@
use super::prepare_provider_run;
use galaxy_agent_core::ToolCall;
use super::{prepare_provider_run, ProviderActionContext};
use crate::ai::agent::api::RequestParams;
use crate::ai::llms::LLMId;
use crate::ai::openai::client::OpenAIClientConfig;
@@ -73,3 +75,23 @@ fn subscription_context_budget_uses_catalog_limit_and_clamps_stale_overrides() {
);
}
}
#[test]
fn tool_titles_are_optional_and_normalized_for_display() {
let context = ProviderActionContext::new_for_test("task");
let mut call = ToolCall {
id: "call".to_string(),
name: "read_files".to_string(),
arguments: serde_json::json!({"_galaxy_tool_title": " Read\n cache configuration "}),
};
assert_eq!(
context.tool_call_title(&call).as_deref(),
Some("Read cache configuration")
);
call.arguments = serde_json::json!({"_galaxy_tool_title": "界".repeat(200)});
assert_eq!(context.tool_call_title(&call).unwrap().chars().count(), 120);
call.arguments = serde_json::json!({"_galaxy_tool_title": " "});
assert_eq!(context.tool_call_title(&call), None);
call.arguments = serde_json::json!({});
assert_eq!(context.tool_call_title(&call), None);
}
+7 -1
View File
@@ -35,7 +35,13 @@ pub(crate) fn action_from_tool_call(
AIAgentActionType::CallMCPTool {
server_id: target.server_id,
name: target.name.clone(),
input: input.clone(),
input: {
let mut arguments = input.clone();
if let Some(parameter) = &target.title_parameter {
arguments.as_object_mut().unwrap().remove(parameter);
}
arguments
},
}
} else {
match call.name.as_str() {
+30
View File
@@ -381,6 +381,7 @@ fn provider_safe_mcp_aliases_resolve_to_the_original_tool() {
MCPToolTarget {
server_id: Some(server_id),
name: "performance_analyze_insight".to_string(),
title_parameter: None,
},
)]);
@@ -550,3 +551,32 @@ fn unknown_tools_are_rejected_before_the_permission_boundary() {
assert!(error.contains("unsupported Rig tool proposal"));
}
#[test]
fn mcp_title_metadata_does_not_change_tool_arguments() {
let alias = "mcp__echo";
let aliases = HashMap::from([(
alias.to_string(),
MCPToolTarget {
server_id: None,
name: "echo".to_string(),
title_parameter: Some("_galaxy_tool_title_".to_string()),
},
)]);
let arguments = serde_json::json!({
"message": "hello",
"_galaxy_tool_title": "a real server argument",
"_galaxy_tool_title_": "Check the echo service"
});
let action = action_from_tool_call(
"task-1",
&call(alias, arguments),
&SkillPathOrigin::Local,
&aliases,
)
.unwrap();
assert!(
matches!(action.action, AIAgentActionType::CallMCPTool { input, .. }
if input == serde_json::json!({"message": "hello", "_galaxy_tool_title": "a real server argument"}))
);
}