Skip to content

server: tool-call readers search for an absent opener once - #882

Merged
ashhart merged 1 commit into
ashhart:mainfrom
jishnuvenugopal:fix/tool-call-openers-once
Oct 16, 2026
Merged

ashhart merged 1 commit into
ashhart:mainfrom
jishnuvenugopal:fix/tool-call-openers-once

Conversation

@jishnuvenugopal

@jishnuvenugopal jishnuvenugopal commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

envelopes and closeCall (zig/src/server/tool_parse.zig) looked for a DSML opener from each <tool_call> they
passed, and hideInto (zig/src/server/reply_text.zig) looked for each kind of call opener from each call it passed.
An opener the reply does not contain was searched for to the end of the reply every time. Each now keeps where an
opener is next and searches again only once it has read past that place. The searches only move forward, so they
find what they found before. Only these scans change: a namespaced <ns:tool_call> opener with no closer is still
searched past once per opener (envelopes, tool_parse.zig:75), and the envelope sort is unchanged.

Verified outcome

  • Why: a reply with many tool calls slowed down with the square of its length, and while a reply streams, the
    server reads the whole answer again after every new piece of text. A 128 KiB reply of calls took 51 ms to parse at
    the end, and 54 ms for the check after each streamed piece.
  • What was done: the search for each kind of tool-call marker remembers where the next one is, instead of starting
    over after every call.
  • Result: the same reply parses in 1.4 ms, and the check after each streamed piece takes 0.1 ms. Output is
    unchanged: 50,000 varied replies give byte-identical results.

Receipt

  • Environment: main at c7137bb, Zig 0.17.0, Apple M4, macOS 27.0.1. Host code: no model or GPU.

  • Exactness: n/a. No engine change; the reply's tokens and text are the same.

  • Decode speed before and after: n/a. bench_openai.py's prompts offer no tools, and these readers only run for
    requests that offer tools.

  • Prompt speed before and after: n/a, for the same reason.

  • Speed on <tool_call>{"name":"lookup","arguments":{"n":1}}</tool_call> repeated, in ReleaseSafe as the server
    ships, fastest of three, c7137bb then this branch:

    Reader 16 KiB 128 KiB
    parse 0.99 ms, 0.18 ms 51.4 ms, 1.4 ms
    closeCall 2.23 ms, 0.06 ms 50.5 ms, 0.5 ms
    hideToolCalls 0.84 ms, 0.01 ms 53.5 ms, 0.1 ms
  • Same output: a local fuzz harness (not part of this change) ran 50,000 edited replies through parse, closeCall,
    hideToolCalls and finishedProse on c7137bb and on this branch. A digest of everything they returned, call ids
    left out, is equal (4c8dc88dffd56f19).

  • Tests run:

    • New, a reply of many calls reads in linear time (tool_parse_test.zig): parse, closeCall and
      hideToolCalls on 400 JSON calls and on 3,200. 8x the calls may take at most 24x the time (or under 20 ms), the
      bound fuzz-cortheon's linear-time test uses. It fails on c7137bb and passes on this branch.
    • zig build test test-golden -Dcpu=apple_m1, uncached: 164 of 164 steps, 1982 of 1990 tests pass, 8 skip,
      0 fail (c7137bb: 1981 of 1989, 8 skip); the one more is the new test. zig build fuzz-cortheon passes.
      zig fmt --check build.zig zig is clean. Lean: 527 problems on the base and 527 on this branch, 0 new
      (lean_new.py).
  • Not run, and why: no CUDA host here. These readers are server code that both backends use, and their tests need no
    GPU.

Checklist

  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off. (No engine
    change.)
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. (No
    engine change; not re-run.)
  • No precision traded for speed. If the bits change, the description says which sums change and why. (No
    arithmetic changes.)
  • Prompt processing is no slower than the last release. (n/a, as above.)
  • The description says which platforms I ran, Metal M1 to M5 and CUDA, and which I could not.
  • New tests fail before the change, pass after it, and skip cleanly without their dependency.
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths. No AI attribution lines.

envelopes and closeCall looked for a DSML opener from each <tool_call> they passed, and hideInto looked for each
kind of call opener from each call it passed, so an opener the reply lacked was searched for to its end every time.
Each now keeps where an opener is next and searches again only once it has read past it.
@ashhart

ashhart commented Oct 15, 2026

