From 184a3d1e4eb0d53ce8c01c56842b5530a106ac53 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Fri, 20 Mar 2026 02:16:37 +0800 Subject: [PATCH] Sync Node tool-call parsing with aggressive fenced/mixed policy --- .../js/helpers/stream-tool-sieve/parse.js | 50 ++++++++++--------- tests/node/stream-tool-sieve.test.js | 12 +++-- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/internal/js/helpers/stream-tool-sieve/parse.js b/internal/js/helpers/stream-tool-sieve/parse.js index 6e6ff7d..22d11d1 100644 --- a/internal/js/helpers/stream-tool-sieve/parse.js +++ b/internal/js/helpers/stream-tool-sieve/parse.js @@ -2,10 +2,8 @@ const { toStringSafe, - looksLikeToolExampleContext, } = require('./state'); const { - stripFencedCodeBlocks, buildToolCallCandidates, parseToolCallsPayload, parseMarkupToolCalls, @@ -38,16 +36,13 @@ function parseToolCalls(text, toolNames) { function parseToolCallsDetailed(text, toolNames) { const result = emptyParseResult(); - if (!toStringSafe(text)) { + const normalized = toStringSafe(text); + if (!normalized) { return result; } - const sanitized = stripFencedCodeBlocks(text); - if (!toStringSafe(sanitized)) { - return result; - } - result.sawToolCallSyntax = looksLikeToolCallSyntax(sanitized); + result.sawToolCallSyntax = looksLikeToolCallSyntax(normalized); - const candidates = buildToolCallCandidates(sanitized); + const candidates = buildToolCallCandidates(normalized); let parsed = []; for (const c of candidates) { parsed = parseToolCallsPayload(c); @@ -63,9 +58,9 @@ function parseToolCallsDetailed(text, toolNames) { } } if (parsed.length === 0) { - parsed = parseMarkupToolCalls(sanitized); + parsed = parseMarkupToolCalls(normalized); if (parsed.length === 0) { - parsed = parseTextKVToolCalls(sanitized); + parsed = parseTextKVToolCalls(normalized); if (parsed.length === 0) { return result; } @@ -90,22 +85,29 @@ function parseStandaloneToolCallsDetailed(text, toolNames) { if (!trimmed) { return result; } - if (trimmed.includes('```')) { - return result; - } - if (looksLikeToolExampleContext(trimmed)) { - return result; - } result.sawToolCallSyntax = looksLikeToolCallSyntax(trimmed); - let parsed = parseToolCallsPayload(trimmed); + const candidates = buildToolCallCandidates(trimmed); + let parsed = []; + for (const c of candidates) { + parsed = parseToolCallsPayload(c); + if (parsed.length === 0) { + parsed = parseMarkupToolCalls(c); + } + if (parsed.length === 0) { + parsed = parseTextKVToolCalls(c); + } + if (parsed.length > 0) { + break; + } + } if (parsed.length === 0) { parsed = parseMarkupToolCalls(trimmed); - } - if (parsed.length === 0) { - parsed = parseTextKVToolCalls(trimmed); - } - if (parsed.length === 0) { - return result; + if (parsed.length === 0) { + parsed = parseTextKVToolCalls(trimmed); + if (parsed.length === 0) { + return result; + } + } } result.sawToolCallSyntax = true; diff --git a/tests/node/stream-tool-sieve.test.js b/tests/node/stream-tool-sieve.test.js index 7c703a1..d4b5481 100644 --- a/tests/node/stream-tool-sieve.test.js +++ b/tests/node/stream-tool-sieve.test.js @@ -91,7 +91,9 @@ test('parseToolCalls supports fenced json and function.arguments string payload' '```', ].join('\n'); const calls = parseToolCalls(text, ['read_file']); - assert.equal(calls.length, 0); + assert.equal(calls.length, 1); + assert.equal(calls[0].name, 'read_file'); + assert.equal(calls[0].input.path, 'README.md'); }); test('parseToolCalls parses text-kv fallback payload', () => { @@ -122,19 +124,19 @@ test('parseToolCalls parses multiple text-kv fallback payloads', () => { assert.equal(calls[1].name, 'bash'); }); -test('parseStandaloneToolCalls only matches standalone payload and ignores mixed prose', () => { +test('parseStandaloneToolCalls parses mixed prose payload', () => { const mixed = '这里是示例:{"tool_calls":[{"name":"read_file","input":{"path":"README.MD"}}]},请勿执行。'; const standalone = '{"tool_calls":[{"name":"read_file","input":{"path":"README.MD"}}]}'; const mixedCalls = parseStandaloneToolCalls(mixed, ['read_file']); const standaloneCalls = parseStandaloneToolCalls(standalone, ['read_file']); - assert.equal(mixedCalls.length, 0); + assert.equal(mixedCalls.length, 1); assert.equal(standaloneCalls.length, 1); }); -test('parseStandaloneToolCalls ignores fenced code block tool_call examples', () => { +test('parseStandaloneToolCalls parses fenced code block tool_call payload', () => { const fenced = ['```json', '{"tool_calls":[{"name":"read_file","input":{"path":"README.MD"}}]}', '```'].join('\n'); const calls = parseStandaloneToolCalls(fenced, ['read_file']); - assert.equal(calls.length, 0); + assert.equal(calls.length, 1); });