@kaleidorg/mind 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,10 +35,25 @@ export interface ParsedTurn {
35
35
  /** Raw stop reason from the SDK, when provided. */
36
36
  stopReason?: string;
37
37
  }
38
+ /**
39
+ * Recover tool calls a model emitted as PLAIN TEXT instead of structured frames
40
+ * — `<tool_call>{"name":…,"arguments":…}</tool_call>` (Qwen/Hermes) or a bare
41
+ * leading `{"name":…,"arguments":…}`. Small local models (and SDK builds that
42
+ * don't apply the tool grammar) do this; without recovery the call leaks into
43
+ * the visible answer and never runs.
44
+ */
45
+ export declare function extractTextToolCalls(text: string): Array<{
46
+ name: string;
47
+ arguments: Record<string, unknown>;
48
+ }>;
38
49
  /**
39
50
  * Map a completion `final` (plus the streamed fallback text) into a ParsedTurn.
40
51
  * `rawContent` prefers the SDK's framed `raw.fullText` so the Engine can anchor
41
52
  * the next turn; falls back to the visible text when a provider has no raw form.
53
+ *
54
+ * When the SDK reports no structured tool calls, we re-scan the raw text for
55
+ * tool calls the model emitted inline (see `extractTextToolCalls`) so they still
56
+ * execute instead of leaking into the chat.
42
57
  */
43
58
  export declare function finalToTurn(final: QvacFinalLike, streamed?: string): ParsedTurn;
44
59
  //# sourceMappingURL=parse.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/qvac/parse.ts"],"names":[],"mappings":"AAQA,qEAAqE;AACrE,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,GAAG,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5B,uEAAuE;IACvE,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACtF;;;;OAIG;IACH,UAAU,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAC;CAC9C;AAED,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,SAAS,EAAE,KAAK,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACpF,gFAAgF;IAChF,SAAS,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,SAAK,GAAG,UAAU,CAc3E"}
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/qvac/parse.ts"],"names":[],"mappings":"AAQA,qEAAqE;AACrE,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,GAAG,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5B,uEAAuE;IACvE,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACtF;;;;OAIG;IACH,UAAU,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAC;CAC9C;AAED,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,SAAS,EAAE,KAAK,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACpF,gFAAgF;IAChF,SAAS,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAkCD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,GACX,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAc7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,SAAK,GAAG,UAAU,CAmB3E"}
@@ -5,22 +5,85 @@
5
5
  * desktop, and the eval harness.
6
6
  */
7
7
  import { cleanAssistantVisibleText } from './text.js';
