@jsonstudio/llms 0.6.633 → 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 (61) 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 +23 -19
  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/filters/index.d.ts +0 -2
  29. package/dist/filters/index.js +0 -2
  30. package/dist/filters/special/request-tools-normalize.d.ts +11 -0
  31. package/dist/filters/special/request-tools-normalize.js +13 -50
  32. package/dist/filters/special/response-apply-patch-toon-decode.js +403 -82
  33. package/dist/filters/special/response-tool-arguments-toon-decode.js +6 -75
  34. package/dist/filters/utils/snapshot-writer.js +42 -4
  35. package/dist/guidance/index.js +8 -2
  36. package/dist/router/virtual-router/engine-health.js +0 -4
  37. package/dist/router/virtual-router/engine-selection.d.ts +2 -1
  38. package/dist/router/virtual-router/engine-selection.js +101 -9
  39. package/dist/router/virtual-router/engine.d.ts +5 -1
  40. package/dist/router/virtual-router/engine.js +188 -5
  41. package/dist/router/virtual-router/routing-instructions.d.ts +6 -0
  42. package/dist/router/virtual-router/routing-instructions.js +18 -3
  43. package/dist/router/virtual-router/sticky-session-store.d.ts +1 -0
  44. package/dist/router/virtual-router/sticky-session-store.js +36 -0
  45. package/dist/router/virtual-router/types.d.ts +22 -0
  46. package/dist/servertool/engine.js +335 -9
  47. package/dist/servertool/handlers/compaction-detect.d.ts +1 -0
  48. package/dist/servertool/handlers/compaction-detect.js +1 -0
  49. package/dist/servertool/handlers/gemini-empty-reply-continue.js +29 -5
  50. package/dist/servertool/handlers/iflow-model-error-retry.js +17 -0
  51. package/dist/servertool/handlers/stop-message-auto.js +199 -19
  52. package/dist/servertool/server-side-tools.d.ts +0 -1
  53. package/dist/servertool/server-side-tools.js +0 -1
  54. package/dist/servertool/types.d.ts +1 -0
  55. package/dist/tools/apply-patch-structured.js +52 -15
  56. package/dist/tools/tool-registry.js +537 -15
  57. package/dist/utils/toon.d.ts +4 -0
  58. package/dist/utils/toon.js +75 -0
  59. package/package.json +4 -2
  60. package/dist/test-output/virtual-router/results.json +0 -1
  61. package/dist/test-output/virtual-router/summary.json +0 -12
@@ -27,15 +27,471 @@ const readNumber = (value) => {
27
27
  }
28
28
  return value;
29
29
  };
