@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.
- package/dist/conversion/codecs/anthropic-openai-codec.js +0 -5
- package/dist/conversion/codecs/openai-openai-codec.js +0 -6
- package/dist/conversion/codecs/responses-openai-codec.js +1 -7
- package/dist/conversion/hub/node-support.js +5 -4
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +14 -1
- package/dist/conversion/hub/pipeline/hub-pipeline.js +82 -18
- package/dist/conversion/hub/pipeline/session-identifiers.js +132 -2
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +23 -19
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +47 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +4 -2
- package/dist/conversion/hub/process/chat-process.js +2 -0
- package/dist/conversion/hub/response/provider-response.js +6 -1
- package/dist/conversion/hub/snapshot-recorder.js +8 -1
- package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +0 -7
- package/dist/conversion/responses/responses-openai-bridge.js +47 -7
- package/dist/conversion/shared/compaction-detect.d.ts +2 -0
- package/dist/conversion/shared/compaction-detect.js +53 -0
- package/dist/conversion/shared/errors.d.ts +1 -1
- package/dist/conversion/shared/reasoning-tool-normalizer.js +7 -0
- package/dist/conversion/shared/snapshot-hooks.d.ts +2 -0
- package/dist/conversion/shared/snapshot-hooks.js +180 -4
- package/dist/conversion/shared/snapshot-utils.d.ts +4 -0
- package/dist/conversion/shared/snapshot-utils.js +4 -0
- package/dist/conversion/shared/tool-filter-pipeline.js +3 -9
- package/dist/conversion/shared/tool-governor.d.ts +2 -0
- package/dist/conversion/shared/tool-governor.js +101 -13
- package/dist/conversion/shared/tool-harvester.js +42 -2
- package/dist/filters/index.d.ts +0 -2
- package/dist/filters/index.js +0 -2
- package/dist/filters/special/request-tools-normalize.d.ts +11 -0
- package/dist/filters/special/request-tools-normalize.js +13 -50
- package/dist/filters/special/response-apply-patch-toon-decode.js +403 -82
- package/dist/filters/special/response-tool-arguments-toon-decode.js +6 -75
- package/dist/filters/utils/snapshot-writer.js +42 -4
- package/dist/guidance/index.js +8 -2
- package/dist/router/virtual-router/engine-health.js +0 -4
- package/dist/router/virtual-router/engine-selection.d.ts +2 -1
- package/dist/router/virtual-router/engine-selection.js +101 -9
- package/dist/router/virtual-router/engine.d.ts +5 -1
- package/dist/router/virtual-router/engine.js +188 -5
- package/dist/router/virtual-router/routing-instructions.d.ts +6 -0
- package/dist/router/virtual-router/routing-instructions.js +18 -3
- package/dist/router/virtual-router/sticky-session-store.d.ts +1 -0
- package/dist/router/virtual-router/sticky-session-store.js +36 -0
- package/dist/router/virtual-router/types.d.ts +22 -0
- package/dist/servertool/engine.js +335 -9
- package/dist/servertool/handlers/compaction-detect.d.ts +1 -0
- package/dist/servertool/handlers/compaction-detect.js +1 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +29 -5
- package/dist/servertool/handlers/iflow-model-error-retry.js +17 -0
- package/dist/servertool/handlers/stop-message-auto.js +199 -19
- package/dist/servertool/server-side-tools.d.ts +0 -1
- package/dist/servertool/server-side-tools.js +0 -1
- package/dist/servertool/types.d.ts +1 -0
- package/dist/tools/apply-patch-structured.js +52 -15
- package/dist/tools/tool-registry.js +537 -15
- package/dist/utils/toon.d.ts +4 -0
- package/dist/utils/toon.js +75 -0
- package/package.json +4 -2
- package/dist/test-output/virtual-router/results.json +0 -1
- 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(
|
|
33
|
-
|
|
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
|
|
608
|
+
const rawArgsAny = tryParseJson(typeof argsString === 'string' ? argsString : '{}');
|
|
97
609
|
switch (normalizedName) {
|
|
98
610
|
case 'apply_patch': {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
+
}
|