8
+ /** Parse the first balanced `{…}` from a string as a `{name, arguments}` call. */
9
+ function parseCallObject(s) {
10
+ const start = s.indexOf('{');
11
+ if (start < 0)
12
+ return null;
13
+ let depth = 0;
14
+ for (let i = start; i < s.length; i++) {
15
+ const ch = s[i];
16
+ if (ch === '{')
17
+ depth++;
18
+ else if (ch === '}' && --depth === 0) {
19
+ try {
20
+ const obj = JSON.parse(s.slice(start, i + 1));
21
+ if (obj && typeof obj.name === 'string') {
22
+ const args = obj.arguments && typeof obj.arguments === 'object'
23
+ ? obj.arguments
24
+ : {};
25
+ return { name: obj.name, arguments: args };
26
+ }
27
+ }
28
+ catch {
29
+ /* malformed JSON — give up on this fragment */
30
+ }
31
+ return null;
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+ /**
37
+ * Recover tool calls a model emitted as PLAIN TEXT instead of structured frames
38
+ * — `<tool_call>{"name":…,"arguments":…}</tool_call>` (Qwen/Hermes) or a bare
39
+ * leading `{"name":…,"arguments":…}`. Small local models (and SDK builds that
40
+ * don't apply the tool grammar) do this; without recovery the call leaks into
41
+ * the visible answer and never runs.
42
+ */
43
+ export function extractTextToolCalls(text) {
44
+ const calls = [];
45
+ for (const m of text.matchAll(/<tool_call\b[^>]*>([\s\S]*?)<\/tool_call>/gi)) {
46
+ const c = parseCallObject(m[1] ?? '');
47
+ if (c)
48
+ calls.push(c);
49
+ }
50
+ if (calls.length)
51
+ return calls;
52
+ // No tags — accept a bare tool-call object only at the very start of the
53
+ // text (so we don't misread JSON the model is merely talking about).
54
+ if (/^\s*\{?\s*"name"\s*:/i.test(text)) {
55
+ const c = parseCallObject(text);
56
+ if (c)
57
+ calls.push(c);
58
+ }
59
+ return calls;
60
+ }
8
61
  /**
9
62
  * Map a completion `final` (plus the streamed fallback text) into a ParsedTurn.
10
63
  * `rawContent` prefers the SDK's framed `raw.fullText` so the Engine can anchor
11
64
  * the next turn; falls back to the visible text when a provider has no raw form.
65
+ *
66
+ * When the SDK reports no structured tool calls, we re-scan the raw text for
67
+ * tool calls the model emitted inline (see `extractTextToolCalls`) so they still
68
+ * execute instead of leaking into the chat.
12
69
  */
13
70
  export function finalToTurn(final, streamed = '') {
14
71
  const rawText = final.contentText || streamed;
15
72
  const text = cleanAssistantVisibleText(rawText);
73
+ let toolCalls = (final.toolCalls ?? []).map((c) => ({
74
+ id: c.id,
75
+ name: c.name,
76
+ arguments: c.arguments ?? {},
77
+ }));
78
+ if (toolCalls.length === 0) {
79
+ const recovered = extractTextToolCalls(final.raw?.fullText ?? rawText);
80
+ if (recovered.length)
81
+ toolCalls = recovered.map((c) => ({ id: undefined, ...c }));
82
+ }
16
83
  return {
17
84
  text,
18
85
  rawContent: final.raw?.fullText ?? rawText,
19
- toolCalls: (final.toolCalls ?? []).map((c) => ({
20
- id: c.id,
21
- name: c.name,
22
- arguments: c.arguments ?? {},
23
- })),
86
+ toolCalls,
24
87
  truncated: final.stopReason === 'length',
25
88
  stopReason: final.stopReason,
26
89
  };
@@ -1 +1 @@
1
- {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/qvac/parse.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC;AA+BtD;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAoB,EAAE,QAAQ,GAAG,EAAE;IAC7D,MAAM,OAAO,GAAG,KAAK,CAAC,WAAW,IAAI,QAAQ,CAAC;IAC9C,MAAM,IAAI,GAAG,yBAAyB,CAAC,OAAO,CAAC,CAAC;IAChD,OAAO;QACL,IAAI;QACJ,UAAU,EAAE,KAAK,CAAC,GAAG,EAAE,QAAQ,IAAI,OAAO;QAC1C,SAAS,EAAE,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC7C,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,EAAE;SAC7B,CAAC,CAAC;QACH,SAAS,EAAE,KAAK,CAAC,UAAU,KAAK,QAAQ;QACxC,UAAU,EAAE,KAAK,CAAC,UAAU;KAC7B,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/qvac/parse.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/qvac/text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkB9D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsB1D"}
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/qvac/text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsB9D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsB1D"}
package/dist/qvac/text.js CHANGED
@@ -14,6 +14,10 @@ export function cleanAssistantVisibleText(text) {
14
14
  // Qwen-style reasoning sometimes arrives in contentText. Never show/speak it.
15
15
  .replace(/<think\b[\s\S]*?<\/think>/gi, ' ')
16
16
  .replace(/<think\b[\s\S]*$/gi, ' ')
17
+ // Tool calls some models emit as text (<tool_call>{…}</tool_call>) are
18
+ // extracted + executed by the Engine (see parse.ts); never show the tags.
19
+ .replace(/<tool_call\b[^>]*>[\s\S]*?<\/tool_call>/gi, ' ')
20
+ .replace(/<tool_call\b[^>]*>[\s\S]*$/gi, ' ')
17
21
  .replace(/\s+/g, ' ')
18
22
  .trim();
19
23
  // Some small local models emit a tool-call object as plain text. Drop the
@@ -1 +1 @@
1
- {"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/qvac/text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,IAAI,OAAO,GAAG,IAAI;QAChB,8EAA8E;SAC7E,OAAO,CAAC,6BAA6B,EAAE,GAAG,CAAC;SAC3C,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC;SAClC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;IAEV,0EAA0E;IAC1E,wEAAwE;IACxE,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC7F,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACjF,CAAC;IAED,OAAO,OAAO;SACX,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;AACZ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,MAAM,UAAU,GAAG,IAAI;SACpB,OAAO,CAAC,oDAAoD,EAAE,mBAAmB,CAAC;SAClF,OAAO,CAAC,0BAA0B,EAAE,wBAAwB,CAAC;SAC7D,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC;SAC/B,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC;SAC3B,OAAO,CAAC,qBAAqB,EAAE,GAAG,CAAC;SACnC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC;SACtB,OAAO,CAAC,2BAA2B,EAAE,GAAG,CAAC;SACzC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAExB,OAAO,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC;SAC1B,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE;QACb,MAAM,IAAI,GAAG,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC9B,OAAO,CAAC,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC;YACxF,IAAI,KAAK,IAAI,CAAC;IAClB,CAAC,CAAC;SACD,IAAI,CAAC,EAAE,CAAC;SACR,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;AACZ,CAAC"}
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/qvac/text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,IAAI,OAAO,GAAG,IAAI;QAChB,8EAA8E;SAC7E,OAAO,CAAC,6BAA6B,EAAE,GAAG,CAAC;SAC3C,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC;QACnC,uEAAuE;QACvE,0EAA0E;SACzE,OAAO,CAAC,2CAA2C,EAAE,GAAG,CAAC;SACzD,OAAO,CAAC,8BAA8B,EAAE,GAAG,CAAC;SAC5C,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;IAEV,0EAA0E;IAC1E,wEAAwE;IACxE,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC7F,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACjF,CAAC;IAED,OAAO,OAAO;SACX,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;AACZ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,MAAM,UAAU,GAAG,IAAI;SACpB,OAAO,CAAC,oDAAoD,EAAE,mBAAmB,CAAC;SAClF,OAAO,CAAC,0BAA0B,EAAE,wBAAwB,CAAC;SAC7D,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC;SAC/B,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC;SAC3B,OAAO,CAAC,qBAAqB,EAAE,GAAG,CAAC;SACnC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC;SACtB,OAAO,CAAC,2BAA2B,EAAE,GAAG,CAAC;SACzC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAExB,OAAO,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC;SAC1B,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE;QACb,MAAM,IAAI,GAAG,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC9B,OAAO,CAAC,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC;YACxF,IAAI,KAAK,IAAI,CAAC;IAClB,CAAC,CAAC;SACD,IAAI,CAAC,EAAE,CAAC;SACR,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;AACZ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaleidorg/mind",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Local-first reasoning + function-calling engine for KaleidoSwap. QVAC-powered.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { finalToTurn } from './parse.js';
2
+ import { finalToTurn, extractTextToolCalls } from './parse.js';
3
3
 
4
4
  describe('finalToTurn', () => {
5
5
  it('uses contentText for visible text and strips reasoning', () => {
@@ -49,4 +49,73 @@ describe('finalToTurn', () => {
49
49
  const out = finalToTurn({});
50
50
  expect(out).toEqual({ text: '', rawContent: '', toolCalls: [], truncated: false, stopReason: undefined });
51
51
  });
52
+
53
+ // The QVAC SDK / small models sometimes emit tool calls as plain text instead
54
+ // of structured frames; finalToTurn must recover them so they still execute.
55
+ describe('inline tool-call recovery (SDK gave no structured toolCalls)', () => {
56
+ it('recovers a <tool_call> block and hides the tags from the answer', () => {
57
+ const out = finalToTurn({
58
+ contentText:
59
+ '<tool_call> {"name": "rln_create_rgb_invoice", "arguments": {}} </tool_call>',
60
+ });
61
+ expect(out.toolCalls).toEqual([{ name: 'rln_create_rgb_invoice', arguments: {} }]);
62
+ expect(out.text).toBe('');
63
+ });
64
+
65
+ it('keeps the trailing sentence after the tag out of the answer but runs the call', () => {
66
+ const out = finalToTurn({
67
+ contentText:
68
+ '<tool_call> {"name": "rln_create_rgb_invoice", "arguments": {}} </tool_call> Please specify the asset ID.',
69
+ });
70
+ expect(out.toolCalls).toEqual([{ name: 'rln_create_rgb_invoice', arguments: {} }]);
71
+ expect(out.text).toBe('Please specify the asset ID.');
72
+ });
73
+
74
+ it('recovers nested arguments', () => {
75
+ const out = finalToTurn({
76
+ contentText:
77
+ '<tool_call> {"name": "lsp_get_order", "arguments": {"order_id": "latest", "access_token": "latest"}} </tool_call>',
78
+ });
79
+ expect(out.toolCalls).toEqual([
80
+ { name: 'lsp_get_order', arguments: { order_id: 'latest', access_token: 'latest' } },
81
+ ]);
82
+ });
83
+
84
+ it('recovers a bare leading tool-call object', () => {
85
+ const out = finalToTurn({ contentText: '{"name": "get_balances", "arguments": {}}' });
86
+ expect(out.toolCalls).toEqual([{ name: 'get_balances', arguments: {} }]);
87
+ });
88
+
89
+ it('does NOT recover when the SDK already returned structured calls', () => {
90
+ const out = finalToTurn({
91
+ contentText: '<tool_call> {"name": "ghost", "arguments": {}} </tool_call>',
92
+ toolCalls: [{ name: 'real_tool', arguments: { a: 1 } }],
93
+ });
94
+ expect(out.toolCalls).toEqual([{ id: undefined, name: 'real_tool', arguments: { a: 1 } }]);
95
+ });
96
+
97
+ it('ignores JSON the model is merely talking about (not a call)', () => {
98
+ const out = finalToTurn({
99
+ contentText: 'A tool call looks like {"name": "x", "arguments": {}} in JSON.',
100
+ });
101
+ expect(out.toolCalls).toEqual([]);
102
+ expect(out.text).toContain('A tool call looks like');
103
+ });
104
+ });
105
+
106
+ describe('extractTextToolCalls', () => {
107
+ it('extracts multiple tagged calls', () => {
108
+ const calls = extractTextToolCalls(
109
+ '<tool_call>{"name":"a","arguments":{}}</tool_call> and <tool_call>{"name":"b","arguments":{"x":1}}</tool_call>',
110
+ );
111
+ expect(calls).toEqual([
112
+ { name: 'a', arguments: {} },
113
+ { name: 'b', arguments: { x: 1 } },
114
+ ]);
115
+ });
116
+
117
+ it('returns [] for plain prose', () => {
118
+ expect(extractTextToolCalls('just a normal answer')).toEqual([]);
119
+ });
120
+ });
52
121
  });
package/src/qvac/parse.ts CHANGED
@@ -35,22 +35,88 @@ export interface ParsedTurn {
35
35
  stopReason?: string;
36
36
  }
37
37
 
38
+ /** Parse the first balanced `{…}` from a string as a `{name, arguments}` call. */
39
+ function parseCallObject(
40
+ s: string,
41
+ ): { name: string; arguments: Record<string, unknown> } | null {
42
+ const start = s.indexOf('{');
43
+ if (start < 0) return null;
44
+ let depth = 0;
45
+ for (let i = start; i < s.length; i++) {
46
+ const ch = s[i];
47
+ if (ch === '{') depth++;
48
+ else if (ch === '}' && --depth === 0) {
49
+ try {
50
+ const obj = JSON.parse(s.slice(start, i + 1)) as {
51
+ name?: unknown;
52
+ arguments?: unknown;
53
+ };
54
+ if (obj && typeof obj.name === 'string') {
55
+ const args =
56
+ obj.arguments && typeof obj.arguments === 'object'
57
+ ? (obj.arguments as Record<string, unknown>)
58
+ : {};
59
+ return { name: obj.name, arguments: args };
60
+ }
61
+ } catch {
62
+ /* malformed JSON — give up on this fragment */
63
+ }
64
+ return null;
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+
70
+ /**
71
+ * Recover tool calls a model emitted as PLAIN TEXT instead of structured frames
72
+ * — `<tool_call>{"name":…,"arguments":…}</tool_call>` (Qwen/Hermes) or a bare
73
+ * leading `{"name":…,"arguments":…}`. Small local models (and SDK builds that
74
+ * don't apply the tool grammar) do this; without recovery the call leaks into
75
+ * the visible answer and never runs.
76
+ */
77
+ export function extractTextToolCalls(
78
+ text: string,
79
+ ): Array<{ name: string; arguments: Record<string, unknown> }> {
80
+ const calls: Array<{ name: string; arguments: Record<string, unknown> }> = [];
81
+ for (const m of text.matchAll(/<tool_call\b[^>]*>([\s\S]*?)<\/tool_call>/gi)) {
82
+ const c = parseCallObject(m[1] ?? '');
83
+ if (c) calls.push(c);
84
+ }
85
+ if (calls.length) return calls;
86
+ // No tags — accept a bare tool-call object only at the very start of the
87
+ // text (so we don't misread JSON the model is merely talking about).
88
+ if (/^\s*\{?\s*"name"\s*:/i.test(text)) {
89
+ const c = parseCallObject(text);
90
+ if (c) calls.push(c);
91
+ }
92
+ return calls;
93
+ }
94
+
38
95
  /**
39
96
  * Map a completion `final` (plus the streamed fallback text) into a ParsedTurn.
40
97
  * `rawContent` prefers the SDK's framed `raw.fullText` so the Engine can anchor
41
98
  * the next turn; falls back to the visible text when a provider has no raw form.
99
+ *
100
+ * When the SDK reports no structured tool calls, we re-scan the raw text for
101
+ * tool calls the model emitted inline (see `extractTextToolCalls`) so they still
102
+ * execute instead of leaking into the chat.
42
103
  */
43
104
  export function finalToTurn(final: QvacFinalLike, streamed = ''): ParsedTurn {
44
105
  const rawText = final.contentText || streamed;
45
106
  const text = cleanAssistantVisibleText(rawText);
107
+ let toolCalls = (final.toolCalls ?? []).map((c) => ({
108
+ id: c.id,
109
+ name: c.name,
110
+ arguments: c.arguments ?? {},
111
+ }));
112
+ if (toolCalls.length === 0) {
113
+ const recovered = extractTextToolCalls(final.raw?.fullText ?? rawText);
114
+ if (recovered.length) toolCalls = recovered.map((c) => ({ id: undefined, ...c }));
115
+ }
46
116
  return {
47
117
  text,
48
118
  rawContent: final.raw?.fullText ?? rawText,
49
- toolCalls: (final.toolCalls ?? []).map((c) => ({
50
- id: c.id,
51
- name: c.name,
52
- arguments: c.arguments ?? {},
53
- })),
119
+ toolCalls,
54
120
  truncated: final.stopReason === 'length',
55
121
  stopReason: final.stopReason,
56
122
  };
package/src/qvac/text.ts CHANGED
@@ -15,6 +15,10 @@ export function cleanAssistantVisibleText(text: string): string {
15
15
  // Qwen-style reasoning sometimes arrives in contentText. Never show/speak it.
16
16
  .replace(/<think\b[\s\S]*?<\/think>/gi, ' ')
17
17
  .replace(/<think\b[\s\S]*$/gi, ' ')
18
+ // Tool calls some models emit as text (<tool_call>{…}</tool_call>) are
19
+ // extracted + executed by the Engine (see parse.ts); never show the tags.
20
+ .replace(/<tool_call\b[^>]*>[\s\S]*?<\/tool_call>/gi, ' ')
21
+ .replace(/<tool_call\b[^>]*>[\s\S]*$/gi, ' ')
18
22
  .replace(/\s+/g, ' ')
19
23
  .trim();
20
24