@jsonstudio/llms 0.6.631 → 0.6.743

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.
Files changed (64) hide show
  1. package/dist/conversion/codecs/anthropic-openai-codec.js +0 -5
  2. package/dist/conversion/codecs/openai-openai-codec.js +0 -6
  3. package/dist/conversion/codecs/responses-openai-codec.js +1 -7
  4. package/dist/conversion/hub/node-support.js +5 -4
  5. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +14 -1
  6. package/dist/conversion/hub/pipeline/hub-pipeline.js +82 -18
  7. package/dist/conversion/hub/pipeline/session-identifiers.js +132 -2
  8. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +130 -15
  9. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +47 -0
  10. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +4 -2
  11. package/dist/conversion/hub/process/chat-process.js +2 -0
  12. package/dist/conversion/hub/response/provider-response.js +6 -1
  13. package/dist/conversion/hub/snapshot-recorder.js +8 -1
  14. package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +0 -7
  15. package/dist/conversion/responses/responses-openai-bridge.js +47 -7
  16. package/dist/conversion/shared/compaction-detect.d.ts +2 -0
  17. package/dist/conversion/shared/compaction-detect.js +53 -0
  18. package/dist/conversion/shared/errors.d.ts +1 -1
  19. package/dist/conversion/shared/reasoning-tool-normalizer.js +7 -0
  20. package/dist/conversion/shared/snapshot-hooks.d.ts +2 -0
  21. package/dist/conversion/shared/snapshot-hooks.js +180 -4
  22. package/dist/conversion/shared/snapshot-utils.d.ts +4 -0
  23. package/dist/conversion/shared/snapshot-utils.js +4 -0
  24. package/dist/conversion/shared/tool-filter-pipeline.js +3 -9
  25. package/dist/conversion/shared/tool-governor.d.ts +2 -0
  26. package/dist/conversion/shared/tool-governor.js +101 -13
  27. package/dist/conversion/shared/tool-harvester.js +42 -2
  28. package/dist/conversion/shared/tooling.d.ts +33 -0
  29. package/dist/conversion/shared/tooling.js +27 -0
  30. package/dist/filters/index.d.ts +0 -2
  31. package/dist/filters/index.js +0 -2
  32. package/dist/filters/special/request-tools-normalize.d.ts +11 -0
  33. package/dist/filters/special/request-tools-normalize.js +13 -50
  34. package/dist/filters/special/response-apply-patch-toon-decode.js +410 -67
  35. package/dist/filters/special/response-tool-arguments-stringify.js +25 -16
  36. package/dist/filters/special/response-tool-arguments-toon-decode.js +8 -76
  37. package/dist/filters/utils/snapshot-writer.js +42 -4
  38. package/dist/guidance/index.js +8 -2
  39. package/dist/router/virtual-router/engine-health.js +0 -4
  40. package/dist/router/virtual-router/engine-selection.d.ts +2 -1
  41. package/dist/router/virtual-router/engine-selection.js +101 -9
  42. package/dist/router/virtual-router/engine.d.ts +5 -1
  43. package/dist/router/virtual-router/engine.js +188 -5
  44. package/dist/router/virtual-router/routing-instructions.d.ts +6 -0
  45. package/dist/router/virtual-router/routing-instructions.js +18 -3
  46. package/dist/router/virtual-router/sticky-session-store.d.ts +1 -0
  47. package/dist/router/virtual-router/sticky-session-store.js +36 -0
  48. package/dist/router/virtual-router/types.d.ts +22 -0
  49. package/dist/servertool/engine.js +335 -9
  50. package/dist/servertool/handlers/compaction-detect.d.ts +1 -0
  51. package/dist/servertool/handlers/compaction-detect.js +1 -0
  52. package/dist/servertool/handlers/gemini-empty-reply-continue.js +29 -5
  53. package/dist/servertool/handlers/iflow-model-error-retry.js +17 -0
  54. package/dist/servertool/handlers/stop-message-auto.js +199 -19
  55. package/dist/servertool/server-side-tools.d.ts +0 -1
  56. package/dist/servertool/server-side-tools.js +0 -1
  57. package/dist/servertool/types.d.ts +1 -0
  58. package/dist/tools/apply-patch-structured.js +52 -15
  59. package/dist/tools/tool-registry.js +537 -15
  60. package/dist/utils/toon.d.ts +4 -0
  61. package/dist/utils/toon.js +75 -0
  62. package/package.json +4 -2
  63. package/dist/test-output/virtual-router/results.json +0 -1
  64. package/dist/test-output/virtual-router/summary.json +0 -12
