From 6efba7b2e40efb41a04aa7eaeccff087f61105d3 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Mon, 30 Mar 2026 12:51:33 +0800 Subject: [PATCH] fix(js): avoid false tool-call capture on plain tool_calls prose --- internal/js/helpers/stream-tool-sieve/sieve.js | 6 +++++- tests/node/stream-tool-sieve.test.js | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/internal/js/helpers/stream-tool-sieve/sieve.js b/internal/js/helpers/stream-tool-sieve/sieve.js index 3250c86..23e477d 100644 --- a/internal/js/helpers/stream-tool-sieve/sieve.js +++ b/internal/js/helpers/stream-tool-sieve/sieve.js @@ -197,7 +197,11 @@ function findToolSegmentStart(state, s) { } const keyIdx = bestKeyIdx; const start = s.slice(0, keyIdx).lastIndexOf('{'); - let candidateStart = start >= 0 ? start : keyIdx; + if (start < 0) { + offset = keyIdx + matchedKeyword.length; + continue; + } + let candidateStart = start; // If the keyword matched inside an XML tag (e.g. "tool_calls" in ""), // back up past the '<' to capture the full tag. if (candidateStart > 0 && s[candidateStart - 1] === '<') { diff --git a/tests/node/stream-tool-sieve.test.js b/tests/node/stream-tool-sieve.test.js index 33b666e..feff59a 100644 --- a/tests/node/stream-tool-sieve.test.js +++ b/tests/node/stream-tool-sieve.test.js @@ -252,6 +252,18 @@ test('sieve keeps plain text intact in tool mode when no tool call appears', () assert.equal(leakedText, '你好,这是普通文本回复。请继续。'); }); +test('sieve does not start capture on plain "tool_calls" prose without opening json brace', () => { + const events = runSieve( + ['前置。', '这里提到 tool_calls 只是解释,不是调用。', '后置。'], + ['read_file'], + ); + const leakedText = collectText(events); + const hasToolCall = events.some((evt) => evt.type === 'tool_calls' && evt.calls?.length > 0); + assert.equal(hasToolCall, false); + assert.equal(leakedText.includes('tool_calls'), true); + assert.equal(leakedText, '前置。这里提到 tool_calls 只是解释,不是调用。后置。'); +}); + test('sieve emits unknown tool payload (no args) as executable tool call', () => { const events = runSieve( ['{"tool_calls":[{"name":"not_in_schema"}]}', '后置正文G。'],