File size: 6,853 Bytes
077865a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import { describe, it, expect } from 'vitest';
import {
  rescueInlineToolCalls,
  startsWithDialectMarker,
  couldBecomeDialectMarker,
  containsDialectMarker,
} from '../../lib/tool-call-rescue.js';

const TOOLS = new Set(['Read', 'Write', 'Bash', 'Grep', 'list_dir']);

describe('inline tool-call dialect rescue', () => {
  describe('Kimi/DeepSeek token dialect', () => {
    it('parses a well-formed functions.NAME:IDX call', () => {
      const text = '<|tool_calls_section_begin|><|tool_call_begin|>functions.Read:0<|tool_call_argument_begin|>{"file_path":"/tmp/x.txt"}<|tool_call_end|><|tool_calls_section_end|>';
      const r = rescueInlineToolCalls(text, TOOLS);
      expect(r.detected).toBe(true);
      expect(r.calls).toEqual([{ name: 'Read', arguments: '{"file_path":"/tmp/x.txt"}' }]);
      expect(r.cleanText).toBe('');
    });

    it('parses multiple calls and keeps surrounding prose as cleanText', () => {
      const text = 'Let me check.\n<|tool_call_begin|>functions.Read:0<|tool_call_argument_begin|>{"file_path":"/a"}<|tool_call_end|><|tool_call_begin|>functions.Grep:1<|tool_call_argument_begin|>{"pattern":"TODO"}<|tool_call_end|>';
      const r = rescueInlineToolCalls(text, TOOLS);
      expect(r.calls).toHaveLength(2);
      expect(r.calls![1]).toEqual({ name: 'Grep', arguments: '{"pattern":"TODO"}' });
      expect(r.cleanText).toBe('Let me check.');
    });

    it('reports detected-but-unparseable when the id token is degraded (no function name)', () => {
      // The live capture that motivated this module: an opaque id where
      // functions.NAME:IDX should be — there is no way to know the tool.
      const text = '<|tool_calls_section_begin|> <|tool_call_begin|> chatcmpl-tool-bde5fae954a11b1b <|tool_call_argument_begin|> {"file_path": "/private/tmp/demo-project/src"} <|tool_call_end|>';
      const r = rescueInlineToolCalls(text, TOOLS);
      expect(r.detected).toBe(true);
      expect(r.calls).toBeNull();
    });

    it('rejects a call naming a tool the request never declared', () => {
      const text = '<|tool_call_begin|>functions.DropTables:0<|tool_call_argument_begin|>{}<|tool_call_end|>';
      const r = rescueInlineToolCalls(text, TOOLS);
      expect(r.detected).toBe(true);
      expect(r.calls).toBeNull();
    });
  });

  describe('llama/groq <function=> dialect', () => {
    it('parses <function=NAME{json}</function> (the failed_generation shape)', () => {
      const text = '<function=Bash{"command": "npm run build"}</function>';
      const r = rescueInlineToolCalls(text, TOOLS);
      expect(r.detected).toBe(true);
      expect(r.calls).toEqual([{ name: 'Bash', arguments: '{"command": "npm run build"}' }]);
    });

    it('parses the variant with a closing > after the name', () => {
      const text = '<function=Read>{"file_path": "/tmp/a"}</function>';
      const r = rescueInlineToolCalls(text, TOOLS);
      expect(r.calls).toEqual([{ name: 'Read', arguments: '{"file_path": "/tmp/a"}' }]);
    });

    it('treats array-shaped arguments as unparseable (live Groq failure shape)', () => {
      const text = '<function=Bash [{"a": "npm run build"}, {"b": "/tmp"}]</function>';
      const r = rescueInlineToolCalls(text, TOOLS);
      expect(r.detected).toBe(true);
      expect(r.calls).toBeNull();
    });
  });

  describe('Qwen/Hermes <tool_call> XML dialect', () => {
    it('parses a single block with object arguments', () => {
      const text = '<tool_call>\n{"name": "Write", "arguments": {"file_path": "/tmp/n.txt", "content": "hi"}}\n</tool_call>';
      const r = rescueInlineToolCalls(text, TOOLS);
      expect(r.calls).toHaveLength(1);
      expect(r.calls![0].name).toBe('Write');
      expect(JSON.parse(r.calls![0].arguments)).toEqual({ file_path: '/tmp/n.txt', content: 'hi' });
    });

    it('accepts the "parameters" key and multiple blocks', () => {
      const text = '<tool_call>{"name": "Read", "parameters": {"file_path": "/a"}}</tool_call><tool_call>{"name": "Grep", "arguments": {"pattern": "x"}}</tool_call>';
      const r = rescueInlineToolCalls(text, TOOLS);
      expect(r.calls).toHaveLength(2);
    });

    it('treats a truncated unclosed block as unparseable', () => {
      const text = '<tool_call>{"name": "Read", "argum';
      const r = rescueInlineToolCalls(text, TOOLS);
      expect(r.detected).toBe(true);
      expect(r.calls).toBeNull();
    });
  });

  describe('bare / fenced JSON dialect (schema-gated)', () => {
    it('rescues a bare JSON object naming a known tool', () => {
      const r = rescueInlineToolCalls('{"name": "list_dir", "arguments": {"path": "/tmp"}}', TOOLS);
      expect(r.detected).toBe(true);
      expect(r.calls).toEqual([{ name: 'list_dir', arguments: '{"path":"/tmp"}' }]);
    });

    it('rescues a ```json fenced call', () => {
      const r = rescueInlineToolCalls('```json\n{"name": "Bash", "arguments": {"command": "ls"}}\n```', TOOLS);
      expect(r.calls).toHaveLength(1);
    });

    it('does NOT touch ordinary JSON answers that do not name a requested tool', () => {
      const r = rescueInlineToolCalls('{"name": "Tashfeen", "age": 30}', TOOLS);
      expect(r.detected).toBe(false);
      expect(r.cleanText).toBe('{"name": "Tashfeen", "age": 30}');
    });

    it('does NOT touch plain prose', () => {
      const r = rescueInlineToolCalls('The port is 49152, configured in config.json.', TOOLS);
      expect(r.detected).toBe(false);
    });
  });

  describe('stream hold-window helpers', () => {
    it('startsWithDialectMarker matches all dialects, with leading whitespace', () => {
      expect(startsWithDialectMarker('<|tool_calls_section_begin|>x')).toBe(true);
      expect(startsWithDialectMarker('  <|tool_call_begin|>')).toBe(true);
      expect(startsWithDialectMarker('\n<tool_call>{}')).toBe(true);
      expect(startsWithDialectMarker('<function=Bash{')).toBe(true);
      expect(startsWithDialectMarker('Hello!')).toBe(false);
      expect(startsWithDialectMarker('<html>')).toBe(false);
    });

    it('couldBecomeDialectMarker holds strict prefixes and releases divergent text', () => {
      expect(couldBecomeDialectMarker('<')).toBe(true);
      expect(couldBecomeDialectMarker('<|to')).toBe(true);
      expect(couldBecomeDialectMarker('<fun')).toBe(true);
      expect(couldBecomeDialectMarker('<tool_ca')).toBe(true);
      expect(couldBecomeDialectMarker('<div')).toBe(false);
      expect(couldBecomeDialectMarker('Hi')).toBe(false);
      // Full marker present is no longer a *prefix* — startsWith takes over.
      expect(couldBecomeDialectMarker('<tool_call>')).toBe(false);
    });

    it('containsDialectMarker finds markers mid-text', () => {
      expect(containsDialectMarker('prose then <function=Read{"a":1}</function>')).toBe(true);
      expect(containsDialectMarker('plain text')).toBe(false);
    });
  });
});