revert: replace fullwidth pipe | with halfwidth | in DSML tool markup

PR #460 introduced fullwidth pipe characters (|) in DSML tool call formatting
to improve parsing robustness, but models exposed to these fullwidth pipes in
system prompts exhibit significantly higher rates of tool output hallucinations.
Reverting to halfwidth pipes (|) drastically reduces tokenizer/perplexity-driven
hallucinations while retaining the existing confusable-hardening in the parser.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
CJACK
2026-05-10 15:18:54 +08:00
parent 3beb31309f
commit cee8757d14
45 changed files with 725 additions and 342 deletions

View File

@@ -18,9 +18,9 @@ test('chat history strict parser merges current input file placeholder', async (
content: 'Continue from the latest state in the attached DS2API_HISTORY.txt context. Treat it as the current working state and answer the latest user request directly.',
}],
history_text: [
'<begin▁of▁sentence>',
'<User>hello',
'<Assistant>hi<end▁of▁sentence>',
'<|begin▁of▁sentence|>',
'<|User|>hello',
'<|Assistant|>hi<|end▁of▁sentence|>',
].join(''),
};
@@ -43,9 +43,9 @@ test('chat history strict parser inserts history after system messages', async (
{ role: 'user', content: 'latest' },
],
history_text: [
'<begin▁of▁sentence>',
'<User>old',
'<Assistant>done<end▁of▁sentence>',
'<|begin▁of▁sentence|>',
'<|User|>old',
'<|Assistant|>done<|end▁of▁sentence|>',
].join(''),
};

View File

