Repository navigation
Conversation
|
Pushed What it adds on top of the DSML stripping: Execute instead of ignoring —
Retry instead of stopping — a truncated marker at the end of the turn ( Stream observability ( Tests: Note: GitHub did not create a check suite for |
上一版(lsdefine#853)只覆盖 <code_run> 家族且以"检测到即重试"为主, 实际运行中仍有两类问题:①反引号/围栏里被讲解的标签会被误判执行; ②<ask_user><question>..</question></ask_user> 这类裸子标签形态、以及 "调用后跟一段说明"的形态仍被漏检。 本次改为解析优先 + 全文级检测: llmcore.py - _strip_dsml_tags:容错 | | ‖ 单/双/三分隔符与破碎开闭标签 - DsmlStreamFilter:流式跨 chunk 剥离(chat_completions / responses 两条路径) - _code_candidates/_xml_code_calls:属性式、体内参数式、压平单行式、JSON 体 - _bare_tag_calls/_bare_child_args:工具名+裸子标签参数;参数体必须纯净标签对, 正文 HTML 片段与裸脚本不会被误判 - _looks_like_prose:中文散文判为"被讨论的标签"→否决 - _try_parse_tool_args:arguments 侧同样剥标记(上游 lsdefine#846-B) - leaked_markup_kind/incomplete + salvaged_call_problem:可完整解析才执行, 截断/语法错/空参数一律转重试 ga.py - 尾部 50 字检测改为全文级:能解析就救回执行,残缺则不执行并要求重生成 实测:36 项验收用例全绿;50 条真实泄漏日志逐块对拍改前/改后行为完全一致(零退步); 对照 lsdefine#838 修复其两处误执行与两处漏检。
…variants, chunk splits) DeepSeek occasionally emits its DSML tool-call markup as plain content text instead of a real tool_calls delta (reported in lsdefine#804, partially addressed by the closed PR lsdefine#805 and by f7e4cc5 in ga.py). The previous content filter still missed three cases, so `</...DSML parameter>` (and friends) reached the agent loop as assistant text, no tool call was registered, and the turn stalled. 1. The pattern required '<' to be immediately followed by the DSML token, so closing tags ('</...') never matched, and the double full-width-pipe variant ('|P||P|DSML|P||P|') only matched from its second pipe, leaving a stray '<' / '</' behind. 2. Stripping ran per SSE chunk, so a marker split across chunks could not be seen at all. 3. The non-streaming _parse_openai_json path never stripped markers. Changes: - one ASCII-safe pattern (built from chr()) covering optional '/', 1-2 pipes of any of | / U+FF5C / U+2016, optional whitespace inside the tag, and tag payloads. - _DsmlStreamFilter holds back a trailing unterminated '<...' (<=64 chars) so markers split across SSE chunks are still removed. - _DSML_LEAK latch + MockResponse.dsml_leak so callers can tell that a leak happened even when the payload text survives; reset at the start of every parse. - both non-streaming JSON paths strip as well. Verified with 27 unit assertions on the real module: known variants, chunk-split markers, SSE end-to-end, and healthy tool-call regressions. Co-Authored-By: GenericAgent <bot@gaagent.ai>
Complements f7e4cc5, which only retried when the content ended with the exact literal `</|DSML| calls>`. That check misses * a leak anywhere except the very end of the turn, * markers that use `||` (double full-width) or full-width/heavy pipes, * a leaked closing tag of a non-`calls` marker such as `</||DSML|| parameter>` - the marker that actually stalls the loop: the raw text is fed back into the next request and the model keeps regenerating broken DSML instead of calling a tool. `llmcore` now latches whether the DSML filter had to remove anything while parsing a response (`response.dsml_leak`), so `do_no_tool` can retry on a positive signal instead of guessing from the text tail. The cheap tail check is kept as a fallback for responses produced by other code paths. Refs lsdefine#804 (closed), lsdefine#805 (closed unmerged), lsdefine#836 (open). Co-Authored-By: GenericAgent <bot@gaagent.ai>
Regression tests for lsdefine#804 / lsdefine#805 / lsdefine#836: variant marker spelling, closing tags of non-`calls` markers, markers split across stream chunks (the exact `</||DSML|| parameter>` case), the non-streaming JSON path, and the latch that `ga.do_no_tool` relies on. Co-Authored-By: GenericAgent <bot@gaagent.ai>
…define#837) The model sometimes ends a turn with the call written as literal markup in the text channel ("<code_run\">" truncated, or a complete "<code_run><parameter ...>" block). No tool_call comes back, so the loop treats the turn as a finished answer and the agent stops silently (session 423177, 2026-10-17 15:34). * llmcore.split_text_tool_calls(): recover trailing markup only -- quoted samples earlier in the reply are left alone -- returning (calls, partial) * agent_loop: execute recovered calls like real tool calls instead of no_tool * ga.do_no_tool: retry once when the marker is truncated mid-tag * responses stream: parse the response.completed output snapshot, handle output_item.done, and flag incomplete / terminal-event-less streams instead of letting a torn turn look like a finished answer Tests: frontends/tests/test_text_channel_tool_call.py (9 cases) Co-Authored-By: GenericAgent <bot@gaagent.ai>
…bug) The lsdefine#837 fix was inert: agent_runner_loop built the known-tool set with {entry["name"] for entry in tools_schema}, but the shipped assets/tools_schema.json uses the OpenAI form {"type": "function", "function": {"name": ...}} -- every lookup returned None, the set came out empty, and split_text_tool_calls returns ([], False) immediately for an empty set. So a leaked block was never recovered and the raw markup reached the user; only the truncated-marker retry worked, because that path lives in do_no_tool. * agent_loop: add tool_names_from_schema(), accepting both the flat and the OpenAI nested form, and use it for the recovery call. * ga.py: do_no_tool's recovered-call branch used to yield "already executed" and return without dispatching anything. It now really dispatches each recovered call (it is only reached when the loop did not already handle it) and honours should_exit. * tests: schema name extraction (nested, flat, shipped file) and a leaked block whose body ends with a "python" language tag -- the exact 2026-10-17 shape. Co-Authored-By: GenericAgent <bot@gaagent.ai>
Follow-up to the text-channel recovery path: three shapes still fell through
and left the loop stopped with no tool call at all.
* <script>...</script> used as the parameter container instead of
<parameter name="script">, with the remaining parameters alongside it
* stray closing tags after the call ('</invoke>', '</function>') made the
call look like prose, so it was neither executed nor retried
* a stream truncated inside a value ('<parameter name="timeout">180') was
not even reported as partial, so do_no_tool just exited
Also stop treating *quoted* markup as a call: markup inside inline code or a
fenced block that is followed by prose is a sample, not an execution.
Co-Authored-By: GenericAgent <bot@gaagent.ai>
The no-tool guard matched the marker anywhere in the last 200 chars, so a reply that *quotes* it -- writing docs, answering a question about the bug, or pasting the regex from an upstream commit -- was rejected as a leaked tool call and its turn was thrown away. Strip code spans (fenced blocks followed by prose, inline `code`) before matching, so only bare markup counts. Co-Authored-By: GenericAgent <bot@gaagent.ai>
bc7cf81 to
2b0e44a
Compare
Fixes #837.
Follow-up to the closed issue #804 (the original "DSML markers leak into assistant content" report) and the closed, unmerged PR #805 (the first per-chunk content filter), and complementary to merged workaround f7e4cc5 and the still-open PR #836. Those cover the symptom end (exact trailer retry / 200-char tail heuristic); this PR fixes the marker matching itself, which is what makes
</||DSML|| parameter>survive all of them.What this fixes
Three holes, all in
llmcore.py, plus a guard that is no longer tied to one hardcoded suffix inga.py:Closing tags and pipe variants were never matched. The marker regex required
<to be immediately followed by the pipe+DSMLsequence (so</...>closing tags escaped) and only accepted a single|(U+FF5C), not|,||or‖. Replaced with one variant-agnostic marker pattern (optional/,[||‖]{1,2}, optional whitespace around theDSMLtoken).Markers split across stream chunks were never seen. Filtering ran per chunk behind a
"DSML" not in textfast path, so"hello<|" + "|DSML||tool_calls>world"matched nothing. Added a small stateful filter (_DsmlStreamFilter) that holds a trailing partial<...back and re-joins it with the next chunk, so a split marker is matched exactly once and in order; the fast path remains for the common case.Non-streaming responses were never filtered (
_parse_openai_json), and neither were the text blocks of a streamed response in some paths.ga.do_no_toolnow retries whenllmcorelatches a leak (response.dsml_leak) - a positive signal, so a leak anywhere in the turn (not just at the very end) and any marker family triggers a clean regeneration - while the cheap tail check stays as a fallback for responses produced elsewhere.Tests
frontends/tests/test_dsml_marker_strip.py(11 tests, runs in the existing desktop CI job, no network/API key): marker spelling variants, closing tags ofinvoke/parameter/tool_callsfamilies, cross-chunk splits (the exact</||DSML|| parameter>case), the non-streaming path, the latch contractga.pydepends on, and non-regression cases (ordinary text untouched; the payload between two leaked markers is preserved).Risk
Only text content is touched, and only when a DSML-shaped marker is actually present; real tool calls are parsed by the provider adapter before this path. Leak-free text takes the same fast path as before (a single
"DSML" in textcheck). The filter holds at most a trailing<...fragment back between chunks, and flushes at stream end, so no content can be lost.Update 2026-10-27 — rebased + two more real-world variants
Rebased onto
main@e4d956f(30 commits, incl.a9a36b4), so the PR appliescleanly again; the one conflict in
ga.pywas resolved by keeping this PR'stext-channel recovery block and dropping the now-deleted
plan_modeguard.Two new commits, both driven by sessions that still stalled after the
above fixes:
llmcore: recover more text-channel call variants (#837)— a call emitted as<code_run><script>...</script><parameter name="type">python</parameter></code_run></invoke></function>was rejected because (a)
<script>was not accepted as a parameter container,(b) anything after the closing tag made it "not a call", (c) a call truncated
mid-value was not flagged as partial, so no retry happened. Now: container
names are parsed and mapped to declared parameter names, a tail of stray
closing tags is tolerated, and a value-less/truncated call returns
partialso the turn is retried instead of silently ending.
ga: don't treat quoted DSML markers as a leak— the guard matched the markeranywhere in the last 200 chars, so a reply that quotes it (writing docs,
answering "what is this bug", pasting the upstream regex) was rejected as a
leaked tool call. Code spans/fenced blocks that are followed by prose are now
stripped before matching; a fence at the very end still counts, because that
is exactly where a leaked call hides.
Verification: the two dead sessions from 2026-10-27 (
model_responses_353300.txt,17:08 and 17:59 — both followed by the user typing "continue") now parse into a
code_runcall, with 6 new tests added (frontends/tests/30 passed).Full
frontends/tests/on this branch: 189 passed, 1 pre-existing failure(
test_conductor_model_routing, also fails onmain@e4d956f).