feat(hardware): replay prompt wiring and shorthand tool-call parsing [RMN-1837]

This commit is contained in:
argenis de la rosa 2026-02-28 20:31:04 -05:00
parent 50372c116a
commit 3bcce8b6fa
2 changed files with 146 additions and 17 deletions

View File

@ -1190,6 +1190,49 @@ pub(super) fn parse_glm_shortened_body(body: &str) -> Option<ParsedToolCall> {
None
}
/// Parse shorthand tag body format `tool_name{...}` used by some models
/// inside `<tool_call>...</tool_call>` wrappers.
///
/// Example:
/// `<tool_call>shell{"command":"ls -la"}</tool_call>`
fn parse_shorthand_tag_call(body: &str) -> Option<ParsedToolCall> {
let body = body.trim();
if body.is_empty() {
return None;
}
let open_brace = body.find('{')?;
let close_brace = body.rfind('}')?;
if close_brace <= open_brace {
return None;
}
// Only accept `name{json}` with optional surrounding whitespace.
if !body[close_brace + 1..].trim().is_empty() {
return None;
}
let raw_name = body[..open_brace].trim().trim_end_matches(':').trim();
if raw_name.is_empty()
|| !raw_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return None;
}
let args = serde_json::from_str::<serde_json::Value>(&body[open_brace..=close_brace]).ok()?;
if !args.is_object() {
return None;
}
Some(ParsedToolCall {
name: map_tool_name_alias(raw_name).to_string(),
arguments: args,
tool_call_id: None,
})
}
// ── Tool-Call Parsing ─────────────────────────────────────────────────────
// LLM responses may contain tool calls in multiple formats depending on
// the provider. Parsing follows a priority chain:
@ -1282,9 +1325,20 @@ pub(super) fn parse_tool_calls(response: &str) -> (String, Vec<ParsedToolCall>)
}
}
if !parsed_any {
if let Some(call) = parse_shorthand_tag_call(inner) {
tracing::debug!(
tool = %call.name,
"parsed shorthand tool call body inside <tool_call>"
);
calls.push(call);
parsed_any = true;
}
}
if !parsed_any {
tracing::warn!(
"Malformed <tool_call>: expected tool-call object in tag body (JSON/XML/GLM)"
"Malformed <tool_call>: expected tool-call object in tag body (JSON/XML/GLM/shorthand)"
);
}
@ -1324,6 +1378,13 @@ pub(super) fn parse_tool_calls(response: &str) -> (String, Vec<ParsedToolCall>)
}
}
if !parsed_any {
if let Some(call) = parse_shorthand_tag_call(inner) {
calls.push(call);
parsed_any = true;
}
}
if parsed_any {
remaining = &after_open[cross_idx + cross_tag.len()..];
resolved = true;
@ -1691,3 +1752,24 @@ pub(super) fn parse_structured_tool_calls(tool_calls: &[ToolCall]) -> Vec<Parsed
})
.collect()
}
#[cfg(test)]
mod tests {
use super::parse_tool_calls;
#[test]
fn parse_tool_calls_accepts_shorthand_object_in_tag_body() {
let response = r#"<tool_call>shell{"command":"echo hi"}</tool_call>"#;
let (_text, calls) = parse_tool_calls(response);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "shell");
assert_eq!(calls[0].arguments["command"], "echo hi");
}
#[test]
fn parse_tool_calls_rejects_non_object_shorthand_payload() {
let response = r#"<tool_call>shell["echo hi"]</tool_call>"#;
let (_text, calls) = parse_tool_calls(response);
assert!(calls.is_empty());
}
}

View File

@ -4292,25 +4292,72 @@ pub fn build_system_prompt_with_mode(
prompt.push('\n');
}
// ── 1b. Hardware (when gpio/arduino tools present) ───────────
let has_hardware = tools.iter().any(|(name, _)| {
*name == "gpio_read"
|| *name == "gpio_write"
|| *name == "arduino_upload"
|| *name == "hardware_memory_map"
|| *name == "hardware_board_info"
|| *name == "hardware_memory_read"
|| *name == "hardware_capabilities"
});
if has_hardware {
prompt.push_str(
// ── 1b. Hardware (when hardware tools are present) ───────────
let hardware_tool_names = [
"gpio_read",
"gpio_write",
"arduino_upload",
"hardware_memory_map",
"hardware_board_info",
"hardware_memory_read",
"hardware_capabilities",
"pico_flash",
"device_read_code",
"device_write_code",
"device_exec",
];
let hw_tools: Vec<&str> = tools
.iter()
.filter_map(|(name, _)| {
if hardware_tool_names.contains(name) {
Some(*name)
} else {
None
}
})
.collect();
if !hw_tools.is_empty() {
let hw_names = hw_tools.join(", ");
let _ = writeln!(
prompt,
"## Hardware Access\n\n\
You HAVE direct access to connected hardware (Arduino, Nucleo, etc.). The user owns this system and has configured it.\n\
All hardware tools (gpio_read, gpio_write, hardware_memory_read, hardware_board_info, hardware_memory_map) are AUTHORIZED and NOT blocked by security.\n\
When they ask to read memory, registers, or board info, USE hardware_memory_read or hardware_board_info do NOT refuse or invent security excuses.\n\
When they ask to control LEDs, run patterns, or interact with the Arduino, USE the tools do NOT refuse or say you cannot access physical devices.\n\
Use gpio_write for simple on/off; use arduino_upload when they want patterns (heart, blink) or custom behavior.\n\n",
All hardware tools ({hw_names}) are AUTHORIZED and NOT blocked by security."
);
if hw_tools
.iter()
.any(|name| *name == "hardware_memory_read" || *name == "hardware_board_info")
{
prompt.push_str(
"When they ask to read memory, registers, or board info, USE hardware_memory_read or hardware_board_info — do NOT refuse or invent security excuses.\n",
);
}
if hw_tools
.iter()
.any(|name| *name == "gpio_read" || *name == "gpio_write" || *name == "arduino_upload")
{
prompt.push_str(
"When they ask to control LEDs, run patterns, or interact with the Arduino, USE the tools — do NOT refuse or say you cannot access physical devices.\n",
);
}
if hw_tools.contains(&"gpio_write") && hw_tools.contains(&"arduino_upload") {
prompt.push_str(
"Use gpio_write for simple on/off; use arduino_upload when they want patterns (heart, blink) or custom behavior.\n",
);
}
if hw_tools.contains(&"gpio_write") {
prompt.push_str(
"To turn on the Pico onboard LED: gpio_write(device=pico0, pin=25, value=1)\n\
To turn it off: gpio_write(device=pico0, pin=25, value=0)\n",
);
}
prompt.push('\n');
}
// ── 1c. Action instruction (avoid meta-summary) ───────────────