Sync Node tool-call parsing with aggressive fenced/mixed policy

This commit is contained in:
CJACK.
2026-03-20 02:16:37 +08:00
parent c4ec14f49a
commit 184a3d1e4e
2 changed files with 33 additions and 29 deletions

View File

@@ -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;

View File

@@ -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);
});