@jsonstudio/llms 0.6.753 → 0.6.802
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/compat/actions/apply-patch-fixer.d.ts +1 -0
- package/dist/conversion/compat/actions/apply-patch-fixer.js +30 -0
- package/dist/conversion/compat/actions/apply-patch-format-fixer.d.ts +1 -0
- package/dist/conversion/compat/actions/apply-patch-format-fixer.js +233 -0
- package/dist/conversion/compat/actions/index.d.ts +2 -0
- package/dist/conversion/compat/actions/index.js +2 -0
- package/dist/conversion/compat/profiles/chat-gemini.json +15 -15
- package/dist/conversion/compat/profiles/chat-glm.json +194 -194
- package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
- package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/dist/conversion/compat/profiles/responses-output2choices-test.json +10 -9
- package/dist/conversion/hub/pipeline/context-limit.d.ts +13 -0
- package/dist/conversion/hub/pipeline/context-limit.js +55 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +35 -0
- package/dist/conversion/shared/bridge-message-utils.d.ts +1 -0
- package/dist/conversion/shared/bridge-message-utils.js +7 -0
- package/dist/conversion/shared/bridge-policies.js +8 -8
- package/dist/conversion/shared/snapshot-hooks.js +54 -1
- package/dist/conversion/shared/tool-governor.js +18 -23
- package/dist/filters/special/response-tool-arguments-stringify.js +3 -22
- package/dist/router/virtual-router/engine-selection.js +49 -4
- package/dist/router/virtual-router/engine.d.ts +5 -0
- package/dist/router/virtual-router/engine.js +21 -0
- package/dist/tools/apply-patch/regression-capturer.d.ts +12 -0
- package/dist/tools/apply-patch/regression-capturer.js +112 -0
- package/dist/tools/apply-patch/structured.d.ts +20 -0
- package/dist/tools/apply-patch/structured.js +441 -0
- package/dist/tools/apply-patch/validator.d.ts +8 -0
- package/dist/tools/apply-patch/validator.js +616 -0
- package/dist/tools/apply-patch-structured.d.ts +1 -20
- package/dist/tools/apply-patch-structured.js +1 -277
- package/dist/tools/args-json.d.ts +1 -0
- package/dist/tools/args-json.js +175 -0
- package/dist/tools/exec-command/normalize.d.ts +17 -0
- package/dist/tools/exec-command/normalize.js +112 -0
- package/dist/tools/exec-command/regression-capturer.d.ts +11 -0
- package/dist/tools/exec-command/regression-capturer.js +144 -0
- package/dist/tools/exec-command/validator.d.ts +6 -0
- package/dist/tools/exec-command/validator.js +22 -0
- package/dist/tools/patch-args-normalizer.d.ts +15 -0
- package/dist/tools/patch-args-normalizer.js +472 -0
- package/dist/tools/patch-regression-capturer.d.ts +1 -0
- package/dist/tools/patch-regression-capturer.js +1 -0
- package/dist/tools/tool-registry.js +36 -541
- package/package.json +1 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// Tool registry and validator (single source of truth)
|
|
2
|
-
import {
|
|
2
|
+
import { parseToolArgsJson } from './args-json.js';
|
|
3
|
+
import { validateApplyPatchArgs } from './apply-patch/validator.js';
|
|
4
|
+
import { captureApplyPatchRegression } from './patch-regression-capturer.js';
|
|
5
|
+
import { validateExecCommandArgs } from './exec-command/validator.js';
|
|
6
|
+
import { captureExecCommandRegression } from './exec-command/regression-capturer.js';
|
|
3
7
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
8
|
const asString = (value) => {
|
|
5
9
|
if (typeof value === 'string' && value.trim().length > 0) {
|
|
@@ -27,471 +31,7 @@ const readNumber = (value) => {
|
|
|
27
31
|
}
|
|
28
32
|
return value;
|
|
29
33
|
};
|
|
30
|
-
|
|
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
|
-
};
|
|
123
|
-
const tryParseJson = (input) => {
|
|
124
|
-
const raw = typeof input === 'string' ? input : '';
|
|
125
|
-
if (!raw.trim())
|
|
126
|
-
return {};
|
|
127
|
-
try {
|
|
128
|
-
const parsed = JSON.parse(raw);
|
|
129
|
-
repairArgKeyArtifactsInObject(parsed);
|
|
130
|
-
return parsed;
|
|
131
|
-
}
|
|
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
|
-
}
|
|
158
|
-
return {};
|
|
159
|
-
}
|
|
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
|
-
};
|
|
34
|
+
// JSON parsing/repair helpers are shared via tools/args-json.ts
|
|
495
35
|
const toJson = (value) => {
|
|
496
36
|
try {
|
|
497
37
|
return JSON.stringify(value ?? {});
|
|
@@ -500,62 +40,6 @@ const toJson = (value) => {
|
|
|
500
40
|
return '{}';
|
|
501
41
|
}
|
|
502
42
|
};
|
|
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
|
-
};
|
|
559
43
|
const detectForbiddenWrite = (script) => {
|
|
560
44
|
const normalized = script.toLowerCase();
|
|
561
45
|
if (!normalized) {
|
|
@@ -588,6 +72,7 @@ const isImagePath = (value) => {
|
|
|
588
72
|
export function getAllowedToolNames() {
|
|
589
73
|
return [
|
|
590
74
|
'shell',
|
|
75
|
+
'exec_command',
|
|
591
76
|
'apply_patch',
|
|
592
77
|
'update_plan',
|
|
593
78
|
'view_image',
|
|
@@ -605,27 +90,37 @@ export function validateToolCall(name, argsString) {
|
|
|
605
90
|
if (!allowed.has(normalizedName)) {
|
|
606
91
|
return { ok: false, reason: 'unknown_tool' };
|
|
607
92
|
}
|
|
608
|
-
const rawArgsAny =
|
|
93
|
+
const rawArgsAny = parseToolArgsJson(typeof argsString === 'string' ? argsString : '{}');
|
|
609
94
|
switch (normalizedName) {
|
|
95
|
+
case 'exec_command': {
|
|
96
|
+
const validation = validateExecCommandArgs(argsString, rawArgsAny);
|
|
97
|
+
if (!validation.ok) {
|
|
98
|
+
const reason = validation.reason || 'unknown';
|
|
99
|
+
captureExecCommandRegression({
|
|
100
|
+
errorType: reason,
|
|
101
|
+
originalArgs: typeof argsString === 'string' ? argsString : String(argsString ?? ''),
|
|
102
|
+
normalizedArgs: typeof argsString === 'string' ? argsString : String(argsString ?? ''),
|
|
103
|
+
validationError: reason,
|
|
104
|
+
source: 'tool-registry.validateToolCall'
|
|
105
|
+
});
|
|
106
|
+
return { ok: false, reason };
|
|
107
|
+
}
|
|
108
|
+
return { ok: true, normalizedArgs: validation.normalizedArgs };
|
|
109
|
+
}
|
|
610
110
|
case 'apply_patch': {
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
return { ok: false, reason: 'invalid_json' };
|
|
625
|
-
}
|
|
626
|
-
return { ok: false, reason: 'missing_changes' };
|
|
627
|
-
}
|
|
628
|
-
return { ok: true, normalizedArgs: toJson({ patch: patchText, input: patchText }) };
|
|
111
|
+
const validation = validateApplyPatchArgs(argsString, rawArgsAny);
|
|
112
|
+
if (!validation.ok) {
|
|
113
|
+
const reason = validation.reason || 'unknown';
|
|
114
|
+
captureApplyPatchRegression({
|
|
115
|
+
errorType: reason,
|
|
116
|
+
originalArgs: typeof argsString === 'string' ? argsString : String(argsString ?? ''),
|
|
117
|
+
normalizedArgs: typeof argsString === 'string' ? argsString : String(argsString ?? ''),
|
|
118
|
+
validationError: reason,
|
|
119
|
+
source: 'tool-registry.validateToolCall'
|
|
120
|
+
});
|
|
121
|
+
return { ok: false, reason };
|
|
122
|
+
}
|
|
123
|
+
return { ok: true, normalizedArgs: validation.normalizedArgs };
|
|
629
124
|
}
|
|
630
125
|
case 'shell': {
|
|
631
126
|
const rawArgs = isRecord(rawArgsAny) ? rawArgsAny : {};
|