Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/openhuman/agent/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,14 +334,17 @@ impl ToolDispatcher for PFormatToolDispatcher {
than JSON.\n\n",
);
instructions
.push_str("```\n<tool_call>\nget_weather[London|metric]\n</tool_call>\n```\n\n");
.push_str("```\n<tool_call>\nget_weather[0|London|1|metric]\n</tool_call>\n```\n\n");
instructions.push_str(
"**Rules:**\n\
- Form: `name[arg1|arg2|...|argN]`. Arguments are positional and must match the \
order shown in each tool's `Call as:` signature in the `## Tools` section above \
(alphabetical by parameter name).\n\
- Form: `name[index|value|index|value|...]`. A `Call as:` signature numbers its \
slots and shows each as a `<name>` placeholder; replace each one with a value, \
keeping its number. `get_weather[0|<location>|1|<unit>]` is called as \
`get_weather[0|London|1|metric]`.\n\
- Send only the arguments you are actually passing, each with its own number. \
`get_weather[1|metric]` sends unit and no location. The numbers do the \
skipping, so there are no empty slots to count.\n\
- Empty calls: `name[]` for zero-arg tools.\n\
- Empty argument: `name[||value]` is three positional values, the first two empty.\n\
- Escapes inside argument values: `\\|` → `|`, `\\]` → `]`, `\\\\` → `\\`.\n\
- You may emit multiple `<tool_call>` blocks in a single response. Each tag holds \
exactly one call.\n\
Expand Down
5 changes: 3 additions & 2 deletions src/openhuman/agent/dispatcher_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ fn pformat_dispatcher_parses_tool_call_tag() {
let dispatcher = PFormatToolDispatcher::new(registry);
let response = ChatResponse {
text: Some(
"Let me check the weather.\n<tool_call>get_weather[London|metric]</tool_call>".into(),
"Let me check the weather.\n<tool_call>get_weather[0|London|1|metric]</tool_call>"
.into(),
),
tool_calls: vec![],
usage: None,
Expand Down Expand Up @@ -181,7 +182,7 @@ fn pformat_dispatcher_handles_multiple_tags() {
let dispatcher = PFormatToolDispatcher::new(registry);
let response = ChatResponse {
text: Some(
"Step 1.\n<tool_call>shell[ls]</tool_call>\nStep 2.\n<tool_call>shell[pwd]</tool_call>"
"Step 1.\n<tool_call>shell[0|ls]</tool_call>\nStep 2.\n<tool_call>shell[0|pwd]</tool_call>"
.into(),
),
tool_calls: vec![],
Expand Down
12 changes: 8 additions & 4 deletions src/openhuman/agent/harness/subagent_runner/tool_prep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,16 @@ pub(crate) fn build_text_mode_tool_instructions() -> String {
"Tool calls use **P-Format** (Parameter-Format): compact, positional, \
pipe-delimited syntax wrapped in `<tool_call>` tags.\n\n",
);
out.push_str("```\n<tool_call>\nGMAIL_FETCH_EMAILS[ca_123||10]\n</tool_call>\n```\n\n");
out.push_str("```\n<tool_call>\nGMAIL_FETCH_EMAILS[0|ca_123|2|10]\n</tool_call>\n```\n\n");
out.push_str(
"**Rules:**\n\
- Form: `name[arg1|arg2|...|argN]`. Arguments are positional and must match the \
order shown in each tool's `Call as:` signature in the `## Tools` section \
(alphabetical by parameter name). Leave a slot empty to omit that argument.\n\
- Form: `name[index|value|index|value|...]`. A `Call as:` signature numbers its \
slots and shows each as a `<name>` placeholder; replace each one with a value, \
keeping its number. `get_weather[0|<location>|1|<unit>]` is called as \
`get_weather[0|London|1|metric]`.\n\
- Send only the arguments you are actually passing, each with its own number. \
`get_weather[1|metric]` sends unit and no location. The numbers do the skipping, \
so there are no empty slots to count.\n\
- Empty calls: `name[]` for zero-arg tools.\n\
- Escapes inside argument values: `\\|` for a literal `|`, `\\]` for `]`, `\\\\` for `\\`.\n\
- Do not nest tags. Emit one tag per call; you can emit multiple tags in the same \
Expand Down
404 changes: 342 additions & 62 deletions src/openhuman/agent/pformat.rs

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions src/openhuman/agent/prompts/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,50 @@ fn subagent_render_options_invert_definition_flags() {
assert!(!narrow.include_memory_md);
}

/// The sub-agent block must teach every escape `split_escaped` honours. It
/// already drifted once on the slot numbering — a sub-agent wrote the old
/// `name[arg|arg]` grammar and `parse_pformat_call` dropped the call whole — so
/// an escape documented in one prompt path and not the other is the same defect
/// in a quieter form: a value carrying a backslash is encoded on a guess.
#[test]
fn the_subagent_pformat_block_documents_every_escape_the_parser_honours() {
let workspace = std::env::temp_dir().join(format!(
"openhuman_pformat_escapes_{}",
uuid::Uuid::new_v4()
));
std::fs::create_dir_all(&workspace).unwrap();

let tools: Vec<Box<dyn Tool>> = vec![Box::new(TestTool)];
let rendered = render_subagent_system_prompt_with_format(
&workspace,
"reasoning-v1",
&[0],
&tools,
&[],
"You are a specialist.",
SubagentRenderOptions::default(),
ToolCallFormat::PFormat,
&[],
None,
None,
);

for (escape, what) in [
(r"`\|`", "a literal pipe"),
(r"`\]`", "a literal closing bracket"),
(r"`\\`", "a literal backslash"),
] {
assert!(
rendered.contains(escape),
"the sub-agent P-Format block must document {escape} for {what}"
);
}

// The escape set is the parser's, not this block's: `pformat::split_pipes`
// decodes exactly these three (`handles_backslash_escape` and its
// neighbours pin the decoding itself).
}

#[test]
fn render_subagent_system_prompt_honors_identity_safety_and_skills_flags() {
let workspace =
Expand Down
9 changes: 5 additions & 4 deletions src/openhuman/agent/prompts/render_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,11 @@ pub fn render_subagent_system_prompt_with_format(
"## Tool Use Protocol\n\n\
Tool calls use **P-Format**: compact, positional, pipe-delimited syntax \
wrapped in `<tool_call>` tags.\n\n\
```\n<tool_call>\ntool_name[arg1|arg2]\n</tool_call>\n```\n\n\
Arguments are positional — match the order shown in each tool's `Call as:` \
signature above (alphabetical by parameter name). \
Escape `|` as `\\|`, `]` as `\\]` inside values. \
```\n<tool_call>\nget_weather[0|London|1|metric]\n</tool_call>\n```\n\n\
A `Call as:` signature numbers its slots and shows each as a `<name>` \
placeholder; replace each one with a value, keeping its number, and send \
only the arguments you are actually passing. \
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Escape `|` as `\\|`, `]` as `\\]`, and `\\` as `\\\\` inside values. \
You may emit multiple `<tool_call>` blocks per response.\n\n\
Use the provided tools to accomplish the task. Reply with a concise, dense \
final answer when you have one — the parent agent will weave it back into the \
Expand Down
2 changes: 1 addition & 1 deletion src/openhuman/agent/prompts/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ impl<'a> PromptTool<'a> {
/// historic format; P-Format is the new default text protocol.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ToolCallFormat {
/// `tool_name[arg1|arg2|...]` — compact, positional. Default.
/// `tool_name[index|value|...]` — compact, slot-numbered. Default.
#[default]
PFormat,
/// Legacy JSON-in-tag rendering with full schemas.
Expand Down
2 changes: 1 addition & 1 deletion src/openhuman/agent/tinyagents/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ mod g1_usage_tests {
#[test]
fn prompt_guided_response_keeps_legacy_pformat_fallback() {
let response = prompt_guided_text_response(
"<tool_call>lookup[7|needle]</tool_call>".to_string(),
"<tool_call>lookup[0|7|1|needle]</tool_call>".to_string(),
&tool_request(),
);

Expand Down
Loading