refactor: allow and preserve empty tool parameter values while updating sieve to release malformed XML as text

This commit is contained in:
CJACK
2026-05-10 01:05:18 +08:00
parent ddd42e532e
commit 740a78ad5a
13 changed files with 185 additions and 88 deletions

View File

@@ -155,6 +155,20 @@ test('parseToolCalls parses arbitrary-prefixed tool tags', () => {
assert.deepEqual(calls[0].input, { file_path: '/tmp/input.txt' });
});
test('parseToolCalls allows all-empty parameter payloads', () => {
const payload = `<TDSMLtool_calls>
<TDSMLinvoke name="TaskOutput">
<TDSMLparameter name="task_id"></TDSMLparameter>
<TDSMLparameter name="block"></TDSMLparameter>
<TDSMLparameter name="timeout"></TDSMLparameter>
</TDSMLinvoke>
</TDSMLtool_calls>`;
const calls = parseToolCalls(payload, ['TaskOutput']);
assert.equal(calls.length, 1);
assert.equal(calls[0].name, 'TaskOutput');
assert.deepEqual(calls[0].input, { task_id: '', block: '', timeout: '' });
});
test('parseToolCalls ignores bare hyphenated tool_calls lookalike', () => {
const payload = '<tool-calls><invoke name="Bash"><parameter name="command">pwd</parameter></invoke></tool-calls>';
const calls = parseToolCalls(payload, ['Bash']);
@@ -509,6 +523,26 @@ test('sieve emits tool_calls for arbitrary-prefixed tool tags', () => {
assert.equal(text.includes('💥'), false);
});
test('sieve emits all-empty arbitrary-prefixed tool tags without leaking text', () => {
const payload = [
'<TDSMLtool_calls>\n',
' <TDSMLinvoke name="TaskOutput">\n',
' <TDSMLparameter name="task_id"></TDSMLparameter>\n',
' <TDSMLparameter name="block"></TDSMLparameter>\n',
' <TDSMLparameter name="timeout"></TDSMLparameter>\n',
' </TDSMLinvoke>\n',
'</TDSMLtool_calls>',
].join('');
for (const chunks of [[payload], payload.match(/.{1,8}/gs)]) {
const events = runSieve(chunks, ['TaskOutput']);
const finalCalls = events.filter((evt) => evt.type === 'tool_calls').flatMap((evt) => evt.calls || []);
assert.equal(finalCalls.length, 1);
assert.equal(finalCalls[0].name, 'TaskOutput');
assert.deepEqual(finalCalls[0].input, { task_id: '', block: '', timeout: '' });
assert.equal(collectText(events), '');
}
});
test('sieve emits tool_calls for extra leading less-than DSML tags without leaking prefix', () => {
const events = runSieve([
'<<|DSML|tool_calls>\n',
@@ -847,7 +881,7 @@ test('sieve keeps embedded invalid tool-like json as normal text to avoid stream
assert.equal(leakedText.toLowerCase().includes('tool_calls'), true);
});
test('sieve passes malformed executable-looking XML through as text', () => {
test('sieve releases malformed executable-looking XML wrappers as text', () => {
const chunk = '<tool_calls><invoke name="read_file"><param>{"path":"README.MD"}</param></invoke></tool_calls>';
const events = runSieve([chunk], ['read_file']);
const leakedText = collectText(events);