Repository navigation
fix: warn on tool-call XML fragments + close-side truncated acp echo (#361) - #365
Conversation
📦 Built Package ArtifactBranch: Option A — Install from npm PR tag (recommended)npm install -g billion-context@pr-365Each push to this PR publishes a new version under the Option B — Download artifact
tar xzf billion-context-pr365.tgz
npm install -g packageThis comment is automatically updated on each push. |
[bot] Picked up. Checking out the PR branch and verifying the pre-flight claims (typecheck / test / build) plus a code review of the three changed files. Will report back with findings. |
[bot] Review complete. I checked out the PR branch, verified every pre-flight claim, traced the new filter logic by hand, and test-merged against the latest master. Pre-flight — all verified ✅
Code review — correct ✅
|
[bot] Full review (re-posting — my previous comment got truncated by the renderer eating literal tag sequences; angle brackets below are entity-encoded so they survive). Pre-flight — all verified ✅
Code review — correct ✅
Caveats — please read before merging
|
…361) Detect the non-acp tool-call XML close-fragment family (</invoke>, </tool_calls>, </parameter>, antml: namespace, JSON-escaped forms) that a model may echo from the context — the same source as acp tag echo that produces a "fake completion" (tool call written as text, no tool_use block). Warn-only, NOT stripped: these fragments are indistinguishable from legitimate prose discussing tool-call code (see #295 review), so stripping would corrupt real content. A once-per-stream warn in the server streaming loop makes the leak attributable in bili.log. Also: close-side truncated acp tag handling (TRUNC_CLOSE) mirroring TRUNC_OPEN; fix dangling RENDER_TAG_DETECT reference (regression); tests.
517752a to
90f6979
Compare
A small model (e.g. MiniMax-M3 via a gateway) can write a tool call as plain text - echoing the tool-call XML template from the context - instead of emitting a real tool block. With no tool block the agent ends the turn early and shows success, but the tool never ran (the "fake completion" from #371, root-cause follow-up to #361). Add a proxy-side detect + retry fallback: - Detect: a response carrying a complete tool-call structure (an opening invoke-style tag, or 2+ distinct closing tags) AND no real tool block for the wire protocol (Anthropic tool_use / OpenAI tool_calls / Responses function_call). Reuses PR #365's containsToolCallXmlFragment detector; the structural guard avoids false-positives on legit prose that merely mentions a single closing tag. - Retry: re-request upstream with a corrective hint appended as a trailing user message. Bounded per turn (max retries) and per session (session.metadata.fakeCompletionStreak, resets on a clean turn). Opt-in (disabled by default): the fallback must buffer the whole response before the client sees it (a fake completion is only knowable at end-of-stream), which defeats incremental streaming. That cost is only worth paying for the low-frequency case, so it is opt-in via BILI_FAKE_COMPLETION_RETRIES (default 0 = pre-#371 passthrough). Tests: tests/fake-completion.test.ts (21) - detection per protocol, the structure guard, hint injection per protocol, and full-proxy integration (fake -> retry -> real tool_use; retry cap via streak; legit prose no-retry).
…anxianglei#1188, ranxianglei#1205) - package.json: acp-kernel 0.0.84 -> 0.0.85 (sole delta: kernel PR ranxianglei#365) - src/wire-drop-warn.ts: user messages no longer drop parts (all non-text parts ride rawOpenaiContentParts since 0.0.85) — detector now flags only the surviving drop class (non-user roles still reduce to text-only) - src/server.ts: warn message reworded for the remaining class - tests/fix-1205-file-parts.test.ts: user-role drop assertions flip to preserved; +4 kernel round-trip activation tests (file refs verbatim, mixed order, inline data-URL byte-exact, lone-image legacy sidecar) - tests/fix-781-summary-images.test.ts: e2e contract updated — media payloads are never folded, so bytes reach the post-compression forward byte-exact and the summary input carries no image placeholders (ranxianglei#1188 path B)
Fixes #361.
What changed
The 0.1.63 leak had two fragment classes reaching the final message. This
addresses both, plus the flush-path question:
Non-acp tool-call XML fragments (the main gap). ,
</tool_calls>, (and
antml:-namespaced / JSON-escapedforms) matched nothing in the filter and always passed through. Added a
containsToolCallXmlFragmentdetector for this family and a once-per-streamwarn in the server streaming loop, so a "fake completion" (model wrote the
tool call as text, no
tool_useblock) is attributable inbili.log.Warn-only, not stripped — these fragments are indistinguishable from
legitimate prose discussing tool-call code (the fix: tag-echo filter strips truncated tag fragments (#294) #295 review deliberately did
not strip them; stripping would corrupt real content).
Close-side truncated acp tags.
TRUNC_OPENonly handled the open side(
<acp …); a truncated close (</acp,</acp …) held atstream end was re-emitted verbatim. Added
TRUNC_CLOSEmirroringTRUNC_OPEN(strip at flush + drop in the streaming filter), and extendedPARTIAL_TAIL/ thedefinitecheck so the close side is held instead ofleaking.
Regression fix. A dangling
RENDER_TAG_DETECTreference (left by anearlier uncommitted refactor) broke
tsc; restored the constant.On the "TAG_OPEN_CAP flush path" question (#361 ask 3)
A truncated open with attributes (
<acp …) is already dropped at flush byTRUNC_OPEN— that path works. The residue that survived was the close side(fixed above) and the non-acp tool-call fragments (now warned, ask 1/2). A bare
<acp(no space) remains an intentional conservative pass-through (coveredby existing tests) and is not changed here.
Pre-flight
npm run typecheck— exit 0npm test— 835/835 pass (833 baseline + 2 new; 1 previously-failing assertion fixed)npm run build— exit 0No
versionbump (content branch).