Copy link
Copy Markdown
Owner

Thank you, @jishnuvenugopal, for fixing the missing-opener rescans in parsing, closeCall and streaming hiding together.

At fe4299b, each cached match remains valid until the cursor passes it, and each call starts with fresh caches. We found no changed parsing or hiding decision. zig/src/server/tool_parse.zig:75 still rescans suffixes for unmatched namespaced openers, and envelope sorting is unchanged.

Please scope the linear-time claim to the scans this change fixes. The parser and reply-text integration checks can then cover mixed and incomplete markup.

Posted by Ash's AI Assistant Synthena

@jishnuvenugopal jishnuvenugopal changed the title server: a reply of many tool calls reads in linear time server: tool-call readers search for an absent opener once Oct 16, 2026
@jishnuvenugopal

Copy link
Copy Markdown
Contributor Author

Thank you for checking the cache lifetimes. Agreed: this only stops the rescans for the DSML opener in envelopes
and closeCall and for the call openers in hideInto. A namespaced opener with no closer is still searched past once
per opener (tool_parse.zig:75), and the envelope sort is unchanged. I've retitled the PR and scoped the description
to those scans.

@jishnuvenugopal

Copy link
Copy Markdown
Contributor Author

Thank you again, @ashhart. The tool_parse.zig:75 scan you noted is fixed in #913, which merges cleanly with this PR, #881 and #883.

@ashhart
ashhart merged commit 99505af into ashhart:main Oct 16, 2026
ashhart added a commit that referenced this pull request Oct 16, 2026
… against the old search

#881's putBack is exact only for replacements without a NUL, which json.quote guarantees for its one caller; its
doc line now says so, and the sort line says the cost: one pass over the text, then M log M for M marks. The two
timing tests from #882 and #883 now name what they measure (each opener, and each key and value end, searched
once) instead of claiming that all tool parsing is linear. A new test parses 20,000 random GLM calls (keys without
values, values without closers, repeated keys, stray tags, whitespace) and checks each against the same pairs
found by #883's parent loop, written out plainly: the arguments must be identical.
ashhart added a commit that referenced this pull request Oct 16, 2026
…VIEW names the refused GLM pair requests rightly

One line each for #881, #882, #883, #889, #890, #709, #848, #892, #867, #868, #871 and #873, crediting their
authors. ZIG-PREVIEW said a two-Mac GLM pair refuses "temperature-1" requests; it refuses any temperature above 0
(backend.zig and engine.zig both test temperature > 0).
@ashhart

ashhart commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Thank you, @jishnuvenugopal, for caching each tool-call opener search.

#882 is included in 1.0.12 as merge 99505af, at fe4299b. The opener results are reused during parsing, closing and hiding calls.

Our 084406f renames the timing test to describe the cached searches. An unmatched namespaced opener can still rescan its suffix, so the claim is limited to the searches this change removes.

The macOS suite passes 2,968 of 2,978 tests with 10 skipped; Linux passes 1,368 of 1,375 with 7 skipped. Additional mixed DSML and namespaced differential cases remain owed on our side.

Posted by Ash's AI Assistant Synthena

@jishnuvenugopal
jishnuvenugopal deleted the fix/tool-call-openers-once branch October 17, 2026 03:06
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
… against the old search

ashhart#881's putBack is exact only for replacements without a NUL, which json.quote guarantees for its one caller; its
doc line now says so, and the sort line says the cost: one pass over the text, then M log M for M marks. The two
timing tests from ashhart#882 and ashhart#883 now name what they measure (each opener, and each key and value end, searched
once) instead of claiming that all tool parsing is linear. A new test parses 20,000 random GLM calls (keys without
values, values without closers, repeated keys, stray tags, whitespace) and checks each against the same pairs
found by ashhart#883's parent loop, written out plainly: the arguments must be identical.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
…VIEW names the refused GLM pair requests rightly

One line each for ashhart#881, ashhart#882, ashhart#883, ashhart#889, ashhart#890, ashhart#709, ashhart#848, ashhart#892, ashhart#867, ashhart#868, ashhart#871 and ashhart#873, crediting their
authors. ZIG-PREVIEW said a two-Mac GLM pair refuses "temperature-1" requests; it refuses any temperature above 0
(backend.zig and engine.zig both test temperature > 0).
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.

2 participants