refactor: update tool call parsing and stream tool sieve logic

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
CJACK
2026-04-28 01:39:32 +08:00
parent 516da04bcd
commit 63271aea8c
10 changed files with 205 additions and 4 deletions

View File

@@ -104,6 +104,13 @@ test('parseToolCalls keeps canonical XML examples inside DSML CDATA', () => {
assert.deepEqual(calls[0].input, { path: 'notes.md', content });
});
test('parseToolCalls preserves simple inline markup inside CDATA as text', () => {
const payload = '<tool_calls><invoke name="Write"><parameter name="description"><![CDATA[<b>urgent</b>]]></parameter></invoke></tool_calls>';
const calls = parseToolCalls(payload, ['Write']);
assert.equal(calls.length, 1);
assert.equal(calls[0].input.description, '<b>urgent</b>');
});
test('parseToolCalls recovers when CDATA never closes inside a valid wrapper', () => {
const payload = '<tool_calls><invoke name="Write"><parameter name="content"><![CDATA[hello world</parameter></invoke></tool_calls>';
const calls = parseToolCalls(payload, ['Write']);
@@ -174,6 +181,13 @@ test('parseToolCalls treats CDATA item-only body as array', () => {
]);
});
test('parseToolCalls treats single-item CDATA body as array', () => {
const payload = '<tool_calls><invoke name="TodoWrite"><parameter name="todos"><![CDATA[<item>one</item>]]></parameter></invoke></tool_calls>';
const calls = parseToolCalls(payload, ['TodoWrite']);
assert.equal(calls.length, 1);
assert.deepEqual(calls[0].input.todos, ['one']);
});
test('parseToolCalls treats CDATA object fragment as object', () => {
const fragment = '<question><![CDATA[Pick one]]></question><options><item><label><![CDATA[A]]></label></item><item><label><![CDATA[B]]></label></item></options>';
const payload = `<tool_calls><invoke name="AskUserQuestion"><parameter name="questions"><![CDATA[${fragment}]]></parameter></invoke></tool_calls>`;
@@ -400,6 +414,31 @@ test('sieve emits tool_calls when DSML tag spans multiple chunks', () => {
assert.equal(finalCalls[0].name, 'read_file');
});
test('sieve emits tool_calls when fullwidth DSML prefix variant spans multiple chunks', () => {
const events = runSieve(
[
'<DSML|tool',
'_calls>\n',
'<|DSML|invoke name="Bash">\n',
'<|DSML|parameter name="command"><![CDATA[ls -la /Users/aq/Desktop/myproject/ds2api/]]></|DSML|parameter>\n',
'<|DSML|parameter name="description"><![CDATA[List project root contents]]></|DSML|parameter>\n',
'</|DSML|invoke>\n',
'<|DSML|invoke name="Bash">\n',
'<|DSML|parameter name="command"><![CDATA[cat /Users/aq/Desktop/myproject/ds2api/package.json 2>/dev/null || echo "No package.json found"]]></|DSML|parameter>\n',
'<|DSML|parameter name="description"><![CDATA[Check for existing package.json]]></|DSML|parameter>\n',
'</|DSML|invoke>\n',
'</|DSML|tool_calls>',
],
['Bash'],
);
const leakedText = collectText(events);
const finalCalls = events.filter((evt) => evt.type === 'tool_calls').flatMap((evt) => evt.calls || []);
assert.equal(leakedText, '');
assert.equal(finalCalls.length, 2);
assert.equal(finalCalls[0].name, 'Bash');
assert.equal(finalCalls[1].name, 'Bash');
});
test('sieve keeps long XML tool calls buffered until the closing tag arrives', () => {
const longContent = 'x'.repeat(4096);
const splitAt = longContent.length / 2;