Skip to content

fix(tool_calling): stop dropping a GLM tag beside a p-format tag - #104

Merged
senamakel merged 1 commit into
harness-tool-callingfrom
fix-glm-sibling-drop
Aug 13, 2026
Merged

fix(tool_calling): stop dropping a GLM tag beside a p-format tag#104
senamakel merged 1 commit into
harness-tool-callingfrom
fix-glm-sibling-drop

Conversation

@senamakel

Copy link
Copy Markdown
Member

Stacked on #102 — based on harness-tool-calling so the diff is just the fix (2 files). GitHub retargets this to main automatically when #102 merges.

Fixes the silent tool-call loss documented as an #[ignore]d test in #102, and un-ignores it.

The bug

parse_tool_calls_with_pformat walks <tool_call>-family tags, taking a p-format body where one parses and re-parsing the rest as JSON. Once any tag yields a p-format call, the walk never falls back to the canonical parse — so every remaining tag is on its own, and the JSON path is all it has.

A GLM body is not JSON:

<tool_call>echo[hello]</tool_call>
<tool_call>shell/command>ls -la</tool_call>

The shell call was dropped. Silently: the agent asked for a tool, didn't get it, and nothing reported it.

Inherited, not introduced by the relocation. The probe fails identically against the pre-port OpenHuman code and against 45557a2. #102 landed it #[ignore]d specifically so it stayed visible rather than being quietly carried forward.

The fix, and the two ways it could have gone wrong

A GLM fallback for a tag body the JSON path could not read. Both risks it introduces are pinned by tests, because both fail silently:

1. Ordering is load-bearing — the fallback runs only when the JSON path found nothing. GLM's name/key>value shape can appear inside a JSON string value:

{"name": "shell", "arguments": {"command": "cat a/b>c"}}

An unconditional fallback counts that body once as JSON and again as GLM, and the agent executes the same tool twice. → a_json_body_is_not_double_counted_by_the_glm_fallback

2. Routing through parse_tool_calls was the obvious alternative and is wrong. That function deliberately forbids the args/parameters/input argument-key aliases for a bare top-level object, so a plain JSON answer cannot be misread as a tool call. But a <tool_call> tag is an explicit marker, where the aliases do apply. Routing through it would have traded this silent drop for a different one, on aliased tagged calls. → a_tagged_body_still_honours_argument_key_aliases

So the permissive JSON path stays exactly as it was, and GLM is tried only after it comes back empty.

Scope note

Only the GLM grammar is added to that branch. A markdown-fenced JSON body already works — extract_json_values finds the JSON inside the fence — and that is pinned by a_pformat_tag_does_not_suppress_a_sibling_fenced_json_tag, which passed before this change and still does.

Verification

🤖 Generated with Claude Code

`parse_tool_calls_with_pformat` walks `<tool_call>`-family tags, taking a
p-format body where one parses and re-parsing the rest as JSON. Once ANY
tag yields a p-format call the walk never falls back to the canonical
parse, so each remaining tag is on its own - and the JSON path is all it
has. A GLM body (`shell/command>ls -la`) is not JSON, so the call was
silently dropped: the agent lost a tool invocation it had asked for, and
nothing reported it.

Adds a GLM fallback for a tag body the JSON path could not read.

Ordering is load-bearing: the fallback runs ONLY when the JSON path found
nothing. GLM's `name/key>value` shape can occur inside a JSON string value
(`{"command": "cat a/b>c"}`), so an unconditional fallback would count
that body once as JSON and again as GLM, and the agent would execute the
same tool twice. Pinned by
`a_json_body_is_not_double_counted_by_the_glm_fallback`.

Routing the branch through `parse_tool_calls` instead was the obvious
alternative and is wrong. That function forbids the `args`/`parameters`/
`input` argument-key aliases for a bare top-level object, deliberately, so
a plain JSON answer cannot be misread as a tool call. But a `<tool_call>`
tag IS an explicit marker, where the aliases DO apply - so routing through
it would have traded this silent drop for a different one, on aliased
tagged calls. Pinned by `a_tagged_body_still_honours_argument_key_aliases`.

The bug is inherited, not introduced by the relocation: the probe fails
identically against the pre-port OpenHuman code. It was landed as an
`#[ignore]`d test in #102 precisely so it stayed visible; this un-ignores
it.

1823 lib tests pass, no ignores. Clippy and fmt clean.

Co-authored-by: Medulla <medulla@tinyhumans.ai>
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3892aaf4-f155-4a79-8798-203dddd851d9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@senamakel
senamakel merged commit 38f7851 into harness-tool-calling Aug 13, 2026
3 checks passed
senamakel added a commit that referenced this pull request Aug 13, 2026
… usable by a host

Two halves, both discovered by actually wiring OpenHuman onto this module.

1. The GLM sibling drop (re-lands #104 against main)

#104 merged into #102's branch, but #102 had already merged to main - so the
fix never reached main. `main` today carries `tool_calling` with the
`#[ignore]`d test and no fix.

`parse_tool_calls_with_pformat` walks `<tool_call>`-family tags. Once ANY
tag yields a p-format call the walk never falls back to the canonical
parse, so every remaining tag has only the JSON path. A GLM body
(`shell/command>ls -la`) is not JSON, so the call was silently dropped.

The fallback runs ONLY when the JSON path found nothing: GLM's
`name/key>value` shape can occur inside a JSON string value
(`{"command": "cat a/b>c"}`), and an unconditional fallback would count
that body twice, making the agent run the same tool twice. Routing through
`parse_tool_calls` instead is also wrong - it forbids the argument-key
aliases for a bare top-level object, but a `<tool_call>` tag IS an explicit
marker where they apply, so that would trade one silent drop for another.
Both pinned by tests.

2. Host-consumability fixes

The first real consumer could not compile against this module:

- `extract_json_values` was unreachable, and it is not a test helper:
  pulling the first JSON object out of model prose is how a host checks a
  required-output contract, which has nothing to do with tool calls. Now
  exported, along with `parse_arguments_value`,
  `parse_glm_style_tool_calls`, `parse_tool_call_value` and
  `parse_tool_calls_from_json_value`, all of which host tests exercise
  directly.
- `parse_tool_call_value` was `#[cfg(test)]`, inherited from a host where
  it was test-only. It is a reasonable primitive - parse one JSON value as
  a tool call - so it is un-gated and documented rather than duplicated
  host-side.
- `build_registry` took `&Value`. A host tool trait that RETURNS a schema
  by value - the common shape - then has to collect into a temporary just
  to hand out references. It takes `Borrow<Value>` now, so both forms work.

Without these a host either shadows the code it just stopped owning, or
loses the tests that covered it. OpenHuman keeps all 62 of its parser
tests working against the crate.

1823 lib tests pass, no ignores. Clippy and fmt clean.

Co-authored-by: Medulla <medulla@tinyhumans.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant