tool parser: streamed arguments keep a file that lists the closing tags (#5040, #4701)

Approach from PR #5040: _close_verdict holds while </tool_call> is still arriving, and the streaming scan of a body whose end is known runs as final. #4701 (final call) and #4702 (longer code fence) are already fixed on the base; the corpus gets the four 4701 specimens (PR #4762 and #5040 fixtures) and runs them whole, in pieces and cut at every byte, with streamed arguments on and off.
This commit is contained in:
Niko1221 committed 2026-11-04 10:37:34 +01:00
1 parent ff36fa9282
commit 3bbf0f399e
2 files changed
+15 -2

No files matched your search

+6 -2
View File
@@ -794,7 +794,10 @@ def _close_verdict(after: str, final: bool) -> str:
text decide, so a call is never held longer than that."""
m = re.match(r"(?:</function>)?\s*</tool_call>", after)
if not m:
return "end" # only `</function>` so far, or the call is cut: as before
tail = after[len(FUNC_END):].lstrip() if after.startswith(FUNC_END) else after
if not final and CALL_END.startswith(tail):
return "hold" # #5040: `</tool_call>` is still arriving: a streamed view must not end the value yet
return "end" # the call is cut, or it has no wrapper: as before
rest = after[m.end():].lstrip()
if not rest:
return "end" if final else "hold" # the usual call: the turn ends right after it
@@ -1861,9 +1864,10 @@ class OutputParser:
if self.stream_tools:
if i >= 0:
whole, self.buf = self.buf, self.buf[:i] # scan only the body
final, self._final = self._final, True # #5040: its end is known, so no closing tag is still arriving
out += self._scan()
out += self._close_scan()
self.buf = whole
self.buf, self._final = whole, final
else:
out += self._scan()
if i < 0 or drop is None:
+9
View File
@@ -105,6 +105,15 @@ add("4078-heredoc", qwen("bash", command=HEREDOC), [("bash", {"command": HEREDOC
add("4078-call-format-in-a-file", qwen("write_file", path="DOC.md", content="Close a call with </function> and then </tool_call>.\nlike this."),
[("write_file", {"path": "DOC.md", "content": "Close a call with </function> and then </tool_call>.\nlike this."})],
source="#210: a file that documents the format")
CLOSE_DOC = ("# Tool calls\n\nThe closing tags, in order:\n\n</parameter>\n</function>\n</tool_call>\n\nNothing may follow the last one.")
add("4701-closing-tags-in-a-file", "I will save the doc.\n\n" + qwen("write_file", path="TOOLS.md", content=CLOSE_DOC),
[("write_file", {"path": "TOOLS.md", "content": CLOSE_DOC})], content="I will save the doc.", source="#4701, #5040: streamed arguments too")
add("4701-closing-tags-in-a-file-recover", qwen("write_file", path="TOOLS.md", content=CLOSE_DOC),
[("write_file", {"path": "TOOLS.md", "content": CLOSE_DOC})], recover=True, source="#4701, recover on")
add("4701-text-after-a-whole-call", qwen("read", path="a.txt") + "\n\nThat is the read.", [("read", {"path": "a.txt"})],
content="That is the read.", source="#4701: with no later close, the first one is the end")
add("4701-two-calls-with-text-between", qwen("read", path="a.txt") + "\n\nAnd the next:\n" + qwen("read", path="b.txt"),
[("read", {"path": "a.txt"}), ("read", {"path": "b.txt"})], content="And the next:", source="#4701: a later call is not a later close")
add("4077-stray-think-line", "The user wants a file with a literal.\n" + STRAY_VALUE + "\nSo I write it.\n</think>\n\n" +
qwen("write_file", path="out.txt", content=STRAY_VALUE),
[("write_file", {"path": "out.txt", "content": STRAY_VALUE})], thinking=True,