30
+ const stripArgKeyArtifacts = (input) => {
31
+ try {
32
+ return String(input || '')
33
+ .replace(/<\/?\s*tool_call[^>]*>/gi, '')
34
+ .replace(/<\/?\s*arg_key\s*>/gi, '')
35
+ .replace(/<\/?\s*arg_value\s*>/gi, '');
36
+ }
37
+ catch {
38
+ return input;
39
+ }
40
+ };
41
+ const coercePrimitive = (raw) => {
42
+ const trimmed = raw.trim();
43
+ if (!trimmed)
44
+ return '';
45
+ if (/^(true|false)$/i.test(trimmed))
46
+ return /^true$/i.test(trimmed);
47
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed))
48
+ return Number(trimmed);
49
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
50
+ (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
51
+ try {
52
+ return JSON.parse(trimmed);
53
+ }
54
+ catch {
55
+ return trimmed;
56
+ }
57
+ }
58
+ return trimmed;
59
+ };
60
+ const extractInjectedArgPairs = (raw) => {
61
+ const delimiter = '</arg_key><arg_value>';
62
+ if (typeof raw !== 'string' || !raw.includes(delimiter))
63
+ return null;
64
+ const parts = raw.split(delimiter);
65
+ if (parts.length < 2)
66
+ return null;
67
+ const looksLikeKey = (s) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(s.trim());
68
+ const pairs = [];
69
+ let baseValue = parts[0] ?? '';
70
+ if (parts.length === 2) {
71
+ const k = (parts[0] ?? '').trim();
72
+ const v = (parts[1] ?? '').trim();
73
+ if (looksLikeKey(k) && v.length > 0) {
74
+ baseValue = '';
75
+ pairs.push({ key: k, value: coercePrimitive(v) });
76
+ }
77
+ return pairs.length ? { baseValue, pairs } : null;
78
+ }
79
+ for (let i = 1; i + 1 < parts.length; i += 2) {
80
+ const key = (parts[i] ?? '').trim();
81
+ const rawValue = (parts[i + 1] ?? '').trim();
82
+ if (!looksLikeKey(key)) {
83
+ continue;
84
+ }
85
+ if (rawValue.length === 0) {
86
+ continue;
87
+ }
88
+ pairs.push({ key, value: coercePrimitive(rawValue) });
89
+ }
90
+ if (!pairs.length)
91
+ return null;
92
+ return { baseValue, pairs };
93
+ };
94
+ const repairArgKeyArtifactsInObject = (value) => {
95
+ const visit = (node) => {
96
+ if (Array.isArray(node)) {
97
+ for (const entry of node) {
98
+ visit(entry);
99
+ }
100
+ return;
101
+ }
102
+ if (!isRecord(node))
103
+ return;
104
+ for (const [k, v] of Object.entries(node)) {
105
+ if (typeof v === 'string') {
106
+ const injected = extractInjectedArgPairs(v);
107
+ if (injected) {
108
+ if (injected.baseValue !== '') {
109
+ node[k] = injected.baseValue;
110
+ }
111
+ for (const pair of injected.pairs) {
112
+ if (!Object.prototype.hasOwnProperty.call(node, pair.key)) {
113
+ node[pair.key] = pair.value;
114
+ }
115
+ }
116
+ }
117
+ }
118
+ visit(node[k]);
119
+ }
120
+ };
121
+ visit(value);
122
+ };
30
123
  const tryParseJson = (input) => {
124
+ const raw = typeof input === 'string' ? input : '';
125
+ if (!raw.trim())
126
+ return {};
31
127
  try {
32
- const parsed = JSON.parse(input);
33
- return isRecord(parsed) ? parsed : {};
128
+ const parsed = JSON.parse(raw);
129
+ repairArgKeyArtifactsInObject(parsed);
130
+ return parsed;
34
131
  }
35
132
  catch {
133
+ // try stripping common tool-call markup artifacts and parsing again
134
+ try {
135
+ const stripped = stripArgKeyArtifacts(raw).trim();
136
+ if (stripped && stripped !== raw) {
137
+ const parsed = JSON.parse(stripped);
138
+ repairArgKeyArtifactsInObject(parsed);
139
+ return parsed;
140
+ }
141
+ }
142
+ catch {
143
+ // continue
144
+ }
145
+ // attempt to parse the first JSON container substring
146
+ try {
147
+ const candidate = raw.match(/\{[\s\S]*\}|\[[\s\S]*\]/)?.[0];
148
+ if (candidate) {
149
+ const strippedCandidate = stripArgKeyArtifacts(candidate).trim();
150
+ const parsed = JSON.parse(strippedCandidate);
151
+ repairArgKeyArtifactsInObject(parsed);
152
+ return parsed;
153
+ }
154
+ }
155
+ catch {
156
+ // ignore
157
+ }
36
158
  return {};
37
159
  }
38
160
  };
