Recover text tool calls and handle a non-SSE response body - #552
Conversation
|
One thing worth stating explicitly about the test evidence here.
Wired against the base revision, it fails the plain-JSON streaming fallback, This came out of an audit of orphaned tests. The wider TARG wiring is being The GoDis job is the only red check on this PR and is unrelated to the change. |
|
Heads-up on merging this one, and it is my doing rather than yours. This branch predates #560, which stopped tracking compiled bytecode. It has a Git leaves the file sitting in the working tree, so resolving with git rm <path>.disOr just rebase onto current master, where the file no longer exists and the Nothing else in the PR is affected — for d in appl appl/mpeg appl/veltro tests; do (cd $d && mk install); done
Sorry for the friction — five open PRs are in this position because of the |
pdfinn
left a comment
There was a problem hiding this comment.
This one turns model text into executed tool calls, which is AGENTS.md's first
threat, so I probed the guard rather than reading it. It holds.
Applied onto current master (the branch is pre-#560), built, and drove
extracttexttoolcalls directly with a granted tool set of [exec]:
prose then object -> 0 call(s)
object then prose -> 0 call(s)
two objects -> 0 call(s)
ungranted tool name -> 0 call(s)
brace inside string -> 1 call(s) (correctly NOT truncated)
markdown fenced -> 0 call(s)
legit bare call -> 1 call(s)
Both constraints do real work. jsonobjectend(s) != len s rejects anything
with prose or a second object around it, so a model quoting an example cannot
have that example fired — and the string-awareness is genuine, since a }
inside a value does not end the object early. validtoolname(bname, tooldefs)
means a recovered call can only name a tool the agent was already granted, so
this cannot widen the tool set, only the encoding a granted call arrives in.
That is the right boundary: it prevents escalation without pretending to
decide whether a granted tool should have been called.
Your own tests pass too — 5 in llmclient_texttools_test, 6 in
llmclient_sse_fallback_test.
Adding both to tests/mkfile TARG rather than leaving them unwired is the
right instinct and increasingly rare; thank you.
One thing to add
dis/tests/llmclient_texttools_test.dis is not in tools/dis-manifest.txt.
The build flagged it:
note: built but not listed in tools/dis-manifest.txt:
dis/tests/llmclient_texttools_test.dis
llmclient_sse_fallback_test.dis is already listed, so it is just the new
file. Since #560 the manifest is the tracked record of what the build must
produce, so a new module needs a TARG entry and a manifest line in the same
commit — that is in AGENTS.md now. One line.
A question, not an objection
Markdown-fenced output is rejected:
```json
{"name":"exec","arguments":{"cmd":"id"}}
Rejecting is the safe direction and I would not change it without thought. But
fencing is probably the single most common way a model emits JSON in prose, so
the feature may be missing the case it most wants. Accepting it would mean
stripping a fence before the whole-content check, which widens the surface a
little — an injected fenced block would then fire, where today it does not.
Worth saying explicitly in the code which way you chose and why, so the next
person does not "improve" it by adding fence-stripping without noticing they
are relaxing an injection guard.
## Smaller
`jsonunslash` undoing `json.text()`'s `\/` escaping is right, and the comment
explaining that tool args are not XML is the kind of thing that stops a future
revert. `ssebodymode` keying on the first non-whitespace byte is a reasonable
heuristic and the `0 = still unknown` state handles a body that has not
arrived yet.
Add the manifest line and this is good. Also needs the three `.dis` conflicts
resolved by deletion — see my other comment.
984ce5f to
f8e80ce
Compare
|
Added the manifest line for On the fenced-block question — I kept the rejection and said so in the code,
Accepting a fence means stripping it before the whole-content check, which is Verified |
|
On the two jobs that are red here beyond Built three trees on macOS arm64 and ran an A/B/C: Boot SEGV: 0/30 failures at each of the three. 90 boots, no repro. Weak on GoDis
The failure here was I had suspected One thing worth your attention, because #372's retry does not cover this if !crashed || attempt >= maxEmuAttempts {
return out, crashed
}This run did not crash. It completed and printed
So the gate is protected against the crash variant and not the silent one. Happy For this PR: re-running both jobs should clear them. Nothing here needs a code
|
f8e80ce to
e15f61f
Compare
|
Rebased onto current master (df34b02). Force-pushed f8e80ce -> e15f61f.
Re-verified on macOS after the rebase. The #578 fixed the ClusterFuzzLite link break, so |
What this changes
Two things in the OpenAI-compatible client, both about responses that do not
arrive in the shape the code assumed.
Tool calls emitted as bare JSON text. Some models answer a tool-enabled
request with the call as ordinary assistant content instead of filling the
structured
tool_callsfield, so the call is never made and the user sees rawJSON.
extracttexttoolcallsalready recovered three shapes; this adds afourth, a lone
{"name":..., "arguments"|"parameters":{...}}object.It only accepts content that is entirely one JSON object, whitespace aside.
Prose around it is rejected, so a model quoting an example of a tool call
cannot turn that example into a real invocation.
jsonobjectendisstring-aware, so braces inside string values do not end the object early, and
jsonunslashundoes the\/escapingjson.text()applies, which otherwisemangles paths on the
TOOL:line.A JSON body from a server that ignored
stream:true. The client asked fora stream and then parsed whatever came back as SSE. A server that answers a
streaming request with one complete
chat.completionobject produced an emptyresponse and no error.
ssebodymodelooks at the first non-whitespace byte ofthe body:
{means a complete object, anything else is treated as SSE.Tests
tests/llmclient_texttools_test.bis new: five cases overextracttexttoolcalls, including the rejections.tests/llmclient_sse_fallback_test.bgains the non-SSE cases. Both files areadded to
tests/mkfileTARG — they are built and run rather than sitting inthe tree unwired.
Against the current
dis/lib/llmclient.dis, before the fix:After, on macOS ARM64:
IncrementalSseNoDuplicationandSseTrailerNotReparsedpass before andafter on purpose. They pin behaviour this branch must not break: that a
streamed reply is assembled once, and that JSON arriving after
[DONE]doesnot replace it. An early version of the body-mode check did break the second
one, which is why it is pinned here.
module/llmclient.mgains one function, sollmsrvwas rebuilt against it;its bytecode is unchanged and its 62 tests still pass.