@@ -646,7 +646,7 @@ test('parseChunkForContent strips citation and reference markers from fragment c
test('parseChunkForContent strips leaked thought control markers from content', () => {
const chunk = {
p: 'response/content',
v: '<▁of▁thought>A<| of_thought |>B<| end_of_thought |>C',
v: '<|▁of▁thought|>A<| of_thought |>B<| end_of_thought |>C',
};
const parsed = parseChunkForContent(chunk, false, 'text');
assert.equal(parsed.finished, false);

View File

@@ -58,7 +58,7 @@ test('parseToolCalls parses DSML shell as XML-compatible tool call', () => {
});
test('parseToolCalls tolerates fullwidth closing slash in DSML wrapper', () => {
const payload = '<DSMLtool_calls><DSMLinvoke name="execute_code"><DSMLparameter name="code"><![CDATA[print("hi")]]></DSMLparameter></DSMLinvoke><DSMLtool_calls>';
const payload = '<|DSML|tool_calls><|DSML|invoke name="execute_code"><|DSML|parameter name="code"><![CDATA[print("hi")]]></|DSML|parameter></|DSML|invoke><DSML|tool_calls>';
const calls = parseToolCalls(payload, ['execute_code']);
assert.equal(calls.length, 1);
assert.equal(calls[0].name, 'execute_code');
@@ -66,7 +66,7 @@ test('parseToolCalls tolerates fullwidth closing slash in DSML wrapper', () => {
});
test('parseToolCalls tolerates sentencepiece separator and fullwidth terminator', () => {
const payload = '<DSML▁tool_calls><DSML▁invoke▁name="execute_code"><DSML▁parameter▁name="code"><![CDATA[print("hi")]]></DSML▁parameter></DSML▁invoke></DSML▁tool_calls';
const payload = '<|DSML▁tool_calls|><|DSML▁invoke▁name="execute_code"><|DSML▁parameter▁name="code"><![CDATA[print("hi")]]></|DSML▁parameter></|DSML▁invoke></|DSML▁tool_calls';
const calls = parseToolCalls(payload, ['execute_code']);
assert.equal(calls.length, 1);
assert.equal(calls[0].name, 'execute_code');
@@ -74,7 +74,7 @@ test('parseToolCalls tolerates sentencepiece separator and fullwidth terminator'
});
test('parseToolCalls tolerates fullwidth opening delimiter and Unicode attribute confusables', () => {
const payload = 'DSML tool_callsDSML invoke name“execute_code”DSML parameter name“code”<![CDATA[print("hi")]]>DSMLparameterDSMLinvokeDSMLtool_calls';
const payload = '|DSML tool_calls|DSML invoke name“execute_code”|DSML parameter name“code”<![CDATA[print("hi")]]>DSML|parameterDSML|invokeDSML|tool_calls';
const calls = parseToolCalls(payload, ['execute_code']);
assert.equal(calls.length, 1);
assert.equal(calls[0].name, 'execute_code');
@@ -162,14 +162,14 @@ test('parseToolCalls ignores camel-prefixed tool markup lookalike', () => {
});
test('parseToolCalls parses fullwidth DSML shell drift', () => {
const payload = `<tool_calls>
<invoke name="Read">
<parameter name="file_path"<![CDATA[/Users/aq/Desktop/myproject/Personal_Blog/README.md]]</parameter>
</invoke>
<invoke name="Read">
<parameter name="file_path"<![CDATA[/Users/aq/Desktop/myproject/Personal_Blog/index.html]]</parameter>
</invoke>
</tool_calls>`;
const payload = `<|tool_calls>
<|invoke name="Read">
<|parameter name="file_path"<![CDATA[/Users/aq/Desktop/myproject/Personal_Blog/README.md]]</|parameter>
</|invoke>
<|invoke name="Read">
<|parameter name="file_path"<![CDATA[/Users/aq/Desktop/myproject/Personal_Blog/index.html]]</|parameter>
</|invoke>
</|tool_calls>`;
const calls = parseToolCalls(payload, ['Read']);
assert.equal(calls.length, 2);
assert.equal(calls[0].name, 'Read');
@@ -179,20 +179,20 @@ test('parseToolCalls parses fullwidth DSML shell drift', () => {
});
test('parseToolCalls parses CJK-angle DSM drift', () => {
const payload = `<DSMtool_calls>
<DSMinvoke name="Bash">
<DSMparameter name="description">〈![CDATA[Show commits on local dev not on origin/dev]]〉〈/DSMparameter〉
<DSMparameter name="command">〈![CDATA[git log --oneline origin/dev..dev]]〉〈/DSMparameter〉
〈/DSMinvoke〉
<DSMinvoke name="Bash">
<DSMparameter name="description">〈![CDATA[Show commits on origin/dev not on local dev]]〉〈/DSMparameter〉
<DSMparameter name="command">〈![CDATA[git log --oneline dev..origin/dev]]〉〈/DSMparameter〉
〈/DSMinvoke〉
<DSMinvoke name="Bash">
<DSMparameter name="description">〈![CDATA[Check tracking branch status]]〉〈/DSMparameter〉
<DSMparameter name="command">〈![CDATA[git status -b --short]]〉〈/DSMparameter〉
〈/DSMinvoke〉
〈/DSMtool_calls〉`;
const payload = `<DSM|tool_calls>
<DSM|invoke name="Bash">
<DSM|parameter name="description"|>〈![CDATA[Show commits on local dev not on origin/dev]]〉〈/DSM|parameter〉
<DSM|parameter name="command"|>〈![CDATA[git log --oneline origin/dev..dev]]〉〈/DSM|parameter〉
〈/DSM|invoke〉
<DSM|invoke name="Bash">
<DSM|parameter name="description"|>〈![CDATA[Show commits on origin/dev not on local dev]]〉〈/DSM|parameter〉
<DSM|parameter name="command"|>〈![CDATA[git log --oneline dev..origin/dev]]〉〈/DSM|parameter〉
〈/DSM|invoke〉
<DSM|invoke name="Bash">
<DSM|parameter name="description"|>〈![CDATA[Check tracking branch status]]〉〈/DSM|parameter〉
<DSM|parameter name="command"|>〈![CDATA[git status -b --short]]〉〈/DSM|parameter〉
〈/DSM|invoke〉
〈/DSM|tool_calls〉`;
const calls = parseToolCalls(payload, ['Bash']);
assert.equal(calls.length, 3);
assert.equal(calls[0].name, 'Bash');
@@ -262,13 +262,13 @@ test('parseToolCalls parses arbitrary-prefixed tool tags', () => {
});
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 payload = `<T|DSML|tool_calls>
<T|DSML|invoke name="TaskOutput">
<T|DSML|parameter name="task_id"></T|DSML|parameter>
<T|DSML|parameter name="block"></T|DSML|parameter>
<T|DSML|parameter name="timeout"></T|DSML|parameter>
</T|DSML|invoke>
</T|DSML|tool_calls>`;
const calls = parseToolCalls(payload, ['TaskOutput']);
assert.equal(calls.length, 1);
assert.equal(calls[0].name, 'TaskOutput');
@@ -603,7 +603,7 @@ test('sieve emits tool_calls for DSML space-separator typo', () => {
});
test('sieve emits tool_calls for fullwidth closing slash and preserves suffix text', () => {
const input = '<DSMLtool_calls><DSMLinvoke name="execute_code"><DSMLparameter name="code"><![CDATA[print("hi")]]></DSMLparameter></DSMLinvoke><DSMLtool_calls> sao cụm này lại đc trả là 1 message';
const input = '<|DSML|tool_calls><|DSML|invoke name="execute_code"><|DSML|parameter name="code"><![CDATA[print("hi")]]></|DSML|parameter></|DSML|invoke><DSML|tool_calls> sao cụm này lại đc trả là 1 message';
const events = runSieve([input], ['execute_code']);
const text = collectText(events);
const finalCalls = events.filter((evt) => evt.type === 'tool_calls').flatMap((evt) => evt.calls || []);
@@ -614,7 +614,7 @@ test('sieve emits tool_calls for fullwidth closing slash and preserves suffix te
});
test('sieve emits tool_calls for sentencepiece separator and fullwidth terminator', () => {
const input = '<DSML▁tool_calls><DSML▁invoke▁name="execute_code"><DSML▁parameter▁name="code"><![CDATA[print("hi")]]></DSML▁parameter></DSML▁invoke></DSML▁tool_calls suffix';
const input = '<|DSML▁tool_calls|><|DSML▁invoke▁name="execute_code"><|DSML▁parameter▁name="code"><![CDATA[print("hi")]]></|DSML▁parameter></|DSML▁invoke></|DSML▁tool_calls suffix';
const events = runSieve([input], ['execute_code']);
const text = collectText(events);
const finalCalls = events.filter((evt) => evt.type === 'tool_calls').flatMap((evt) => evt.calls || []);
@@ -625,7 +625,7 @@ test('sieve emits tool_calls for sentencepiece separator and fullwidth terminato
});
test('sieve emits tool_calls for fullwidth opening delimiter and Unicode attribute confusables', () => {
const input = 'DSML tool_callsDSML invoke name“execute_code”DSML parameter name“code”<![CDATA[print("hi")]]>DSMLparameterDSMLinvokeDSMLtool_calls suffix';
const input = '|DSML tool_calls|DSML invoke name“execute_code”|DSML parameter name“code”<![CDATA[print("hi")]]>DSML|parameterDSML|invokeDSML|tool_calls suffix';
const events = runSieve([input], ['execute_code']);
const text = collectText(events);
const finalCalls = events.filter((evt) => evt.type === 'tool_calls').flatMap((evt) => evt.calls || []);
@@ -718,12 +718,12 @@ test('sieve emits tool_calls for arbitrary-prefixed tool tags', () => {
test('sieve emits tool_calls for CJK-angle DSM drift', () => {
const events = runSieve([
'<DSMtool_calls>\n',
'<DSMinvoke name="Bash">\n',
'<DSMparameter name="description">〈![CDATA[Check tracking branch status]]〉〈/DSMparameter〉\n',
'<DSMparameter name="command">〈![CDATA[git status -b --short]]〉〈/DSMparameter〉\n',
'〈/DSMinvoke〉\n',
'〈/DSMtool_calls〉',
'<DSM|tool_calls>\n',
'<DSM|invoke name="Bash">\n',
'<DSM|parameter name="description"|>〈![CDATA[Check tracking branch status]]〉〈/DSM|parameter〉\n',
'<DSM|parameter name="command"|>〈![CDATA[git status -b --short]]〉〈/DSM|parameter〉\n',
'〈/DSM|invoke〉\n',
'〈/DSM|tool_calls〉',
], ['Bash']);
const finalCalls = events.flatMap((evt) => (evt.type === 'tool_calls' ? evt.calls : []));
assert.equal(finalCalls.length, 1);
@@ -770,13 +770,13 @@ test('sieve emits tool_calls for ideographic-comma DSML drift', () => {
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>',
'<T|DSML|tool_calls>\n',
' <T|DSML|invoke name="TaskOutput">\n',
' <T|DSML|parameter name="task_id"></T|DSML|parameter>\n',
' <T|DSML|parameter name="block"></T|DSML|parameter>\n',
' <T|DSML|parameter name="timeout"></T|DSML|parameter>\n',
' </T|DSML|invoke>\n',
'</T|DSML|tool_calls>',
].join('');
for (const chunks of [[payload], payload.match(/.{1,8}/gs)]) {
const events = runSieve(chunks, ['TaskOutput']);
@@ -859,14 +859,14 @@ test('sieve preserves review body with alias mentions before real DSML tool call
const events = runSieve([
"Done reviewing the diff. Here's my analysis before we commit:\n\n",
'Summary of Changes\n',
'DSML wrapper variant support — recognize aliases (<dsml|tool_calls>, <|tool_calls>, <tool_calls>) alongside canonical <tool_calls> and <|DSML|tool_calls> wrappers.\n\n',
'DSML wrapper variant support — recognize aliases (<dsml|tool_calls>, <|tool_calls>) alongside canonical <tool_calls> and <|DSML|tool_calls> wrappers.\n\n',
'<|DSML|tool_calls>\n',
'<|DSML|invoke name="Bash">\n',
'<|DSML|parameter name="command"><![CDATA[git add docs/toolcall-semantics.md internal/toolstream/tool_sieve_xml.go]]></|DSML|parameter>\n',
'<|DSML|parameter name="description"><![CDATA[Stage all relevant changed files]]></|DSML|parameter>\n',
'</|DSML|invoke>\n',
'<|DSML|invoke name="Bash">\n',
'<|DSML|parameter name="command"><![CDATA[git commit -m "$(cat <<\'EOF\'\nfeat(toolstream): expand DSML wrapper detection\n\nSupport DSML wrapper aliases: <dsml|tool_calls>, <|tool_calls>, <tool_calls> alongside existing canonical wrappers.\nEOF\n)"]]></|DSML|parameter>\n',
'<|DSML|parameter name="command"><![CDATA[git commit -m "$(cat <<\'EOF\'\nfeat(toolstream): expand DSML wrapper detection\n\nSupport DSML wrapper aliases: <dsml|tool_calls> and <|tool_calls> alongside existing canonical wrappers.\nEOF\n)"]]></|DSML|parameter>\n',
'<|DSML|parameter name="description"><![CDATA[Create commit with all staged changes]]></|DSML|parameter>\n',
'</|DSML|invoke>\n',
'</|DSML|tool_calls>',
@@ -993,7 +993,7 @@ test('sieve emits tool_calls when DSML tag spans multiple chunks', () => {
test('sieve emits tool_calls when fullwidth DSML prefix variant spans multiple chunks', () => {
const events = runSieve(
[
'<DSML|tool',
'<|DSML|tool',
'_calls>\n',
'<|DSML|invoke name="Bash">\n',
'<|DSML|parameter name="command"><![CDATA[ls -la /Users/aq/Desktop/myproject/ds2api/]]></|DSML|parameter>\n',