@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.
- 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 +130 -15
- 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/conversion/shared/tooling.d.ts +33 -0
- package/dist/conversion/shared/tooling.js +27 -0
- 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 +410 -67
- package/dist/filters/special/response-tool-arguments-stringify.js +25 -16
- package/dist/filters/special/response-tool-arguments-toon-decode.js +8 -76
- 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
|
@@ -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
|
|
39
|
-
for (const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
194
|
-
|
|
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] =
|
|
134
|
+
merged[key] = coerceToonValue(value);
|
|
203
135
|
}
|
|
204
136
|
try {
|
|
205
137
|
fn.arguments = JSON.stringify(merged);
|