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
+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"}))
);
}