From 1fe1240240a0d88ba7847b101144154b56a56f86 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Mon, 30 Mar 2026 15:59:34 +0800 Subject: [PATCH] fix(js): prevent XML wrapper attribute tool_calls scan loop --- .../helpers/stream-tool-sieve/parse_payload.js | 5 ++++- tests/node/stream-tool-sieve.test.js | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/internal/js/helpers/stream-tool-sieve/parse_payload.js b/internal/js/helpers/stream-tool-sieve/parse_payload.js index e658be5..c480033 100644 --- a/internal/js/helpers/stream-tool-sieve/parse_payload.js +++ b/internal/js/helpers/stream-tool-sieve/parse_payload.js @@ -102,7 +102,10 @@ function extractToolCallObjects(text) { const obj = extractJSONObjectFrom(raw, start); if (obj.ok) { out.push(raw.slice(start, obj.end).trim()); - offset = obj.end; + // Ensure forward progress even when the matched keyword is outside + // the extracted JSON object (e.g. closing XML wrapper tags containing + // "tool_calls" after an earlier JSON arguments object). + offset = Math.max(obj.end, idx + matched.length); idx = -1; break; } diff --git a/tests/node/stream-tool-sieve.test.js b/tests/node/stream-tool-sieve.test.js index 8f8a2bd..e53086e 100644 --- a/tests/node/stream-tool-sieve.test.js +++ b/tests/node/stream-tool-sieve.test.js @@ -227,6 +227,24 @@ test('sieve flushes incomplete captured XML tool blocks without leaking raw tags assert.equal(leakedText.includes(' { + const events = runSieve( + [ + '前置正文H。', + 'read_file{"path":"README.MD"}', + '后置正文I。', + ], + ['read_file'], + ); + const leakedText = collectText(events); + const hasToolCall = events.some((evt) => evt.type === 'tool_calls' && evt.calls?.length > 0); + assert.equal(hasToolCall, true); + assert.equal(leakedText.includes('前置正文H。'), true); + assert.equal(leakedText.includes('后置正文I。'), true); + assert.equal(leakedText.includes(''), false); + assert.equal(leakedText.includes(''), false); +}); + test('sieve still intercepts large tool json payloads over previous capture limit', () => { const large = 'a'.repeat(9000); const payload = `{"tool_calls":[{"name":"read_file","input":{"path":"${large}"}}]}`;