Skip to content
Merged
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
31 changes: 20 additions & 11 deletions crates/tinyagents-harness/src/tool_calling/dialect/pformat.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
//! The positional dialect: `<tool_call>read_file[src/main.rs]</tool_call>`.
//! The slot-indexed dialect: `<tool_call>read_file[0|src/main.rs]</tool_call>`.
//!
//! Roughly an 80% token saving over the JSON form on the call side, and more
//! than that on the catalogue side, since a signature replaces a schema. See
//! [`crate::tool_calling::pformat`] for the grammar itself.
//!
//! The interesting property is that it degrades rather than fails: a body that
//! is not a well-formed positional call falls through to the JSON parser per
//! tag, so a model that mixes the two forms in one response — or ignores the
//! protocol entirely and emits JSON — is still understood.
//! is not a well-formed p-format call falls through to the JSON parser per tag,
//! so a model that mixes the two forms in one response — or ignores the protocol
//! entirely and emits JSON — is still understood. That fallback is also what
//! makes the parser's strictness affordable: a call with a miscounted or
//! non-numeric index is refused here and retried as JSON, rather than being
//! bound to whichever parameters it happens to line up with.

use std::sync::Arc;

Expand Down Expand Up @@ -58,19 +61,25 @@ impl PFormatDialect {
let mut instructions = String::new();
instructions.push_str("## Tool Use Protocol\n\n");
instructions.push_str(
"Tool calls use **P-Format** (Parameter-Format): compact, positional, \
"Tool calls use **P-Format** (Parameter-Format): compact, slot-indexed, \
pipe-delimited syntax wrapped in `<tool_call>` tags. ~80% cheaper on tokens \
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\
- Empty calls: `name[]` for zero-arg tools.\n\
- Empty argument: `name[||value]` is three positional values, the first two empty.\n\
- Form: `name[index|value|index|value|...]`. Each value is preceded by the slot \
number it fills, taken from that tool's `Call as:` signature in the `## Tools` \
section above.\n\
- **Send only the arguments you mean to send.** To pass just the third slot, \
write `name[2|value]` — there are no empty slots to count.\n\
- The signature shows each slot as `index|<name>`, e.g. \
`search[0|<query>|1|<limit>]`. `<name>` is a placeholder: replace it with the \
value, and do not send the name itself.\n\
- Empty calls: `name[]` for zero-arg tools, or for a call sending no arguments.\n\
- A call whose indices are missing, non-numeric, or not in the signature is \
**rejected** — it will not run. Copy the numbers from the signature.\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
12 changes: 6 additions & 6 deletions crates/tinyagents-harness/src/tool_calling/dialect/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ fn xml_dialect_embeds_the_full_schema_catalogue() {
}

#[test]
fn pformat_dialect_parses_a_positional_call() {
fn pformat_dialect_parses_an_indexed_call() {
let registry = build_registry([("get_weather", weather_schema().parameters)]);
let dialect = PFormatDialect::new(registry);

let (_text, calls) = dialect.parse_response(&response(
"<tool_call>get_weather[London|metric]</tool_call>",
"<tool_call>get_weather[0|London|1|metric]</tool_call>",
));

assert_eq!(calls.len(), 1);
Expand All @@ -85,7 +85,7 @@ fn pformat_dialect_falls_back_to_json_per_tag() {
let dialect = PFormatDialect::new(registry);

let (_text, calls) = dialect.parse_response(&response(
"<tool_call>get_weather[London|metric]</tool_call>\n\
"<tool_call>get_weather[0|London|1|metric]</tool_call>\n\
<tool_call>{\"name\": \"other_tool\", \"arguments\": {\"x\": 1}}</tool_call>",
));

Expand All @@ -104,7 +104,7 @@ fn pformat_dialect_leaves_the_catalogue_to_the_prompt() {
// (`get_weather` and `Call as:` still appear — as the syntax example and as
// a pointer at the `## Tools` section that owns the real listing.)
assert!(!instructions.contains("Look up the weather"));
assert!(!instructions.contains("get_weather[location|unit]"));
assert!(!instructions.contains("get_weather[0|<location>|1|<unit>]"));
assert!(!PFormatDialect::new(PFormatRegistry::new()).embeds_tool_catalogue());
}

Expand Down Expand Up @@ -532,15 +532,15 @@ fn catalogue_signature_matches_what_the_parser_reconstructs() {
let rendered = render_pformat_catalogue(&tools);

assert!(rendered.starts_with(CATALOGUE_HEADING));
assert!(rendered.contains("Call as: `get_weather[location|unit]`"));
assert!(rendered.contains("Call as: `get_weather[0|<location>|1|<unit>]`"));

// The catalogue order is the order the parser assigns, not a coincidence.
let dialect = PFormatDialect::new(build_registry([(
"get_weather",
weather_schema().parameters,
)]));
let (_text, calls) = dialect.parse_response(&response(
"<tool_call>get_weather[London|metric]</tool_call>",
"<tool_call>get_weather[0|London|1|metric]</tool_call>",
));
assert_eq!(calls[0].arguments["location"], "London");
assert_eq!(calls[0].arguments["unit"], "metric");
Expand Down
8 changes: 4 additions & 4 deletions crates/tinyagents-harness/src/tool_calling/parse_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ fn a_pformat_tag_does_not_suppress_a_sibling_glm_tag() {
);

let response = concat!(
"<tool_call>echo[hello]</tool_call>\n",
"<tool_call>echo[0|hello]</tool_call>\n",
"<tool_call>shell/command>ls -la</tool_call>"
);
let (_narrative, calls) = parse_tool_calls_with_pformat(response, &reg);
Expand Down Expand Up @@ -446,7 +446,7 @@ fn a_pformat_tag_does_not_suppress_a_sibling_fenced_json_tag() {
);

let response = concat!(
"<tool_call>echo[hello]</tool_call>\n",
"<tool_call>echo[0|hello]</tool_call>\n",
"<tool_call>\n```json\n{\"name\": \"shell\", \"arguments\": {\"command\": \"ls\"}}\n```\n</tool_call>"
);
let (_narrative, calls) = parse_tool_calls_with_pformat(response, &reg);
Expand Down Expand Up @@ -478,7 +478,7 @@ fn a_json_body_is_not_double_counted_by_the_glm_fallback() {
);

let response = concat!(
"<tool_call>echo[hello]</tool_call>\n",
"<tool_call>echo[0|hello]</tool_call>\n",
"<tool_call>{\"name\": \"shell\", \"arguments\": {\"command\": \"cat a/b>c\"}}</tool_call>"
);
let (_narrative, calls) = parse_tool_calls_with_pformat(response, &reg);
Expand Down Expand Up @@ -510,7 +510,7 @@ fn a_tagged_body_still_honours_argument_key_aliases() {
);

let response = concat!(
"<tool_call>echo[hello]</tool_call>\n",
"<tool_call>echo[0|hello]</tool_call>\n",
"<tool_call>{\"name\": \"shell\", \"args\": {\"command\": \"ls\"}}</tool_call>"
);
let (_narrative, calls) = parse_tool_calls_with_pformat(response, &reg);
Expand Down
Loading