@@ -10,6 +10,400 @@ function envEnabled() {
10
10
  function isObject(v) {
11
11
  return !!v && typeof v === 'object' && !Array.isArray(v);
12
12
  }
13
+ function readString(value) {
14
+ if (typeof value === 'string') {
15
+ const trimmed = value.trim();
16
+ return trimmed ? trimmed : undefined;
17
+ }
18
+ return undefined;
19
+ }
20
+ function stripCodeFences(text) {
21
+ const trimmed = text.trim();
22
+ const fenceRe = /```(?:diff|patch|apply_patch|toon|text|json)?\s*([\s\S]*?)```/gi;
23
+ const candidates = [];
24
+ let match = null;
25
+ while ((match = fenceRe.exec(trimmed))) {
26
+ if (match[1]) {
27
+ candidates.push(match[1].trim());
28
+ }
29
+ }
30
+ if (!candidates.length) {
31
+ return text;
32
+ }
33
+ for (const candidate of candidates) {
34
+ if (candidate.includes('*** Begin Patch') ||
35
+ candidate.includes('*** Update File:') ||
36
+ candidate.includes('diff --git')) {
37
+ return candidate;
38
+ }
39
+ }
40
+ return candidates[0] ?? text;
41
+ }
42
+ function looksLikePatch(text) {
43
+ if (!text)
44
+ return false;
45
+ const t = text.trim();
46
+ if (!t)
47
+ return false;
48
+ return (t.includes('*** Begin Patch') ||
49
+ t.includes('*** Update File:') ||
50
+ t.includes('*** Add File:') ||
51
+ t.includes('*** Delete File:') ||
52
+ t.includes('diff --git') ||
53
+ /^(?:@@|\+\+\+\s|---\s)/m.test(t));
54
+ }
55
+ function convertGitDiffToApplyPatch(text) {
56
+ const lines = text.replace(/\r\n/g, '\n').split('\n');
57
+ const files = [];
58
+ let current = null;
59
+ let sawDiff = false;
60
+ const flush = () => {
61
+ if (!current)
62
+ return;
63
+ if (current.path && current.kind === 'delete') {
64
+ files.push(current);
65
+ current = null;
66
+ return;
67
+ }
68
+ if (!current.path || (current.kind === 'update' && current.lines.length === 0)) {
69
+ current = null;
70
+ return;
71
+ }
72
+ files.push(current);
73
+ current = null;
74
+ };
75
+ for (const raw of lines) {
76
+ const line = raw;
77
+ const diffMatch = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
78
+ if (diffMatch) {
79
+ sawDiff = true;
80
+ flush();
81
+ const path = diffMatch[2] || diffMatch[1];
82
+ current = { path, kind: 'update', lines: [] };
83
+ continue;
84
+ }
85
+ if (!current) {
86
+ continue;
87
+ }
88
+ if (line.startsWith('new file mode')) {
89
+ current.kind = 'add';
90
+ continue;
91
+ }
92
+ if (line.startsWith('deleted file mode')) {
93
+ current.kind = 'delete';
94
+ continue;
95
+ }
96
+ if (line.startsWith('index ') || line.startsWith('old mode') || line.startsWith('new mode')) {
97
+ continue;
98
+ }
99
+ if (line.startsWith('--- ')) {
100
+ if (line.includes('/dev/null')) {
101
+ current.kind = 'add';
102
+ }
103
+ continue;
104
+ }
105
+ if (line.startsWith('+++ ')) {
106
+ if (line.includes('/dev/null')) {
107
+ current.kind = 'delete';
108
+ }
109
+ else {
110
+ const plusMatch = line.match(/^\+\+\+\s+(?:b\/)?(.+)$/);
111
+ if (plusMatch && plusMatch[1]) {
112
+ current.path = plusMatch[1];
113
+ }
114
+ }
115
+ continue;
116
+ }
117
+ if (current.kind === 'delete') {
118
+ continue;
119
+ }
120
+ if (current.kind === 'add') {
121
+ if (line.startsWith('+') && !line.startsWith('+++')) {
122
+ current.lines.push(line);
123
+ }
124
+ else if (line.startsWith('\\')) {
125
+ current.lines.push(`+${line}`);
126
+ }
127
+ continue;
128
+ }
129
+ if (line.startsWith('@@') ||
130
+ line.startsWith('+') ||
131
+ line.startsWith('-') ||
132
+ line.startsWith(' ') ||
133
+ line.startsWith('\\')) {
134
+ current.lines.push(line.startsWith('\\') ? ` ${line}` : line);
135
+ }
136
+ }
137
+ flush();
138
+ if (!sawDiff || files.length === 0) {
139
+ return null;
140
+ }
141
+ const output = ['*** Begin Patch'];
142
+ for (const file of files) {
143
+ if (!file.path)
144
+ continue;
145
+ if (file.kind === 'add') {
146
+ output.push(`*** Add File: ${file.path}`);
147
+ for (const line of file.lines) {
148
+ if (line.startsWith('+')) {
149
+ output.push(line);
150
+ }
151
+ else {
152
+ output.push(`+${line}`);
153
+ }
154
+ }
155
+ continue;
156
+ }
157
+ if (file.kind === 'delete') {
158
+ output.push(`*** Delete File: ${file.path}`);
159
+ continue;
160
+ }
161
+ output.push(`*** Update File: ${file.path}`);
162
+ for (const line of file.lines) {
163
+ if (line.startsWith('@@') || line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) {
164
+ output.push(line);
165
+ }
166
+ else {
167
+ output.push(` ${line}`);
168
+ }
169
+ }
170
+ }
171
+ output.push('*** End Patch');
172
+ return output.join('\n');
173
+ }
174
+ function normalizeApplyPatchText(raw) {
175
+ if (!raw)
176
+ return raw;
177
+ let text = raw.replace(/\r\n/g, '\n');
178
+ text = stripCodeFences(text);
179
+ text = text.trim();
180
+ if (!text) {
181
+ return raw;
182
+ }
183
+ if (!text.includes('*** Begin Patch') && text.includes('diff --git')) {
184
+ const converted = convertGitDiffToApplyPatch(text);
185
+ if (converted) {
186
+ text = converted;
187
+ }
188
+ }
189
+ if (text.includes('*** Create File:')) {
190
+ text = text.replace(/\*\*\* Create File:/g, '*** Add File:');
191
+ }
192
+ const hasBegin = text.includes('*** Begin Patch');
193
+ const hasEnd = text.includes('*** End Patch');
194
+ if (hasBegin && !hasEnd) {
195
+ text = `${text}\n*** End Patch`;
196
+ }
197
+ if (!hasBegin && /^\*\*\* (Add|Update|Delete) File:/m.test(text)) {
198
+ text = `*** Begin Patch\n${text}\n*** End Patch`;
199
+ }
200
+ if (!text.includes('*** Begin Patch')) {
201
+ return text;
202
+ }
203
+ const lines = text.split('\n');
204
+ const output = [];
205
+ let inUpdateSection = false;
206
+ let afterUpdateHeader = false;
207
+ for (const line of lines) {
208
+ if (line.startsWith('*** Begin Patch')) {
209
+ output.push(line);
210
+ inUpdateSection = false;
211
+ afterUpdateHeader = false;
212
+ continue;
213
+ }
214
+ if (line.startsWith('*** End Patch')) {
215
+ output.push(line);
216
+ inUpdateSection = false;
217
+ afterUpdateHeader = false;
218
+ continue;
219
+ }
220
+ if (line.startsWith('*** Update File:')) {
221
+ output.push(line);
222
+ inUpdateSection = true;
223
+ afterUpdateHeader = true;
224
+ continue;
225
+ }
226
+ if (line.startsWith('*** Add File:') || line.startsWith('*** Delete File:')) {
227
+ output.push(line);
228
+ inUpdateSection = false;
229
+ afterUpdateHeader = false;
230
+ continue;
231
+ }
232
+ if (inUpdateSection) {
233
+ if (afterUpdateHeader && line.trim() === '') {
234
+ continue;
235
+ }
236
+ afterUpdateHeader = false;
237
+ if (line.startsWith('@@') || line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) {
238
+ output.push(line);
239
+ }
240
+ else {
241
+ output.push(` ${line}`);
242
+ }
243
+ continue;
244
+ }
245
+ output.push(line);
246
+ }
247
+ return output.join('\n');
248
+ }
249
+ function collectFunctionCallAdapters(payload) {
250
+ const adapters = [];
251
+ const anyPayload = payload;
252
+ const choices = Array.isArray(anyPayload.choices)
253
+ ? (anyPayload.choices ?? [])
254
+ : [];
255
+ for (const choice of choices) {
256
+ const message = choice && typeof choice === 'object' ? choice.message : undefined;
257
+ const toolCalls = message && Array.isArray(message.tool_calls) ? message.tool_calls : [];
258
+ for (const tc of toolCalls) {
259
+ const fnObj = tc && typeof tc === 'object' ? tc.function : undefined;
260
+ if (fnObj && typeof fnObj === 'object') {
261
+ adapters.push({
262
+ name: typeof fnObj.name === 'string' ? String(fnObj.name) : undefined,
263
+ getArguments: () => fnObj.arguments,
264
+ setArguments: (value) => {
265
+ fnObj.arguments = value;
266
+ }
267
+ });
268
+ }
269
+ }
270
+ }
271
+ const output = Array.isArray(anyPayload.output) ? anyPayload.output : [];
272
+ for (const item of output) {
273
+ if (!item || typeof item !== 'object')
274
+ continue;
275
+ const type = String(item.type || '').toLowerCase();
276
+ if (type === 'function_call') {
277
+ adapters.push({
278
+ name: typeof item.name === 'string' ? String(item.name) : undefined,
279
+ getArguments: () => item.arguments,
280
+ setArguments: (value) => {
281
+ item.arguments = value;
282
+ }
283
+ });
284
+ continue;
285
+ }
286
+ if (type === 'message' && Array.isArray(item.tool_calls)) {
287
+ for (const tc of item.tool_calls) {
288
+ const fnObj = tc && typeof tc === 'object' ? tc.function : undefined;
289
+ if (!fnObj || typeof fnObj !== 'object')
290
+ continue;
291
+ adapters.push({
292
+ name: typeof fnObj.name === 'string' ? String(fnObj.name) : undefined,
293
+ getArguments: () => fnObj.arguments,
294
+ setArguments: (value) => {
295
+ fnObj.arguments = value;
296
+ }
297
+ });
298
+ }
299
+ }
300
+ }
301
+ return adapters;
302
+ }
303
+ function buildSingleChangePayload(record) {
304
+ const kindRaw = readString(record.kind);
305
+ if (!kindRaw)
306
+ return undefined;
307
+ const change = {
308
+ kind: kindRaw.toLowerCase(),
309
+ lines: record.lines ?? record.text ?? record.body,
310
+ target: readString(record.target),
311
+ anchor: readString(record.anchor)
312
+ };
313
+ if (typeof record.use_anchor_indent === 'boolean') {
314
+ change.use_anchor_indent = record.use_anchor_indent;
315
+ }
316
+ const changeFile = readString(record.file);
317
+ if (changeFile) {
318
+ change.file = changeFile;
319
+ }
320
+ return {
321
+ file: changeFile,
322
+ changes: [change]
323
+ };
324
+ }
325
+ function coerceStructuredPayload(record) {
326
+ if (isStructuredApplyPatchPayload(record)) {
327
+ return record;
328
+ }
329
+ if (Array.isArray(record.changes) && record.changes.length === 0) {
330
+ return undefined;
331
+ }
332
+ const single = buildSingleChangePayload(record);
333
+ if (single) {
334
+ return single;
335
+ }
336
+ return undefined;
337
+ }
338
+ function maybeNormalizeArguments(argIn) {
339
+ let parsed;
340
+ let patchText;
341
+ if (typeof argIn === 'string') {
342
+ const trimmed = argIn.trim();
343
+ if (!trimmed)
344
+ return undefined;
345
+ try {
346
+ parsed = JSON.parse(trimmed);
347
+ }
348
+ catch {
349
+ if (looksLikePatch(trimmed)) {
350
+ patchText = trimmed;
351
+ }
352
+ else {
353
+ const stripped = stripCodeFences(trimmed);
354
+ if (looksLikePatch(stripped)) {
355
+ patchText = stripped;
356
+ }
357
+ }
358
+ }
359
+ }
360
+ else if (isObject(argIn)) {
361
+ parsed = argIn;
362
+ }
363
+ else {
364
+ return undefined;
365
+ }
366
+ const record = parsed ?? {};
367
+ if (!patchText) {
368
+ const toon = readString(record.toon);
369
+ if (looksLikePatch(toon)) {
370
+ patchText = toon;
371
+ }
372
+ }
373
+ if (!patchText) {
374
+ const payload = coerceStructuredPayload(record);
375
+ if (payload) {
376
+ try {
377
+ patchText = buildStructuredPatch(payload);
378
+ }
379
+ catch (error) {
380
+ if (!(error instanceof StructuredApplyPatchError)) {
381
+ /* ignore */
382
+ }
383
+ }
384
+ }
385
+ }
386
+ if (!patchText) {
387
+ const patchField = readString(record.patch);
388
+ if (looksLikePatch(patchField)) {
389
+ patchText = patchField;
390
+ }
391
+ }
392
+ if (!patchText) {
393
+ const inputField = readString(record.input);
394
+ if (looksLikePatch(inputField)) {
395
+ patchText = inputField;
396
+ }
397
+ }
398
+ if (!patchText) {
399
+ return undefined;
400
+ }
401
+ const normalizedText = normalizeApplyPatchText(patchText);
402
+ if (!looksLikePatch(normalizedText)) {
403
+ return undefined;
404
+ }
405
+ return JSON.stringify({ input: normalizedText, patch: normalizedText });
406
+ }
13
407
  /**
14
408
  * Response-side apply_patch arguments 规范化(TOON + 结构化 JSON → 统一 diff 文本)。
15
409
  *
@@ -35,79 +429,28 @@ export class ResponseApplyPatchToonDecodeFilter {
35
429
  return { ok: true, data: input };
36
430
  try {
37
431
  const out = JSON.parse(JSON.stringify(input || {}));
38
- const choices = Array.isArray(out.choices) ? out.choices : [];
39
- for (const ch of choices) {
40
- const msg = ch && ch.message ? ch.message : undefined;
41
- const tcs = msg && Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
42
- for (const tc of tcs) {
432
+ const adapters = collectFunctionCallAdapters(out);
433
+ for (const adapter of adapters) {
434
+ try {
435
+ const name = adapter.name ? adapter.name.trim().toLowerCase() : '';
436
+ if (name !== 'apply_patch') {
437
+ continue;
438
+ }
439
+ const normalizedArgs = maybeNormalizeArguments(adapter.getArguments());
440
+ if (!normalizedArgs) {
441
+ continue;
442
+ }
43
443
  try {
44
- const fn = tc && tc.function ? tc.function : undefined;
45
- if (!fn || typeof fn !== 'object')
46
- continue;
47
- const nameRaw = fn.name;
48
- if (typeof nameRaw !== 'string' || nameRaw.trim().toLowerCase() !== 'apply_patch') {
49
- continue;
50
- }
51
- const argIn = fn.arguments;
52
- let parsed;
53
- if (typeof argIn === 'string') {
54
- if (!argIn.trim())
55
- continue;
56
- try {
57
- parsed = JSON.parse(argIn);
58
- }
59
- catch {
60
- // 如果 arguments 不是 JSON 字符串,则保持原样交给下游处理
61
- continue;
62
- }
63
- }
64
- else if (isObject(argIn)) {
65
- parsed = argIn;
66
- }
67
- else {
68
- continue;
69
- }
70
- if (!isObject(parsed))
71
- continue;
72
- // 优先处理 toon: "<patch text>" 形态(兼容旧 TOON 协议)
73
- const toon = parsed.toon;
74
- let patchText;
75
- if (typeof toon === 'string' && toon.trim()) {
76
- if (toon.includes('*** Begin Patch') && toon.includes('*** End Patch')) {
77
- patchText = toon;
78
- }
79
- }
80
- // 否则尝试结构化 JSON(changes 数组 → 统一 diff)
81
- if (!patchText && isStructuredApplyPatchPayload(parsed)) {
82
- try {
83
- patchText = buildStructuredPatch(parsed);
84
- }
85
- catch (error) {
86
- if (error instanceof StructuredApplyPatchError) {
87
- // 结构化 payload 无法构建补丁时,保留原始 arguments,
88
- // 由下游工具或客户端根据自身策略报错;这里不吞掉错误。
89
- continue;
90
- }
91
- continue;
92
- }
93
- }
94
- if (!patchText) {
95
- continue;
96
- }
97
- const normalized = { input: patchText, patch: patchText };
98
- try {
99
- fn.arguments = JSON.stringify(normalized);
100
- }
101
- catch {
102
- // stringify 失败时保留原始 arguments
103
- }
444
+ adapter.setArguments(normalizedArgs);
104
445
  }
105
446
  catch {
106
- // 针对单个 tool_call best-effort,不影响其他工具
447
+ // ignore single adapter failures
107
448
  }
108
449
  }
450
+ catch {
451
+ // 单个 tool_call best-effort
452
+ }
109
453
  }
110
- out.choices = choices;
111
454
  return { ok: true, data: out };
112
455
  }
113
456
  catch {
@@ -1,26 +1,26 @@
1
+ import { repairFindMeta } from '../../conversion/shared/tooling.js';
1
2
  function isObject(v) {
2
3
  return !!v && typeof v === 'object' && !Array.isArray(v);
3
4
  }
4
- function repairFindMeta(s) {
5
- // Minimal, idempotent repairs for common find expressions within a shell:
6
- // - ensure -exec terminator is escaped exactly once: ; => \\;
7
- // - collapse multiple backslashes before ; to a single backslash
8
- // - escape unescaped parentheses used in predicates: ( ) -> \\( \\)
5
+ function normalizeExecCommandArgs(args) {
9
6
  try {
10
- const hasFind = /(^|\s)find\s/.test(s);
11
- if (!hasFind)
12
- return s;
13
- let out = s;
14
- // Only escape semicolon not already escaped (negative lookbehind)
15
- out = out.replace(/-exec([^;]*?)(?<!\\);/g, (_m, g1) => `-exec${g1} \\;`);
16
- // Collapse multiple backslashes immediately before ; into a single backslash
17
- out = out.replace(/-exec([^;]*?)\\+;/g, (_m, g1) => `-exec${g1} \\;`);
18
- // Escape parentheses only when not already escaped
19
- out = out.replace(/(?<!\\)\(/g, '\\(').replace(/(?<!\\)\)/g, '\\)');
7
+ const out = { ...args };
8
+ const rawCmd = typeof out.cmd === 'string' && out.cmd.trim().length
9
+ ? String(out.cmd)
10
+ : typeof out.command === 'string' && out.command.trim().length
11
+ ? String(out.command)
12
+ : undefined;
13
+ if (rawCmd) {
14
+ const fixed = repairFindMeta(rawCmd);
15
+ out.cmd = fixed;
16
+ if (typeof out.command === 'string') {
17
+ out.command = fixed;
18
+ }
19
+ }
20
20
  return out;
21
21
  }
22
22
  catch {
23
- return s;
23
+ return args;
24
24
  }
25
25
  }
26
26
  function packShellCommand(cmd) {
@@ -102,6 +102,15 @@ export class ResponseToolArgumentsStringifyFilter {
102
102
  fn.arguments = '{}';
103
103
  }
104
104
  }
105
+ else if ((name === 'exec_command' || name === 'shell_command' || name === 'bash') && isObject(parsed)) {
106
+ const normalized = normalizeExecCommandArgs(parsed);
107
+ try {
108
+ fn.arguments = JSON.stringify(normalized ?? {});
109
+ }
110
+ catch {
111
+ fn.arguments = '{}';
112
+ }
113
+ }
105
114
  else {
106
115
  if (typeof argIn !== 'string') {
107
116
  try {
@@ -1,4 +1,6 @@
1
1
  import { isShellToolName, normalizeToolName } from '../../tools/tool-description-utils.js';
2
+ import { repairFindMeta } from '../../conversion/shared/tooling.js';
3
+ import { decodeToonToKeyValue, coerceToonValue } from '../../utils/toon.js';
2
4
  function envEnabled() {
3
5
  // Default ON. Allow disabling via env RCC_TOON_ENABLE/ROUTECODEX_TOON_ENABLE = 0|false|off
4
6
  const v = String(process?.env?.RCC_TOON_ENABLE || process?.env?.ROUTECODEX_TOON_ENABLE || '').toLowerCase();
@@ -7,71 +9,6 @@ function envEnabled() {
7
9
  return !(v === '0' || v === 'false' || v === 'off');
8
10
  }
9
11
  function isObject(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
10
- function decodeToonPairs(toon) {
11
- try {
12
- const out = {};
13
- const lines = String(toon).split(/\r?\n/);
14
- let currentKey = null;
15
- let currentVal = '';
16
- const flush = () => {
17
- if (currentKey) {
18
- out[currentKey] = currentVal;
19
- }
20
- currentKey = null;
21
- currentVal = '';
22
- };
23
- for (const raw of lines) {
24
- const line = raw.trim();
25
- if (!line)
26
- continue;
27
- const m = line.match(/^([A-Za-z0-9_\-]+)\s*:\s*(.*)$/);
28
- if (m) {
29
- // 新的 key: value 行,先提交上一段,再开始累积新 key 的值
30
- flush();
31
- currentKey = m[1];
32
- currentVal = m[2] ?? '';
33
- }
34
- else {
35
- // 非 key: value 行视为上一 key 的续行(例如多行脚本)
36
- if (!currentKey) {
37
- // 如果一开始就遇到无法识别的行,认为整个 TOON 不是我们支持的形态
38
- return null;
39
- }
40
- currentVal += (currentVal ? '\n' : '') + raw;
41
- }
42
- }
43
- flush();
44
- return Object.keys(out).length ? out : null;
45
- }
46
- catch {
47
- return null;
48
- }
49
- }
50
- function coerceToPrimitive(value) {
51
- const trimmed = value.trim();
52
- if (!trimmed)
53
- return '';
54
- const lower = trimmed.toLowerCase();
55
- if (lower === 'true')
56
- return true;
57
- if (lower === 'false')
58
- return false;
59
- if (/^[+-]?\d+(\.\d+)?$/.test(trimmed)) {
60
- const num = Number(trimmed);
61
- if (Number.isFinite(num))
62
- return num;
63
- }
64
- if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
65
- (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
66
- try {
67
- return JSON.parse(trimmed);
68
- }
69
- catch {
70
- // fall through
71
- }
72
- }
73
- return value;
74
- }
75
12
  /**
76
13
  * Decode arguments.toon to standard JSON ({command, workdir?}) and map tool name 'shell_toon' → 'shell'.
77
14
  * Stage: response_pre (before arguments stringify and invariants).
@@ -98,7 +35,6 @@ export class ResponseToolArgumentsToonDecodeFilter {
98
35
  const toolName = typeof rawName === 'string' ? rawName : '';
99
36
  const normalizedName = normalizeToolName(toolName);
100
37
  const isShellLike = isShellToolName(toolName);
101
- const isApplyPatch = normalizedName === 'apply_patch';
102
38
  const argIn = fn.arguments;
103
39
  let parsed = undefined;
104
40
  if (typeof argIn === 'string') {
@@ -117,7 +53,7 @@ export class ResponseToolArgumentsToonDecodeFilter {
117
53
  const toon = parsed.toon;
118
54
  if (typeof toon !== 'string' || !toon.trim())
119
55
  continue;
120
- const kv = decodeToonPairs(toon);
56
+ const kv = decodeToonToKeyValue(toon);
121
57
  if (!kv) {
122
58
  const preview = toon.split(/\r?\n/).slice(0, 5).join('\n');
123
59
  const warnMsg = `response_tool_arguments_toon_decode: failed to decode TOON arguments for tool "${fn.name ?? 'unknown'}"`;
@@ -136,10 +72,6 @@ export class ResponseToolArgumentsToonDecodeFilter {
136
72
  }
137
73
  continue; // keep original if decode fails
138
74
  }
139
- // apply_patch 的 toon 由专门的 ResponseApplyPatchToonDecodeFilter 处理,这里跳过,避免覆盖。
140
- if (isApplyPatch) {
141
- continue;
142
- }
143
75
  if (isShellLike) {
144
76
  const commandRaw = (typeof kv['command'] === 'string' && kv['command'].trim()
145
77
  ? kv['command']
@@ -156,7 +88,7 @@ export class ResponseToolArgumentsToonDecodeFilter {
156
88
  ? kv['with_escalated_permissions']
157
89
  : undefined;
158
90
  const justificationRaw = typeof kv['justification'] === 'string' ? kv['justification'] : undefined;
159
- const command = commandRaw.trim();
91
+ const command = repairFindMeta(commandRaw.trim());
160
92
  if (command) {
161
93
  const merged = {
162
94
  cmd: command,
@@ -190,16 +122,16 @@ export class ResponseToolArgumentsToonDecodeFilter {
190
122
  catch {
191
123
  /* keep original */
192
124
  }
193
- if (typeof fn.name === 'string' && fn.name === 'shell_toon') {
194
- fn.name = 'shell';
195
- }
125
+ }
126
+ if (typeof fn.name === 'string' && fn.name === 'shell_toon') {
127
+ fn.name = 'shell';
196
128
  }
197
129
  }
198
130
  else {
199
131
  // 通用 TOON → JSON 解码:除 shell / apply_patch 以外的工具,将 key: value 对映射为普通 JSON 字段。
200
132
  const merged = {};
201
133
  for (const [key, value] of Object.entries(kv)) {
202
- merged[key] = coerceToPrimitive(value);
134
+ merged[key] = coerceToonValue(value);
203
135
  }
204
136
  try {
205
137
  fn.arguments = JSON.stringify(merged);