161
+ const stripCodeFences = (text) => {
162
+ const trimmed = text.trim();
163
+ // Only treat the entire payload as fenced when it *starts* with a code fence.
164
+ // Patch bodies (especially added Markdown files) may legitimately contain ``` blocks;
165
+ // we must not strip those.
166
+ if (!trimmed.startsWith('```')) {
167
+ return text;
168
+ }
169
+ const fenceRe = /^```(?:diff|patch|apply_patch|text|json)?[ \t]*\n([\s\S]*?)\n```/gmi;
170
+ const candidates = [];
171
+ let match = null;
172
+ while ((match = fenceRe.exec(trimmed))) {
173
+ if (match[1]) {
174
+ candidates.push(match[1].trim());
175
+ }
176
+ }
177
+ if (!candidates.length) {
178
+ return text;
179
+ }
180
+ for (const candidate of candidates) {
181
+ if (candidate.includes('*** Begin Patch') ||
182
+ candidate.includes('*** Update File:') ||
183
+ candidate.includes('diff --git')) {
184
+ return candidate;
185
+ }
186
+ }
187
+ return candidates[0] ?? text;
188
+ };
189
+ const looksLikePatch = (text) => {
190
+ if (!text)
191
+ return false;
192
+ const t = text.trim();
193
+ if (!t)
194
+ return false;
195
+ return (t.includes('*** Begin Patch') ||
196
+ t.includes('*** Update File:') ||
197
+ t.includes('*** Add File:') ||
198
+ t.includes('*** Delete File:') ||
199
+ t.includes('diff --git') ||
200
+ /^(?:@@|\+\+\+\s|---\s)/m.test(t));
201
+ };
202
+ const convertGitDiffToApplyPatch = (text) => {
203
+ const lines = text.replace(/\r\n/g, '\n').split('\n');
204
+ const files = [];
205
+ let current = null;
206
+ let sawDiff = false;
207
+ const flush = () => {
208
+ if (!current)
209
+ return;
210
+ if (current.path && current.kind === 'delete') {
211
+ files.push(current);
212
+ current = null;
213
+ return;
214
+ }
215
+ if (!current.path || (current.kind === 'update' && current.lines.length === 0)) {
216
+ current = null;
217
+ return;
218
+ }
219
+ files.push(current);
220
+ current = null;
221
+ };
222
+ for (const raw of lines) {
223
+ const line = raw;
224
+ const diffMatch = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
225
+ if (diffMatch) {
226
+ sawDiff = true;
227
+ flush();
228
+ const path = diffMatch[2] || diffMatch[1];
229
+ current = { path, kind: 'update', lines: [] };
230
+ continue;
231
+ }
232
+ if (!current) {
233
+ continue;
234
+ }
235
+ if (line.startsWith('new file mode')) {
236
+ current.kind = 'add';
237
+ continue;
238
+ }
239
+ if (line.startsWith('deleted file mode')) {
240
+ current.kind = 'delete';
241
+ continue;
242
+ }
243
+ if (line.startsWith('index ') || line.startsWith('old mode') || line.startsWith('new mode')) {
244
+ continue;
245
+ }
246
+ if (line.startsWith('--- ')) {
247
+ if (line.includes('/dev/null')) {
248
+ current.kind = 'add';
249
+ }
250
+ continue;
251
+ }
252
+ if (line.startsWith('+++ ')) {
253
+ if (line.includes('/dev/null')) {
254
+ current.kind = 'delete';
255
+ }
256
+ else {
257
+ const plusMatch = line.match(/^\+\+\+\s+(?:b\/)?(.+)$/);
258
+ if (plusMatch && plusMatch[1]) {
259
+ current.path = plusMatch[1];
260
+ }
261
+ }
262
+ continue;
263
+ }
264
+ if (current.kind === 'delete') {
265
+ continue;
266
+ }
267
+ if (current.kind === 'add') {
268
+ if (line.startsWith('+') && !line.startsWith('+++')) {
269
+ current.lines.push(line);
270
+ }
271
+ else if (line.startsWith('\\')) {
272
+ current.lines.push(`+${line}`);
273
+ }
274
+ continue;
275
+ }
276
+ if (line.startsWith('@@') ||
277
+ line.startsWith('+') ||
278
+ line.startsWith('-') ||
279
+ line.startsWith(' ') ||
280
+ line.startsWith('\\')) {
281
+ current.lines.push(line.startsWith('\\') ? ` ${line}` : line);
282
+ }
283
+ }
284
+ flush();
285
+ if (!sawDiff || files.length === 0) {
286
+ return null;
287
+ }
288
+ const output = ['*** Begin Patch'];
289
+ for (const file of files) {
290
+ if (!file.path)
291
+ continue;
292
+ if (file.kind === 'add') {
293
+ output.push(`*** Add File: ${file.path}`);
294
+ for (const line of file.lines) {
295
+ if (line.startsWith('+')) {
296
+ output.push(line);
297
+ }
298
+ else {
299
+ output.push(`+${line}`);
300
+ }
301
+ }
302
+ continue;
303
+ }
304
+ if (file.kind === 'delete') {
305
+ output.push(`*** Delete File: ${file.path}`);
306
+ continue;
307
+ }
308
+ output.push(`*** Update File: ${file.path}`);
309
+ for (const line of file.lines) {
310
+ if (line.startsWith('@@') || line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) {
311
+ output.push(line);
312
+ }
313
+ else {
314
+ output.push(` ${line}`);
315
+ }
316
+ }
317
+ }
318
+ output.push('*** End Patch');
319
+ return output.join('\n');
320
+ };
321
+ const decodeEscapedNewlinesIfNeeded = (value) => {
322
+ if (!value)
323
+ return value;
324
+ if (value.includes('\n'))
325
+ return value;
326
+ if (!value.includes('\\n') && !value.includes('\\r') && !value.toLowerCase().includes('\\u000a') && !value.toLowerCase().includes('\\u000d')) {
327
+ return value;
328
+ }
329
+ // Only decode when the patch appears to be newline-escaped into a single line.
330
+ // This avoids corrupting legitimate file content that contains "\n" sequences.
331
+ let out = value;
332
+ out = out.replace(/\\r\\n/g, '\n');
333
+ out = out.replace(/\\n/g, '\n');
334
+ out = out.replace(/\\r/g, '\n');
335
+ out = out.replace(/\\u000a/gi, '\n');
336
+ out = out.replace(/\\u000d/gi, '\n');
337
+ return out;
338
+ };
339
+ const normalizeApplyPatchText = (raw) => {
340
+ if (!raw)
341
+ return raw;
342
+ let text = raw.replace(/\r\n/g, '\n');
343
+ text = decodeEscapedNewlinesIfNeeded(text);
344
+ text = stripCodeFences(text);
345
+ text = text.trim();
346
+ if (!text) {
347
+ return raw;
348
+ }
349
+ if (!text.includes('*** Begin Patch') && text.includes('diff --git')) {
350
+ const converted = convertGitDiffToApplyPatch(text);
351
+ if (converted) {
352
+ text = converted;
353
+ }
354
+ }
355
+ else if (!text.includes('*** Begin Patch') && !text.includes('diff --git')) {
356
+ // 兼容只包含 --- / +++ / @@ 的 Git diff 片段(无 diff --git 头)
357
+ const minusMatch = text.match(/^---\s+(.*)$/m);
358
+ const plusMatch = text.match(/^\+\+\+\s+(.*)$/m);
359
+ if (minusMatch && plusMatch) {
360
+ const rawPlus = plusMatch[1] || '';
361
+ const pathMatch = rawPlus.match(/^(?:b\/)?(.+)$/);
362
+ const path = (pathMatch && pathMatch[1] ? pathMatch[1] : rawPlus).trim();
363
+ if (path) {
364
+ const synthetic = `diff --git a/${path} b/${path}\n${text}`;
365
+ const converted = convertGitDiffToApplyPatch(synthetic);
366
+ if (converted) {
367
+ text = converted;
368
+ }
369
+ }
370
+ }
371
+ }
372
+ if (text.includes('*** Create File:')) {
373
+ text = text.replace(/\*\*\* Create File:/g, '*** Add File:');
374
+ }
375
+ let hasBegin = text.includes('*** Begin Patch');
376
+ const hasEnd = text.includes('*** End Patch');
377
+ if (hasBegin && !hasEnd) {
378
+ text = `${text}\n*** End Patch`;
379
+ }
380
+ if (!hasBegin && /^\*\*\* (Add|Update|Delete) File:/m.test(text)) {
381
+ text = `*** Begin Patch\n${text}\n*** End Patch`;
382
+ hasBegin = true;
383
+ }
384
+ if (!text.includes('*** Begin Patch')) {
385
+ return text;
386
+ }
387
+ // 容错处理:如果补丁前后包裹了多余的 JSON / 文本(例如 {"patch":"*** Begin Patch ... *** End Patch"}),
388
+ // 则截断到第一个 '*** Begin Patch' 与 '*** End Patch' 之间的主体,恢复为标准 apply_patch 片段。
389
+ const beginIndex = text.indexOf('*** Begin Patch');
390
+ if (beginIndex > 0) {
391
+ text = text.slice(beginIndex);
392
+ }
393
+ const endMarker = '*** End Patch';
394
+ const firstEndIndex = text.indexOf(endMarker);
395
+ const concatSignatures = [
396
+ `${endMarker}","input":"*** Begin Patch`,
397
+ `${endMarker}","patch":"*** Begin Patch`,
398
+ `${endMarker}\\",\\"input\\":\\"*** Begin Patch`,
399
+ `${endMarker}\\",\\"patch\\":\\"*** Begin Patch`
400
+ ];
401
+ const hasConcatenationSignal = concatSignatures.some((sig) => text.includes(sig));
402
+ if (hasConcatenationSignal && firstEndIndex >= 0) {
403
+ text = text.slice(0, firstEndIndex + endMarker.length);
404
+ }
405
+ else {
406
+ const lastEndIndex = text.lastIndexOf(endMarker);
407
+ if (lastEndIndex >= 0) {
408
+ const afterEnd = text.slice(lastEndIndex + endMarker.length);
409
+ if (afterEnd.trim().length > 0) {
410
+ text = text.slice(0, lastEndIndex + endMarker.length);
411
+ }
412
+ }
413
+ }
414
+ const lines = text.split('\n');
415
+ const output = [];
416
+ let inUpdateSection = false;
417
+ let afterUpdateHeader = false;
418
+ for (const line of lines) {
419
+ if (line.startsWith('*** Begin Patch')) {
420
+ output.push(line);
421
+ inUpdateSection = false;
422
+ afterUpdateHeader = false;
423
+ continue;
424
+ }
425
+ if (line.startsWith('*** End Patch')) {
426
+ output.push(line);
427
+ inUpdateSection = false;
428
+ afterUpdateHeader = false;
429
+ continue;
430
+ }
431
+ if (line.startsWith('*** Update File:')) {
432
+ output.push(line);
433
+ inUpdateSection = true;
434
+ afterUpdateHeader = true;
435
+ continue;
436
+ }
437
+ if (line.startsWith('*** Add File:') || line.startsWith('*** Delete File:')) {
438
+ output.push(line);
439
+ inUpdateSection = false;
440
+ afterUpdateHeader = false;
441
+ continue;
442
+ }
443
+ if (inUpdateSection) {
444
+ if (afterUpdateHeader && line.trim() === '') {
445
+ continue;
446
+ }
447
+ afterUpdateHeader = false;
448
+ if (line.startsWith('@@') || line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) {
449
+ output.push(line);
450
+ }
451
+ else {
452
+ output.push(` ${line}`);
453
+ }
454
+ continue;
455
+ }
456
+ output.push(line);
457
+ }
458
+ return output.join('\n');
459
+ };
460
+ const buildSingleChangePayload = (record) => {
461
+ const kindRaw = asString(record.kind);
462
+ if (!kindRaw)
463
+ return undefined;
464
+ const change = {
465
+ kind: kindRaw.toLowerCase(),
466
+ lines: record.lines ?? record.text ?? record.body,
467
+ target: asString(record.target) ?? undefined,
468
+ anchor: asString(record.anchor) ?? undefined
469
+ };
470
+ if (typeof record.use_anchor_indent === 'boolean') {
471
+ change.use_anchor_indent = record.use_anchor_indent;
472
+ }
473
+ const changeFile = asString(record.file);
474
+ if (changeFile) {
475
+ change.file = changeFile;
476
+ }
477
+ return {
478
+ file: changeFile,
479
+ changes: [change]
480
+ };
481
+ };
482
+ const coerceStructuredPayload = (record) => {
483
+ if (isStructuredApplyPatchPayload(record)) {
484
+ return record;
485
+ }
486
+ if (Array.isArray(record.changes) && record.changes.length === 0) {
487
+ return undefined;
488
+ }
489
+ const single = buildSingleChangePayload(record);
490
+ if (single) {
491
+ return single;
492
+ }
493
+ return undefined;
494
+ };
39
495
  const toJson = (value) => {
40
496
  try {
41
497
  return JSON.stringify(value ?? {});
@@ -44,6 +500,62 @@ const toJson = (value) => {
44
500
  return '{}';
45
501
  }
46
502
  };
503
+ const extractApplyPatchText = (argsString, rawArgs) => {
504
+ const rawTrimmed = typeof argsString === 'string' ? argsString.trim() : '';
505
+ const looksJsonContainer = rawTrimmed.startsWith('{') || rawTrimmed.startsWith('[');
506
+ if (!looksJsonContainer && looksLikePatch(rawTrimmed)) {
507
+ return { patchText: normalizeApplyPatchText(rawTrimmed) };
508
+ }
509
+ if (isRecord(rawArgs)) {
510
+ // Special case: argsString claims to be JSON but isn't parseable (tryParseJson fallback returns {}),
511
+ // and the raw text looks like a patch. Treat it as a patch string to avoid dropping edits.
512
+ if (looksJsonContainer && Object.keys(rawArgs).length === 0 && looksLikePatch(rawTrimmed)) {
513
+ return { patchText: normalizeApplyPatchText(rawTrimmed) };
514
+ }
515
+ const patchField = asString(rawArgs.patch);
516
+ if (patchField && looksLikePatch(patchField)) {
517
+ return { patchText: normalizeApplyPatchText(patchField) };
518
+ }
519
+ const inputField = asString(rawArgs.input);
520
+ if (inputField && looksLikePatch(inputField)) {
521
+ return { patchText: normalizeApplyPatchText(inputField) };
522
+ }
523
+ const payload = coerceStructuredPayload(rawArgs);
524
+ if (payload) {
525
+ try {
526
+ return { patchText: buildStructuredPatch(payload) };
527
+ }
528
+ catch (error) {
529
+ if (!(error instanceof StructuredApplyPatchError)) {
530
+ throw error;
531
+ }
532
+ return { failureReason: error.reason || 'structured_apply_patch_error' };
533
+ }
534
+ }
535
+ }
536
+ else if (Array.isArray(rawArgs) && rawArgs.length > 0) {
537
+ // 兼容形态:arguments 直接是单元素数组 [{ file, changes: [...] }]
538
+ const first = rawArgs.find((entry) => isRecord(entry));
539
+ if (first) {
540
+ const payload = coerceStructuredPayload(first);
541
+ if (payload) {
542
+ try {
543
+ return { patchText: buildStructuredPatch(payload) };
544
+ }
545
+ catch (error) {
546
+ if (!(error instanceof StructuredApplyPatchError)) {
547
+ throw error;
548
+ }
549
+ return { failureReason: error.reason || 'structured_apply_patch_error' };
550
+ }
551
+ }
552
+ }
553
+ }
554
+ else if (typeof rawArgs === 'string' && looksLikePatch(rawArgs)) {
555
+ return { patchText: normalizeApplyPatchText(rawArgs) };
556
+ }
557
+ return {};
558
+ };
47
559
  const detectForbiddenWrite = (script) => {
48
560
  const normalized = script.toLowerCase();
49
561
  if (!normalized) {
@@ -93,25 +605,30 @@ export function validateToolCall(name, argsString) {
93
605
  if (!allowed.has(normalizedName)) {
94
606
  return { ok: false, reason: 'unknown_tool' };
95
607
  }
96
- const rawArgs = tryParseJson(typeof argsString === 'string' ? argsString : '{}');
608
+ const rawArgsAny = tryParseJson(typeof argsString === 'string' ? argsString : '{}');
97
609
  switch (normalizedName) {
98
610
  case 'apply_patch': {
99
- const record = rawArgs;
100
- if (!isStructuredApplyPatchPayload(record)) {
101
- return { ok: false, reason: 'missing_changes' };
102
- }
103
- try {
104
- const patchText = buildStructuredPatch(record);
105
- return { ok: true, normalizedArgs: toJson({ patch: patchText, input: patchText }) };
106
- }
107
- catch (error) {
108
- if (error instanceof StructuredApplyPatchError) {
109
- return { ok: false, reason: error.reason || 'invalid_patch_payload' };
611
+ const extracted = extractApplyPatchText(argsString, rawArgsAny);
612
+ const patchText = extracted.patchText;
613
+ if (!patchText) {
614
+ const rawTrimmed = typeof argsString === 'string' ? argsString.trim() : '';
615
+ const looksJsonContainer = rawTrimmed.startsWith('{') || rawTrimmed.startsWith('[');
616
+ if (extracted.failureReason) {
617
+ return { ok: false, reason: extracted.failureReason };
110
618
  }
111
- return { ok: false, reason: 'invalid_patch_payload' };
619
+ if (looksJsonContainer &&
620
+ isRecord(rawArgsAny) &&
621
+ Object.keys(rawArgsAny).length === 0 &&
622
+ rawTrimmed.length > 0 &&
623
+ !looksLikePatch(rawTrimmed)) {
624
+ return { ok: false, reason: 'invalid_json' };
625
+ }
626
+ return { ok: false, reason: 'missing_changes' };
112
627
  }
628
+ return { ok: true, normalizedArgs: toJson({ patch: patchText, input: patchText }) };
113
629
  }
114
630
  case 'shell': {
631
+ const rawArgs = isRecord(rawArgsAny) ? rawArgsAny : {};
115
632
  const rawCommand = rawArgs.command;
116
633
  let normalizedCommand = null;
117
634
  if (typeof rawCommand === 'string') {
@@ -153,6 +670,7 @@ export function validateToolCall(name, argsString) {
153
670
  return { ok: true, normalizedArgs: toJson(shellArgs) };
154
671
  }
155
672
  case 'update_plan': {
673
+ const rawArgs = isRecord(rawArgsAny) ? rawArgsAny : {};
156
674
  if (!Array.isArray(rawArgs.plan)) {
157
675
  return { ok: false, reason: 'missing_plan' };
158
676
  }
@@ -174,6 +692,7 @@ export function validateToolCall(name, argsString) {
174
692
  };
175
693
  }
176
694
  case 'view_image': {
695
+ const rawArgs = isRecord(rawArgsAny) ? rawArgsAny : {};
177
696
  const pathValue = asString(rawArgs.path);
178
697
  if (!pathValue || !isImagePath(pathValue)) {
179
698
  return { ok: false, reason: 'invalid_image_path' };
@@ -181,6 +700,7 @@ export function validateToolCall(name, argsString) {
181
700
  return { ok: true, normalizedArgs: toJson({ path: pathValue }) };
182
701
  }
183
702
  case 'list_mcp_resources': {
703
+ const rawArgs = isRecord(rawArgsAny) ? rawArgsAny : {};
184
704
  const payload = {};
185
705
  const server = asString(rawArgs.server);
186
706
  if (server) {
@@ -197,6 +717,7 @@ export function validateToolCall(name, argsString) {
197
717
  return { ok: true, normalizedArgs: toJson(payload) };
198
718
  }
199
719
  case 'read_mcp_resource': {
720
+ const rawArgs = isRecord(rawArgsAny) ? rawArgsAny : {};
200
721
  const server = asString(rawArgs.server);
201
722
  const uri = asString(rawArgs.uri);
202
723
  if (!server || !uri) {
@@ -205,6 +726,7 @@ export function validateToolCall(name, argsString) {
205
726
  return { ok: true, normalizedArgs: toJson({ server, uri }) };
206
727
  }
207
728
  case 'list_mcp_resource_templates': {
729
+ const rawArgs = isRecord(rawArgsAny) ? rawArgsAny : {};
208
730
  const payload = {};
209
731
  const server = asString(rawArgs.server);
210
732
  if (server) {
@@ -0,0 +1,4 @@
1
+ export type ToonKeyValue = Record<string, string>;
2
+ export declare function decodeToonToKeyValue(toon: string): ToonKeyValue | null;
3
+ export declare function coerceToonValue(value: string): unknown;
4
+ export declare function decodeToonToJson(toon: string): Record<string, unknown> | null;
@@ -0,0 +1,75 @@
1
+ export function decodeToonToKeyValue(toon) {
2
+ try {
3
+ const output = {};
4
+ const lines = String(toon ?? '')
5
+ .replace(/\r\n/g, '\n')
6
+ .split('\n');
7
+ let currentKey = null;
8
+ let currentValue = '';
9
+ const flush = () => {
10
+ if (currentKey) {
11
+ output[currentKey] = currentValue;
12
+ }
13
+ currentKey = null;
14
+ currentValue = '';
15
+ };
16
+ for (const raw of lines) {
17
+ const match = raw.match(/^([A-Za-z0-9_\-]+)\s*:\s*(.*)$/);
18
+ if (match) {
19
+ flush();
20
+ currentKey = match[1];
21
+ currentValue = match[2] ?? '';
22
+ }
23
+ else if (currentKey) {
24
+ currentValue += (currentValue ? '\n' : '') + raw;
25
+ }
26
+ else if (raw.trim()) {
27
+ // Encountered non-empty line before any key was established → treat as invalid TOON.
28
+ return null;
29
+ }
30
+ }
31
+ flush();
32
+ return Object.keys(output).length ? output : null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ export function coerceToonValue(value) {
39
+ const trimmed = value.trim();
40
+ if (!trimmed) {
41
+ return '';
42
+ }
43
+ const lowered = trimmed.toLowerCase();
44
+ if (lowered === 'true')
45
+ return true;
46
+ if (lowered === 'false')
47
+ return false;
48
+ if (/^[+-]?\d+(\.\d+)?$/.test(trimmed)) {
49
+ const num = Number(trimmed);
50
+ if (Number.isFinite(num)) {
51
+ return num;
52
+ }
53
+ }
54
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
55
+ (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
56
+ try {
57
+ return JSON.parse(trimmed);
58
+ }
59
+ catch {
60
+ // fall through
61
+ }
62
+ }
63
+ return value;
64
+ }
65
+ export function decodeToonToJson(toon) {
66
+ const kv = decodeToonToKeyValue(toon);
67
+ if (!kv) {
68
+ return null;
69
+ }
70
+ const result = {};
71
+ for (const [key, value] of Object.entries(kv)) {
72
+ result[key] = coerceToonValue(value);
73
+ }
74
+ return result;
75
+ }