@r3b1s/pi-repair-layer 0.2.1 → 0.3.0
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/README.md +159 -293
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/{index.ts → dist/index.js} +2 -1
- package/dist/index.js.map +1 -0
- package/dist/src/core.d.ts +8 -0
- package/dist/src/core.d.ts.map +1 -0
- package/dist/src/core.js +7 -0
- package/dist/src/core.js.map +1 -0
- package/dist/src/envelope.d.ts +21 -0
- package/dist/src/envelope.d.ts.map +1 -0
- package/dist/src/envelope.js +158 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/grammar-recovery.d.ts +105 -0
- package/dist/src/grammar-recovery.d.ts.map +1 -0
- package/dist/src/grammar-recovery.js +1052 -0
- package/dist/src/grammar-recovery.js.map +1 -0
- package/dist/src/grammar.d.ts +2 -0
- package/dist/src/grammar.d.ts.map +1 -0
- package/dist/src/grammar.js +2 -0
- package/dist/src/grammar.js.map +1 -0
- package/dist/src/index.d.ts +36 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +558 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lifecycle.d.ts +34 -0
- package/dist/src/lifecycle.d.ts.map +1 -0
- package/dist/src/lifecycle.js +133 -0
- package/dist/src/lifecycle.js.map +1 -0
- package/dist/src/pi.d.ts +19 -0
- package/dist/src/pi.d.ts.map +1 -0
- package/dist/src/pi.js +38 -0
- package/dist/src/pi.js.map +1 -0
- package/dist/src/pipeline.d.ts +10 -0
- package/dist/src/pipeline.d.ts.map +1 -0
- package/dist/src/pipeline.js +166 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/policy.d.ts +16 -0
- package/dist/src/policy.d.ts.map +1 -0
- package/dist/src/policy.js +31 -0
- package/dist/src/policy.js.map +1 -0
- package/dist/src/preprocess.d.ts +37 -0
- package/dist/src/preprocess.d.ts.map +1 -0
- package/dist/src/preprocess.js +216 -0
- package/dist/src/preprocess.js.map +1 -0
- package/dist/src/repair-engine.d.ts +91 -0
- package/dist/src/repair-engine.d.ts.map +1 -0
- package/dist/src/repair-engine.js +457 -0
- package/dist/src/repair-engine.js.map +1 -0
- package/dist/src/settings.d.ts +38 -0
- package/dist/src/settings.d.ts.map +1 -0
- package/dist/src/settings.js +87 -0
- package/dist/src/settings.js.map +1 -0
- package/dist/src/tables.d.ts +16 -0
- package/dist/src/tables.d.ts.map +1 -0
- package/dist/src/tables.js +302 -0
- package/dist/src/tables.js.map +1 -0
- package/dist/src/types.d.ts +52 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/value-strips.d.ts +52 -0
- package/dist/src/value-strips.d.ts.map +1 -0
- package/dist/src/value-strips.js +191 -0
- package/dist/src/value-strips.js.map +1 -0
- package/docs/how-it-works.md +227 -0
- package/docs/operations.md +141 -0
- package/docs/research.md +247 -0
- package/docs/tool-owner-integration.md +327 -0
- package/package.json +31 -6
- package/src/grammar-recovery.ts +0 -1269
- package/src/index.ts +0 -621
- package/src/repair-engine.ts +0 -518
- package/src/settings.ts +0 -95
- package/src/tables.ts +0 -184
- package/src/value-strips.ts +0 -218
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grammar-leak recovery — adapted in place from monotykamary/pi-tool-repair
|
|
3
|
+
* (MIT), `src/grammar-repair.ts`. The grammar-family parsers, candidate
|
|
4
|
+
* selection, code-fence awareness, and `removeRanges` are kept close to
|
|
5
|
+
* upstream; the driver (`recoverGrammarLeaks`) is rewritten to integrate this
|
|
6
|
+
* extension's settings, model gate, and telemetry, and to add a stopReason gate
|
|
7
|
+
* upstream lacks.
|
|
8
|
+
*
|
|
9
|
+
* What it does: some models print a tool call as literal text (10 grammar
|
|
10
|
+
* families — DSML, Qwen/GLM XML, Kimi/Mistral/Llama/MiniMax sentinels, etc.)
|
|
11
|
+
* instead of emitting a real tool call. On the `message_end` hook this detects
|
|
12
|
+
* those leaked blocks, strips them from the assistant text, and — in `recover`
|
|
13
|
+
* mode — promotes the parsed calls to real toolCalls that execute the same turn.
|
|
14
|
+
*
|
|
15
|
+
* The one deliberate improvement over upstream: promotion happens ONLY when the
|
|
16
|
+
* original message's `stopReason` is `"stop"` (see `recoverGrammarLeaks`).
|
|
17
|
+
* Upstream promotes regardless and overwrites `stopReason: "length"`, defeating
|
|
18
|
+
* pi's protection that fails all tool calls on truncated output
|
|
19
|
+
* (docs/research.md Claim 7). Stripping leaked text is permitted on any
|
|
20
|
+
* stopReason; only promotion is gated.
|
|
21
|
+
*/
|
|
22
|
+
export const GRAMMAR_NAMES = [
|
|
23
|
+
"dsml",
|
|
24
|
+
"invoke",
|
|
25
|
+
"qwen",
|
|
26
|
+
"kimi",
|
|
27
|
+
"mistral",
|
|
28
|
+
"llama",
|
|
29
|
+
"glm",
|
|
30
|
+
"granite",
|
|
31
|
+
"minimax-text",
|
|
32
|
+
"olmo",
|
|
33
|
+
];
|
|
34
|
+
const ALL_GRAMMARS = [...GRAMMAR_NAMES];
|
|
35
|
+
/**
|
|
36
|
+
* Open-model families known to print tool-call grammar as text. Grammar
|
|
37
|
+
* recovery runs only when the current model matches one of these — a heuristic
|
|
38
|
+
* gate that keeps the parsers off frontier models (Claude/GPT/Gemini) that emit
|
|
39
|
+
* native tool calls and may legitimately quote grammar in prose. The real
|
|
40
|
+
* safety is the known-tool / empty-args / stopReason gates; this list is the
|
|
41
|
+
* cheap first filter and can be loosened as telemetry warrants.
|
|
42
|
+
*/
|
|
43
|
+
export const GRAMMAR_RECOVERY_MODELS = [
|
|
44
|
+
/glm/i,
|
|
45
|
+
/kimi/i,
|
|
46
|
+
/minimax/i,
|
|
47
|
+
/qwen/i,
|
|
48
|
+
/mistral/i,
|
|
49
|
+
/llama/i,
|
|
50
|
+
/granite/i,
|
|
51
|
+
/olmo/i,
|
|
52
|
+
/deepseek/i,
|
|
53
|
+
];
|
|
54
|
+
/** Whether grammar recovery's model gate matches the given model id. */
|
|
55
|
+
export function modelLeaksGrammar(modelId) {
|
|
56
|
+
if (!modelId)
|
|
57
|
+
return false;
|
|
58
|
+
return GRAMMAR_RECOVERY_MODELS.some((re) => re.test(modelId));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Detect leaked tool-call grammar in an assistant message, strip it from the
|
|
62
|
+
* text, and (in `recover` mode, on a `stopReason: "stop"` message with no
|
|
63
|
+
* existing toolCalls) promote the parsed calls to real toolCalls.
|
|
64
|
+
*/
|
|
65
|
+
export function recoverGrammarLeaks(message, options) {
|
|
66
|
+
const unchanged = {
|
|
67
|
+
changed: false,
|
|
68
|
+
recoveredCalls: [],
|
|
69
|
+
strippedGrammars: [],
|
|
70
|
+
strippedRanges: 0,
|
|
71
|
+
promoted: false,
|
|
72
|
+
observed: false,
|
|
73
|
+
detectedGrammars: [],
|
|
74
|
+
detectedRanges: 0,
|
|
75
|
+
promotedCallIds: [],
|
|
76
|
+
message,
|
|
77
|
+
};
|
|
78
|
+
if (options.mode === "off" ||
|
|
79
|
+
message.role !== "assistant" ||
|
|
80
|
+
!Array.isArray(message.content)) {
|
|
81
|
+
return unchanged;
|
|
82
|
+
}
|
|
83
|
+
const enabled = new Set(options.grammars ?? ALL_GRAMMARS);
|
|
84
|
+
const requireKnownTool = options.requireKnownTool ?? true;
|
|
85
|
+
const unknownToolText = options.unknownToolText ?? "preserve";
|
|
86
|
+
const existingToolCalls = message.content.filter(isToolCallContent);
|
|
87
|
+
const recoveredCalls = [];
|
|
88
|
+
const strippedGrammars = new Set();
|
|
89
|
+
const detectedGrammars = new Set();
|
|
90
|
+
let detectedRanges = 0;
|
|
91
|
+
let strippedRanges = 0;
|
|
92
|
+
let changed = false;
|
|
93
|
+
const nextContent = message.content.map((part) => {
|
|
94
|
+
const text = getPartText(part);
|
|
95
|
+
if (text === undefined)
|
|
96
|
+
return part;
|
|
97
|
+
const detected = selectCandidates(parseToolGrammarCandidates(text, enabled));
|
|
98
|
+
if (detected.length === 0)
|
|
99
|
+
return part;
|
|
100
|
+
detectedRanges += detected.length;
|
|
101
|
+
for (const candidate of detected)
|
|
102
|
+
detectedGrammars.add(candidate.grammar);
|
|
103
|
+
if (options.mode === "observe")
|
|
104
|
+
return part;
|
|
105
|
+
const candidates = detected.filter((candidate) => {
|
|
106
|
+
if (candidate.stripOnly)
|
|
107
|
+
return true;
|
|
108
|
+
if (isAllowedTool(candidate.name, requireKnownTool, options.knownTools)) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return unknownToolText === "strip";
|
|
112
|
+
});
|
|
113
|
+
if (candidates.length === 0)
|
|
114
|
+
return part;
|
|
115
|
+
strippedRanges += candidates.length;
|
|
116
|
+
changed = true;
|
|
117
|
+
for (const candidate of candidates) {
|
|
118
|
+
strippedGrammars.add(candidate.grammar);
|
|
119
|
+
if (candidate.stripOnly ||
|
|
120
|
+
!isAllowedTool(candidate.name, requireKnownTool, options.knownTools)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Safety: a candidate that parsed to an empty argument object is almost
|
|
124
|
+
// always a malformed fragment (e.g. `<tool_call>write</tool_call>`); it
|
|
125
|
+
// would crash as a real call with missing required properties. Skip it.
|
|
126
|
+
if (Object.keys(candidate.arguments).length === 0)
|
|
127
|
+
continue;
|
|
128
|
+
recoveredCalls.push({
|
|
129
|
+
name: candidate.name,
|
|
130
|
+
arguments: candidate.arguments,
|
|
131
|
+
grammar: candidate.grammar,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const strippedText = removeRanges(text, candidates.map((candidate) => candidate.range));
|
|
135
|
+
return setPartText(part, strippedText);
|
|
136
|
+
});
|
|
137
|
+
// Promotion gate: recover mode, no existing real toolCalls, something to
|
|
138
|
+
// promote, AND the original stopReason is "stop" (never overwrite
|
|
139
|
+
// "length"/"error"/"aborted"). Stripping above is allowed regardless.
|
|
140
|
+
const shouldRecover = options.mode === "recover" &&
|
|
141
|
+
existingToolCalls.length === 0 &&
|
|
142
|
+
recoveredCalls.length > 0 &&
|
|
143
|
+
message.stopReason === "stop";
|
|
144
|
+
if (!changed && !shouldRecover) {
|
|
145
|
+
if (detectedRanges === 0)
|
|
146
|
+
return unchanged;
|
|
147
|
+
return {
|
|
148
|
+
...unchanged,
|
|
149
|
+
observed: true,
|
|
150
|
+
detectedGrammars: [...detectedGrammars],
|
|
151
|
+
detectedRanges,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (shouldRecover) {
|
|
155
|
+
let index = 0;
|
|
156
|
+
for (const call of recoveredCalls) {
|
|
157
|
+
const id = makeRecoveredToolCallId(call.grammar, index++);
|
|
158
|
+
nextContent.push({
|
|
159
|
+
type: "toolCall",
|
|
160
|
+
id,
|
|
161
|
+
name: call.name,
|
|
162
|
+
arguments: call.arguments,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const nextMessage = {
|
|
167
|
+
...message,
|
|
168
|
+
content: nextContent,
|
|
169
|
+
};
|
|
170
|
+
if (shouldRecover)
|
|
171
|
+
nextMessage.stopReason = "toolUse";
|
|
172
|
+
return {
|
|
173
|
+
changed: changed || shouldRecover,
|
|
174
|
+
recoveredCalls: shouldRecover ? recoveredCalls : [],
|
|
175
|
+
strippedGrammars: [...strippedGrammars],
|
|
176
|
+
strippedRanges,
|
|
177
|
+
promoted: shouldRecover,
|
|
178
|
+
observed: detectedRanges > 0,
|
|
179
|
+
detectedGrammars: [...detectedGrammars],
|
|
180
|
+
detectedRanges,
|
|
181
|
+
promotedCallIds: shouldRecover
|
|
182
|
+
? nextContent
|
|
183
|
+
.filter(isToolCallContent)
|
|
184
|
+
.slice(existingToolCalls.length)
|
|
185
|
+
.map((call) => call.id)
|
|
186
|
+
: [],
|
|
187
|
+
message: nextMessage,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** Pure parse: leaked tool calls in text (no stripping, no gates). For tests. */
|
|
191
|
+
export function parseToolGrammarLeaks(text, grammars = ALL_GRAMMARS) {
|
|
192
|
+
const enabled = new Set(grammars);
|
|
193
|
+
return selectCandidates(parseToolGrammarCandidates(text, enabled))
|
|
194
|
+
.filter((candidate) => !candidate.stripOnly)
|
|
195
|
+
.map((candidate) => ({
|
|
196
|
+
name: candidate.name,
|
|
197
|
+
arguments: candidate.arguments,
|
|
198
|
+
grammar: candidate.grammar,
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
// ─── Grammar-family parsers (adapted verbatim from upstream) ──────────────────
|
|
202
|
+
function parseToolGrammarCandidates(text, enabled) {
|
|
203
|
+
const candidates = [];
|
|
204
|
+
if (enabled.has("dsml")) {
|
|
205
|
+
candidates.push(...parseDsml(text));
|
|
206
|
+
candidates.push(...parseDsmlDanglingMarkers(text));
|
|
207
|
+
}
|
|
208
|
+
if (enabled.has("kimi"))
|
|
209
|
+
candidates.push(...parseKimi(text));
|
|
210
|
+
if (enabled.has("mistral")) {
|
|
211
|
+
candidates.push(...parseMistral(text));
|
|
212
|
+
candidates.push(...parseBareJsonToolCalls(text, "mistral"));
|
|
213
|
+
}
|
|
214
|
+
if (enabled.has("minimax-text"))
|
|
215
|
+
candidates.push(...parseMiniMaxText01(text));
|
|
216
|
+
if (enabled.has("invoke"))
|
|
217
|
+
candidates.push(...parseInvokeXml(text));
|
|
218
|
+
if (enabled.has("qwen") || enabled.has("glm") || enabled.has("granite")) {
|
|
219
|
+
candidates.push(...parseToolCallXml(text, enabled));
|
|
220
|
+
}
|
|
221
|
+
if (enabled.has("granite"))
|
|
222
|
+
candidates.push(...parseBarePythonicToolCalls(text, "granite"));
|
|
223
|
+
if (enabled.has("llama")) {
|
|
224
|
+
candidates.push(...parseLlamaPythonTag(text));
|
|
225
|
+
candidates.push(...parseBareJsonToolCalls(text, "llama"));
|
|
226
|
+
}
|
|
227
|
+
if (enabled.has("olmo"))
|
|
228
|
+
candidates.push(...parseOlmo(text));
|
|
229
|
+
return candidates.filter((candidate) => candidate.range.end > candidate.range.start);
|
|
230
|
+
}
|
|
231
|
+
function parseDsml(text) {
|
|
232
|
+
const candidates = [];
|
|
233
|
+
const prefix = "(?:|{1,2}DSML|{1,2}|DSML||\\s*\\|\\s*DSML\\s*\\|\\s*)";
|
|
234
|
+
const outerOpen = new RegExp(`<${prefix}(?:tool_calls|function_calls)>`, "giu");
|
|
235
|
+
for (const match of text.matchAll(outerOpen)) {
|
|
236
|
+
if (match.index === undefined || isInsideCodeFence(text, match.index))
|
|
237
|
+
continue;
|
|
238
|
+
const start = match.index;
|
|
239
|
+
const bodyStart = start + match[0].length;
|
|
240
|
+
const close = findDsmlClose(text, bodyStart, "tool_calls") ??
|
|
241
|
+
findDsmlClose(text, bodyStart, "function_calls");
|
|
242
|
+
const end = close ? close.end : findBestUnclosedDsmlEnd(text, bodyStart);
|
|
243
|
+
if (end === undefined)
|
|
244
|
+
continue;
|
|
245
|
+
const body = text.slice(bodyStart, close ? close.start : end);
|
|
246
|
+
const calls = parseDsmlInvokes(body);
|
|
247
|
+
for (const call of calls) {
|
|
248
|
+
candidates.push({ ...call, grammar: "dsml", range: { start, end } });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return candidates;
|
|
252
|
+
}
|
|
253
|
+
function parseDsmlDanglingMarkers(text) {
|
|
254
|
+
if (!text.includes("DSML"))
|
|
255
|
+
return [];
|
|
256
|
+
const prefix = "(?:|{1,2}DSML|{1,2}|DSML||\\s*\\|\\s*DSML\\s*\\|\\s*)";
|
|
257
|
+
const markerRe = new RegExp(`</?${prefix}(?:tool_calls|function_calls|invoke|parameter)(?:\\s+[^>\\n]*)?>?`, "giu");
|
|
258
|
+
const candidates = [];
|
|
259
|
+
for (const match of text.matchAll(markerRe)) {
|
|
260
|
+
if (match.index === undefined)
|
|
261
|
+
continue;
|
|
262
|
+
if (isInsideCodeFence(text, match.index))
|
|
263
|
+
continue;
|
|
264
|
+
candidates.push({
|
|
265
|
+
name: "",
|
|
266
|
+
arguments: {},
|
|
267
|
+
grammar: "dsml",
|
|
268
|
+
range: { start: match.index, end: match.index + match[0].length },
|
|
269
|
+
stripOnly: true,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return candidates;
|
|
273
|
+
}
|
|
274
|
+
function findDsmlClose(text, from, outerName) {
|
|
275
|
+
const prefix = "(?:|{1,2}DSML|{1,2}|DSML||\\s*\\|\\s*DSML\\s*\\|\\s*)";
|
|
276
|
+
const closeRe = new RegExp(`</${prefix}${outerName}>`, "giu");
|
|
277
|
+
closeRe.lastIndex = from;
|
|
278
|
+
const match = closeRe.exec(text);
|
|
279
|
+
return match && match.index >= from
|
|
280
|
+
? { start: match.index, end: match.index + match[0].length }
|
|
281
|
+
: undefined;
|
|
282
|
+
}
|
|
283
|
+
function findBestUnclosedDsmlEnd(text, from) {
|
|
284
|
+
const invokeClose = /<\/(?:|{1,2}DSML|{1,2}|DSML||\s*\|\s*DSML\s*\|\s*)invoke>/giu;
|
|
285
|
+
invokeClose.lastIndex = from;
|
|
286
|
+
let end;
|
|
287
|
+
for (;;) {
|
|
288
|
+
const match = invokeClose.exec(text);
|
|
289
|
+
if (!match)
|
|
290
|
+
break;
|
|
291
|
+
end = match.index + match[0].length;
|
|
292
|
+
}
|
|
293
|
+
return end;
|
|
294
|
+
}
|
|
295
|
+
function parseDsmlInvokes(body) {
|
|
296
|
+
const calls = [];
|
|
297
|
+
const prefix = "(?:|{1,2}DSML|{1,2}|DSML||\\s*\\|\\s*DSML\\s*\\|\\s*)";
|
|
298
|
+
const invokeRe = new RegExp(`<${prefix}invoke\\s+name=["']([^"']+)["']\\s*>`, "giu");
|
|
299
|
+
for (const match of body.matchAll(invokeRe)) {
|
|
300
|
+
if (match.index === undefined)
|
|
301
|
+
continue;
|
|
302
|
+
const name = match[1]?.trim();
|
|
303
|
+
if (!name)
|
|
304
|
+
continue;
|
|
305
|
+
const invokeBodyStart = match.index + match[0].length;
|
|
306
|
+
const close = findPattern(body, new RegExp(`</${prefix}invoke>`, "iu"), invokeBodyStart);
|
|
307
|
+
if (!close)
|
|
308
|
+
continue;
|
|
309
|
+
const invokeBody = body.slice(invokeBodyStart, close.start);
|
|
310
|
+
calls.push({ name, arguments: parseDsmlArguments(invokeBody) });
|
|
311
|
+
}
|
|
312
|
+
return calls;
|
|
313
|
+
}
|
|
314
|
+
function parseDsmlArguments(body) {
|
|
315
|
+
const args = {};
|
|
316
|
+
const prefix = "(?:|{1,2}DSML|{1,2}|DSML||\\s*\\|\\s*DSML\\s*\\|\\s*)";
|
|
317
|
+
const paramRe = new RegExp(`<${prefix}parameter\\s+name=["']([^"']+)["'](?:\\s+string=["'](true|false)["'])?\\s*>([\\s\\S]*?)</${prefix}parameter>`, "giu");
|
|
318
|
+
for (const match of body.matchAll(paramRe)) {
|
|
319
|
+
const key = match[1]?.trim();
|
|
320
|
+
if (!key)
|
|
321
|
+
continue;
|
|
322
|
+
const stringAttr = match[2];
|
|
323
|
+
const rawValue = match[3] ?? "";
|
|
324
|
+
args[key] =
|
|
325
|
+
stringAttr === "false"
|
|
326
|
+
? parseJsonValueOrString(rawValue.trim())
|
|
327
|
+
: rawValue;
|
|
328
|
+
}
|
|
329
|
+
if (Object.keys(args).length > 0)
|
|
330
|
+
return args;
|
|
331
|
+
const direct = parseJsonObject(extractFirstBalancedJson(body.trim())?.json ?? body.trim());
|
|
332
|
+
return normalizeArgumentsObject(direct) ?? {};
|
|
333
|
+
}
|
|
334
|
+
function parseKimi(text) {
|
|
335
|
+
const candidates = [];
|
|
336
|
+
const sectionRe = /<\|tool_calls?_section_begin\|>([\s\S]*?)<\|tool_calls?_section_end\|>/gi;
|
|
337
|
+
for (const section of text.matchAll(sectionRe)) {
|
|
338
|
+
if (section.index === undefined || isInsideCodeFence(text, section.index))
|
|
339
|
+
continue;
|
|
340
|
+
const sectionStart = section.index;
|
|
341
|
+
const body = section[1] ?? "";
|
|
342
|
+
const callRe = /<\|tool_call_begin\|>([^<]*?)<\|tool_call_argument_begin\|>([\s\S]*?)<\|tool_call_end\|>/gi;
|
|
343
|
+
for (const call of body.matchAll(callRe)) {
|
|
344
|
+
const idText = (call[1] ?? "").trim();
|
|
345
|
+
const name = parseKimiToolName(idText);
|
|
346
|
+
if (!name)
|
|
347
|
+
continue;
|
|
348
|
+
const args = parseJsonObject(call[2]?.trim() ?? "") ?? {};
|
|
349
|
+
candidates.push({
|
|
350
|
+
name,
|
|
351
|
+
arguments: args,
|
|
352
|
+
grammar: "kimi",
|
|
353
|
+
range: { start: sectionStart, end: sectionStart + section[0].length },
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return candidates;
|
|
358
|
+
}
|
|
359
|
+
function parseKimiToolName(idText) {
|
|
360
|
+
const canonical = /^functions\.([A-Za-z_][\w.-]*):\d+$/.exec(idText);
|
|
361
|
+
if (canonical)
|
|
362
|
+
return canonical[1];
|
|
363
|
+
const relaxed = /^(?:functions\.)?([A-Za-z_][\w.-]*)(?::\d+)?$/.exec(idText);
|
|
364
|
+
if (relaxed && !/^call[_-]?\d+$/i.test(relaxed[1] ?? ""))
|
|
365
|
+
return relaxed[1];
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
function parseMistral(text) {
|
|
369
|
+
const candidates = [];
|
|
370
|
+
const marker = "[TOOL_CALLS]";
|
|
371
|
+
let index = 0;
|
|
372
|
+
for (index = text.indexOf(marker, index); index !== -1; index = text.indexOf(marker, index)) {
|
|
373
|
+
if (isInsideCodeFence(text, index)) {
|
|
374
|
+
index += marker.length;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const afterMarker = index + marker.length;
|
|
378
|
+
const rest = text.slice(afterMarker).trimStart();
|
|
379
|
+
const whitespace = text.slice(afterMarker).length - rest.length;
|
|
380
|
+
const jsonStart = afterMarker + whitespace;
|
|
381
|
+
if (rest.startsWith("[")) {
|
|
382
|
+
const extracted = extractFirstBalancedJson(text.slice(jsonStart));
|
|
383
|
+
if (extracted?.json.startsWith("[")) {
|
|
384
|
+
for (const item of parseJsonArrayObjects(extracted.json)) {
|
|
385
|
+
const call = callFromJsonObject(item);
|
|
386
|
+
if (call) {
|
|
387
|
+
candidates.push({
|
|
388
|
+
...call,
|
|
389
|
+
grammar: "mistral",
|
|
390
|
+
range: { start: index, end: jsonStart + extracted.end },
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
index = jsonStart + extracted.end;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const v11 = /^([A-Za-z_][\w.-]*)\[CALL_ID\]([^[]*)\[ARGS\]/.exec(rest);
|
|
399
|
+
if (v11) {
|
|
400
|
+
const name = v11[1];
|
|
401
|
+
const argsStart = jsonStart + v11[0].length;
|
|
402
|
+
const extracted = extractFirstBalancedJson(text.slice(argsStart));
|
|
403
|
+
if (extracted) {
|
|
404
|
+
candidates.push({
|
|
405
|
+
name,
|
|
406
|
+
arguments: normalizeArgumentsObject(parseJsonObject(extracted.json)) ?? {},
|
|
407
|
+
grammar: "mistral",
|
|
408
|
+
range: { start: index, end: argsStart + extracted.end },
|
|
409
|
+
});
|
|
410
|
+
index = argsStart + extracted.end;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
index += marker.length;
|
|
415
|
+
}
|
|
416
|
+
return candidates;
|
|
417
|
+
}
|
|
418
|
+
function parseMiniMaxText01(text) {
|
|
419
|
+
const candidates = [];
|
|
420
|
+
const re = /<function_call>[\s\S]*?functions\.([A-Za-z_][\w.-]*)\s*\(/gi;
|
|
421
|
+
for (const match of text.matchAll(re)) {
|
|
422
|
+
if (match.index === undefined || isInsideCodeFence(text, match.index))
|
|
423
|
+
continue;
|
|
424
|
+
const name = match[1];
|
|
425
|
+
const openParen = match.index + match[0].length - 1;
|
|
426
|
+
const closeParen = findMatching(text, openParen, "(", ")");
|
|
427
|
+
if (closeParen === undefined)
|
|
428
|
+
continue;
|
|
429
|
+
const rawArgs = text.slice(openParen + 1, closeParen).trim();
|
|
430
|
+
const args = normalizeArgumentsObject(parseJsonObject(rawArgs)) ?? {};
|
|
431
|
+
const fenceEnd = text.indexOf("```", closeParen);
|
|
432
|
+
const end = fenceEnd === -1 ? closeParen + 1 : fenceEnd + 3;
|
|
433
|
+
candidates.push({
|
|
434
|
+
name,
|
|
435
|
+
arguments: args,
|
|
436
|
+
grammar: "minimax-text",
|
|
437
|
+
range: { start: match.index, end },
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
return candidates;
|
|
441
|
+
}
|
|
442
|
+
function parseInvokeXml(text) {
|
|
443
|
+
const candidates = [];
|
|
444
|
+
const wrappedRe = /<(?:[A-Za-z][\w.-]*:)?tool_call>([\s\S]*?)<\/(?:[A-Za-z][\w.-]*:)?tool_call>/gi;
|
|
445
|
+
for (const wrapper of text.matchAll(wrappedRe)) {
|
|
446
|
+
if (wrapper.index === undefined || isInsideCodeFence(text, wrapper.index))
|
|
447
|
+
continue;
|
|
448
|
+
const calls = parseInvokeBody(wrapper[1] ?? "");
|
|
449
|
+
for (const call of calls) {
|
|
450
|
+
candidates.push({
|
|
451
|
+
...call,
|
|
452
|
+
grammar: "invoke",
|
|
453
|
+
range: { start: wrapper.index, end: wrapper.index + wrapper[0].length },
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const standaloneRe = /<invoke\s+name=["']([^"']+)["']\s*>[\s\S]*?<\/invoke>/gi;
|
|
458
|
+
for (const match of text.matchAll(standaloneRe)) {
|
|
459
|
+
if (match.index === undefined || isInsideCodeFence(text, match.index))
|
|
460
|
+
continue;
|
|
461
|
+
const calls = parseInvokeBody(match[0]);
|
|
462
|
+
for (const call of calls) {
|
|
463
|
+
candidates.push({
|
|
464
|
+
...call,
|
|
465
|
+
grammar: "invoke",
|
|
466
|
+
range: { start: match.index, end: match.index + match[0].length },
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
candidates.push(...parseMalformedMiniMaxInvoke(text));
|
|
471
|
+
return candidates;
|
|
472
|
+
}
|
|
473
|
+
function parseInvokeBody(body) {
|
|
474
|
+
const calls = [];
|
|
475
|
+
const invokeRe = /<invoke\s+name=["']([^"']+)["']\s*>([\s\S]*?)<\/invoke>/gi;
|
|
476
|
+
for (const match of body.matchAll(invokeRe)) {
|
|
477
|
+
const name = match[1]?.trim();
|
|
478
|
+
if (!name)
|
|
479
|
+
continue;
|
|
480
|
+
calls.push({ name, arguments: parseInvokeArguments(match[2] ?? "") });
|
|
481
|
+
}
|
|
482
|
+
return calls;
|
|
483
|
+
}
|
|
484
|
+
function parseInvokeArguments(body) {
|
|
485
|
+
const args = {};
|
|
486
|
+
const paramRe = /<parameter\s+name=["']([^"']+)["'](?:\s+string=["'](true|false)["'])?\s*>([\s\S]*?)<\/parameter>/gi;
|
|
487
|
+
for (const match of body.matchAll(paramRe)) {
|
|
488
|
+
const key = match[1]?.trim();
|
|
489
|
+
if (!key)
|
|
490
|
+
continue;
|
|
491
|
+
const raw = match[3] ?? "";
|
|
492
|
+
args[key] =
|
|
493
|
+
match[2] === "false"
|
|
494
|
+
? parseJsonValueOrString(raw.trim())
|
|
495
|
+
: maybeParseJsonValue(raw.trim());
|
|
496
|
+
}
|
|
497
|
+
if (Object.keys(args).length > 0)
|
|
498
|
+
return args;
|
|
499
|
+
return normalizeArgumentsObject(parseJsonObject(body.trim())) ?? {};
|
|
500
|
+
}
|
|
501
|
+
function parseMalformedMiniMaxInvoke(text) {
|
|
502
|
+
const candidates = [];
|
|
503
|
+
const re = /(?:^|\n)(\s*)invoke\s+name=["']([^"']+)["']\s*>([\s\S]*?)(?:\n\s*(?:\/invoke|invoke)>|$)/gi;
|
|
504
|
+
for (const match of text.matchAll(re)) {
|
|
505
|
+
if (match.index === undefined || isInsideCodeFence(text, match.index))
|
|
506
|
+
continue;
|
|
507
|
+
const start = match.index + (match[0].startsWith("\n") ? 1 : 0);
|
|
508
|
+
const name = match[2]?.trim();
|
|
509
|
+
if (!name)
|
|
510
|
+
continue;
|
|
511
|
+
const args = {};
|
|
512
|
+
const paramRe = /parameter\s+name=["']([^"']+)["']\s*>([\s\S]*?)\s*parameter>/gi;
|
|
513
|
+
for (const param of (match[3] ?? "").matchAll(paramRe)) {
|
|
514
|
+
const key = param[1]?.trim();
|
|
515
|
+
if (key)
|
|
516
|
+
args[key] = maybeParseJsonValue((param[2] ?? "").trim());
|
|
517
|
+
}
|
|
518
|
+
candidates.push({
|
|
519
|
+
name,
|
|
520
|
+
arguments: args,
|
|
521
|
+
grammar: "invoke",
|
|
522
|
+
range: { start, end: match.index + match[0].length },
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
return candidates;
|
|
526
|
+
}
|
|
527
|
+
function parseToolCallXml(text, enabled) {
|
|
528
|
+
const candidates = [];
|
|
529
|
+
const wrapperRe = /<(tool_call|tools)>[\s\S]*?<\/\1>/gi;
|
|
530
|
+
for (const match of text.matchAll(wrapperRe)) {
|
|
531
|
+
if (match.index === undefined || isInsideCodeFence(text, match.index))
|
|
532
|
+
continue;
|
|
533
|
+
const tag = match[1]?.toLowerCase();
|
|
534
|
+
const openTagEnd = match[0].indexOf(">") + 1;
|
|
535
|
+
const body = match[0].slice(openTagEnd, match[0].length - `</${tag}>`.length);
|
|
536
|
+
if (enabled.has("granite") || enabled.has("qwen")) {
|
|
537
|
+
const jsonGrammar = tag === "tools" || !enabled.has("granite") ? "qwen" : "granite";
|
|
538
|
+
const jsonCalls = parseToolCallJsonBody(body, jsonGrammar);
|
|
539
|
+
for (const call of jsonCalls) {
|
|
540
|
+
candidates.push({
|
|
541
|
+
...call,
|
|
542
|
+
range: { start: match.index, end: match.index + match[0].length },
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (enabled.has("glm")) {
|
|
547
|
+
const glmCall = parseGlmToolCallBody(body);
|
|
548
|
+
if (glmCall)
|
|
549
|
+
candidates.push({
|
|
550
|
+
...glmCall,
|
|
551
|
+
range: { start: match.index, end: match.index + match[0].length },
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
if (enabled.has("qwen")) {
|
|
555
|
+
const qwenCalls = parseQwenFunctionBody(body);
|
|
556
|
+
for (const call of qwenCalls) {
|
|
557
|
+
candidates.push({
|
|
558
|
+
...call,
|
|
559
|
+
range: { start: match.index, end: match.index + match[0].length },
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (enabled.has("qwen")) {
|
|
565
|
+
const bareFunctionRe = /<function=([A-Za-z_][\w.-]*)>[\s\S]*?<\/function>/gi;
|
|
566
|
+
for (const match of text.matchAll(bareFunctionRe)) {
|
|
567
|
+
if (match.index === undefined || isInsideCodeFence(text, match.index))
|
|
568
|
+
continue;
|
|
569
|
+
const calls = parseQwenFunctionBody(match[0]);
|
|
570
|
+
for (const call of calls) {
|
|
571
|
+
candidates.push({
|
|
572
|
+
...call,
|
|
573
|
+
range: { start: match.index, end: match.index + match[0].length },
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return candidates;
|
|
579
|
+
}
|
|
580
|
+
function parseToolCallJsonBody(body, grammar) {
|
|
581
|
+
const calls = [];
|
|
582
|
+
const trimmed = unwrapMarkdownFence(body.trim());
|
|
583
|
+
const json = extractFirstBalancedJson(trimmed)?.json ?? trimmed;
|
|
584
|
+
const parsed = parseJsonValue(json);
|
|
585
|
+
if (Array.isArray(parsed)) {
|
|
586
|
+
for (const item of parsed) {
|
|
587
|
+
if (isObject(item)) {
|
|
588
|
+
const call = callFromJsonObject(item);
|
|
589
|
+
if (call)
|
|
590
|
+
calls.push({ ...call, grammar });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return calls;
|
|
594
|
+
}
|
|
595
|
+
if (isObject(parsed)) {
|
|
596
|
+
const call = callFromJsonObject(parsed);
|
|
597
|
+
if (call)
|
|
598
|
+
calls.push({ ...call, grammar });
|
|
599
|
+
}
|
|
600
|
+
return calls;
|
|
601
|
+
}
|
|
602
|
+
function parseQwenFunctionBody(body) {
|
|
603
|
+
const calls = [];
|
|
604
|
+
const functionRe = /<function=([A-Za-z_][\w.-]*)>\s*([\s\S]*?)<\/function>/gi;
|
|
605
|
+
for (const match of body.matchAll(functionRe)) {
|
|
606
|
+
const name = match[1]?.trim();
|
|
607
|
+
if (!name)
|
|
608
|
+
continue;
|
|
609
|
+
const args = {};
|
|
610
|
+
const paramRe = /<parameter=([^>]+)>([\s\S]*?)<\/parameter>/gi;
|
|
611
|
+
for (const param of (match[2] ?? "").matchAll(paramRe)) {
|
|
612
|
+
const key = param[1]?.trim();
|
|
613
|
+
if (!key)
|
|
614
|
+
continue;
|
|
615
|
+
args[key] = maybeParseJsonValue((param[2] ?? "").trim());
|
|
616
|
+
}
|
|
617
|
+
calls.push({ name, arguments: args, grammar: "qwen" });
|
|
618
|
+
}
|
|
619
|
+
return calls;
|
|
620
|
+
}
|
|
621
|
+
function parseGlmToolCallBody(body) {
|
|
622
|
+
const keyRe = /<arg_key>([\s\S]*?)<\/arg_key>/gi;
|
|
623
|
+
const valueRe = /<arg_value>([\s\S]*?)<\/arg_value>/gi;
|
|
624
|
+
const keys = [...body.matchAll(keyRe)]
|
|
625
|
+
.map((m) => (m[1] ?? "").trim())
|
|
626
|
+
.filter(Boolean);
|
|
627
|
+
const values = [...body.matchAll(valueRe)].map((m) => (m[1] ?? "").trim());
|
|
628
|
+
const nameEnd = keys.length === 0 ? body.length : body.search(/<arg_key>/i);
|
|
629
|
+
const name = body.slice(0, nameEnd).trim().split(/\s+/)[0];
|
|
630
|
+
if (!name || !/^[A-Za-z_][\w.-]*$/.test(name))
|
|
631
|
+
return undefined;
|
|
632
|
+
const args = {};
|
|
633
|
+
keys.forEach((key, i) => {
|
|
634
|
+
args[key] = maybeParseJsonValue(values[i] ?? "");
|
|
635
|
+
});
|
|
636
|
+
return { name, arguments: args, grammar: "glm" };
|
|
637
|
+
}
|
|
638
|
+
function parseLlamaPythonTag(text) {
|
|
639
|
+
const candidates = [];
|
|
640
|
+
const marker = "<|python_tag|>";
|
|
641
|
+
let index = 0;
|
|
642
|
+
for (index = text.indexOf(marker, index); index !== -1; index = text.indexOf(marker, index)) {
|
|
643
|
+
if (isInsideCodeFence(text, index)) {
|
|
644
|
+
index += marker.length;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const bodyStart = index + marker.length;
|
|
648
|
+
const rest = text.slice(bodyStart).trimStart();
|
|
649
|
+
const whitespace = text.slice(bodyStart).length - rest.length;
|
|
650
|
+
const payloadStart = bodyStart + whitespace;
|
|
651
|
+
const extracted = extractFirstBalancedJson(text.slice(payloadStart));
|
|
652
|
+
if (extracted) {
|
|
653
|
+
const parsed = parseJsonValue(extracted.json);
|
|
654
|
+
for (const call of callsFromJsonValue(parsed)) {
|
|
655
|
+
candidates.push({
|
|
656
|
+
...call,
|
|
657
|
+
grammar: "llama",
|
|
658
|
+
range: { start: index, end: payloadStart + extracted.end },
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
index = payloadStart + extracted.end;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const lineEnd = findLineEnd(text, payloadStart);
|
|
665
|
+
for (const call of parsePythonicCalls(text.slice(payloadStart, lineEnd))) {
|
|
666
|
+
candidates.push({
|
|
667
|
+
...call,
|
|
668
|
+
grammar: "llama",
|
|
669
|
+
range: { start: index, end: lineEnd },
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
index = lineEnd;
|
|
673
|
+
}
|
|
674
|
+
return candidates;
|
|
675
|
+
}
|
|
676
|
+
function parseBareJsonToolCalls(text, grammar) {
|
|
677
|
+
const candidates = [];
|
|
678
|
+
const objectRe = /\{\s*"(?:name|function_name|function)"/g;
|
|
679
|
+
for (const match of text.matchAll(objectRe)) {
|
|
680
|
+
if (match.index === undefined || isInsideCodeFence(text, match.index))
|
|
681
|
+
continue;
|
|
682
|
+
const extracted = extractFirstBalancedJson(text.slice(match.index));
|
|
683
|
+
if (!extracted)
|
|
684
|
+
continue;
|
|
685
|
+
for (const call of callsFromJsonValue(parseJsonValue(extracted.json))) {
|
|
686
|
+
candidates.push({
|
|
687
|
+
...call,
|
|
688
|
+
grammar,
|
|
689
|
+
range: { start: match.index, end: match.index + extracted.end },
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const arrayRe = /\[\s*\{\s*"(?:name|function_name|function)"/g;
|
|
694
|
+
for (const match of text.matchAll(arrayRe)) {
|
|
695
|
+
if (match.index === undefined || isInsideCodeFence(text, match.index))
|
|
696
|
+
continue;
|
|
697
|
+
const extracted = extractFirstBalancedJson(text.slice(match.index));
|
|
698
|
+
if (!extracted)
|
|
699
|
+
continue;
|
|
700
|
+
for (const call of callsFromJsonValue(parseJsonValue(extracted.json))) {
|
|
701
|
+
candidates.push({
|
|
702
|
+
...call,
|
|
703
|
+
grammar,
|
|
704
|
+
range: { start: match.index, end: match.index + extracted.end },
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return candidates;
|
|
709
|
+
}
|
|
710
|
+
function parseBarePythonicToolCalls(text, grammar) {
|
|
711
|
+
const candidates = [];
|
|
712
|
+
const lineRe = /(?:^|\n)\s*([A-Za-z_][\w.-]*)\s*\(/g;
|
|
713
|
+
for (const match of text.matchAll(lineRe)) {
|
|
714
|
+
if (match.index === undefined || isInsideCodeFence(text, match.index))
|
|
715
|
+
continue;
|
|
716
|
+
const start = match.index + (match[0].startsWith("\n") ? 1 : 0);
|
|
717
|
+
const openParen = match.index + match[0].lastIndexOf("(");
|
|
718
|
+
const closeParen = findMatching(text, openParen, "(", ")");
|
|
719
|
+
if (closeParen === undefined)
|
|
720
|
+
continue;
|
|
721
|
+
const lineStart = text.lastIndexOf("\n", openParen) + 1;
|
|
722
|
+
if (text.slice(lineStart, match.index).trim() !== "")
|
|
723
|
+
continue;
|
|
724
|
+
const [call] = parsePythonicCalls(text.slice(start, closeParen + 1));
|
|
725
|
+
if (call)
|
|
726
|
+
candidates.push({
|
|
727
|
+
...call,
|
|
728
|
+
grammar,
|
|
729
|
+
range: { start, end: closeParen + 1 },
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
return candidates;
|
|
733
|
+
}
|
|
734
|
+
function parseOlmo(text) {
|
|
735
|
+
const candidates = [];
|
|
736
|
+
const re = /<function_calls>([\s\S]*?)<\/function_calls>/gi;
|
|
737
|
+
for (const match of text.matchAll(re)) {
|
|
738
|
+
if (match.index === undefined || isInsideCodeFence(text, match.index))
|
|
739
|
+
continue;
|
|
740
|
+
for (const call of parsePythonicCalls(match[1] ?? "")) {
|
|
741
|
+
candidates.push({
|
|
742
|
+
...call,
|
|
743
|
+
grammar: "olmo",
|
|
744
|
+
range: { start: match.index, end: match.index + match[0].length },
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return candidates;
|
|
749
|
+
}
|
|
750
|
+
function parsePythonicCalls(text) {
|
|
751
|
+
const calls = [];
|
|
752
|
+
const re = /(?:^|\n)\s*([A-Za-z_][\w.-]*)\s*\(/g;
|
|
753
|
+
for (const match of text.matchAll(re)) {
|
|
754
|
+
if (match.index === undefined)
|
|
755
|
+
continue;
|
|
756
|
+
const name = match[1];
|
|
757
|
+
const openParen = match.index + match[0].lastIndexOf("(");
|
|
758
|
+
const closeParen = findMatching(text, openParen, "(", ")");
|
|
759
|
+
if (closeParen === undefined)
|
|
760
|
+
continue;
|
|
761
|
+
const argsText = text.slice(openParen + 1, closeParen);
|
|
762
|
+
calls.push({ name, arguments: parseKeywordArguments(argsText) });
|
|
763
|
+
}
|
|
764
|
+
return calls;
|
|
765
|
+
}
|
|
766
|
+
function parseKeywordArguments(text) {
|
|
767
|
+
const args = {};
|
|
768
|
+
for (const part of splitTopLevel(text, ",")) {
|
|
769
|
+
const eq = findTopLevelChar(part, "=");
|
|
770
|
+
if (eq === -1)
|
|
771
|
+
continue;
|
|
772
|
+
const key = part.slice(0, eq).trim();
|
|
773
|
+
if (!/^[A-Za-z_][\w.-]*$/.test(key))
|
|
774
|
+
continue;
|
|
775
|
+
args[key] = parsePythonishValue(part.slice(eq + 1).trim());
|
|
776
|
+
}
|
|
777
|
+
return args;
|
|
778
|
+
}
|
|
779
|
+
function parsePythonishValue(raw) {
|
|
780
|
+
const trimmed = raw.trim();
|
|
781
|
+
if (trimmed === "True")
|
|
782
|
+
return true;
|
|
783
|
+
if (trimmed === "False")
|
|
784
|
+
return false;
|
|
785
|
+
if (trimmed === "None")
|
|
786
|
+
return null;
|
|
787
|
+
if (/^[-+]?\d+(?:\.\d+)?$/.test(trimmed))
|
|
788
|
+
return Number(trimmed);
|
|
789
|
+
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
|
790
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
|
791
|
+
return trimmed.slice(1, -1).replace(/\\(['"\\])/g, "$1");
|
|
792
|
+
}
|
|
793
|
+
return parseJsonValueOrString(trimmed
|
|
794
|
+
.replace(/\bTrue\b/g, "true")
|
|
795
|
+
.replace(/\bFalse\b/g, "false")
|
|
796
|
+
.replace(/\bNone\b/g, "null"));
|
|
797
|
+
}
|
|
798
|
+
function callsFromJsonValue(value) {
|
|
799
|
+
if (Array.isArray(value)) {
|
|
800
|
+
return value.flatMap((item) => isObject(item) ? callsFromJsonValue(item) : []);
|
|
801
|
+
}
|
|
802
|
+
if (!isObject(value))
|
|
803
|
+
return [];
|
|
804
|
+
const call = callFromJsonObject(value);
|
|
805
|
+
return call ? [call] : [];
|
|
806
|
+
}
|
|
807
|
+
function callFromJsonObject(value) {
|
|
808
|
+
const name = value.name ??
|
|
809
|
+
value.function_name ??
|
|
810
|
+
(isObject(value.function) ? value.function.name : undefined);
|
|
811
|
+
if (typeof name !== "string" || !name.trim())
|
|
812
|
+
return undefined;
|
|
813
|
+
let args = value.arguments ?? value.args ?? value.parameters;
|
|
814
|
+
if (args === undefined && isObject(value.function))
|
|
815
|
+
args = value.function.arguments;
|
|
816
|
+
const normalized = normalizeArgumentsObject(args) ?? {};
|
|
817
|
+
return { name: name.trim(), arguments: normalized };
|
|
818
|
+
}
|
|
819
|
+
function normalizeArgumentsObject(value) {
|
|
820
|
+
if (typeof value === "string") {
|
|
821
|
+
return normalizeArgumentsObject(parseJsonValue(value));
|
|
822
|
+
}
|
|
823
|
+
if (isObject(value)) {
|
|
824
|
+
const nested = value.arguments;
|
|
825
|
+
if (typeof nested === "string" || isObject(nested)) {
|
|
826
|
+
const unwrapped = normalizeArgumentsObject(nested);
|
|
827
|
+
if (unwrapped)
|
|
828
|
+
return unwrapped;
|
|
829
|
+
}
|
|
830
|
+
return value;
|
|
831
|
+
}
|
|
832
|
+
return undefined;
|
|
833
|
+
}
|
|
834
|
+
function parseJsonArrayObjects(json) {
|
|
835
|
+
const parsed = parseJsonValue(json);
|
|
836
|
+
return Array.isArray(parsed) ? parsed.filter(isObject) : [];
|
|
837
|
+
}
|
|
838
|
+
function parseJsonObject(json) {
|
|
839
|
+
const parsed = parseJsonValue(json);
|
|
840
|
+
return isObject(parsed) ? parsed : undefined;
|
|
841
|
+
}
|
|
842
|
+
function parseJsonValue(json) {
|
|
843
|
+
try {
|
|
844
|
+
return JSON.parse(json);
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
return undefined;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function parseJsonValueOrString(value) {
|
|
851
|
+
const parsed = parseJsonValue(value);
|
|
852
|
+
return parsed === undefined ? value : parsed;
|
|
853
|
+
}
|
|
854
|
+
function maybeParseJsonValue(value) {
|
|
855
|
+
if (value === "")
|
|
856
|
+
return "";
|
|
857
|
+
if (/^(?:true|false|null|-?\d|[[{]|")/.test(value)) {
|
|
858
|
+
return parseJsonValueOrString(value);
|
|
859
|
+
}
|
|
860
|
+
return value;
|
|
861
|
+
}
|
|
862
|
+
function selectCandidates(candidates) {
|
|
863
|
+
const selected = [];
|
|
864
|
+
const sorted = [...candidates].sort((a, b) => {
|
|
865
|
+
if (a.range.start !== b.range.start)
|
|
866
|
+
return a.range.start - b.range.start;
|
|
867
|
+
return b.range.end - b.range.start - (a.range.end - a.range.start);
|
|
868
|
+
});
|
|
869
|
+
for (const candidate of sorted) {
|
|
870
|
+
const duplicate = selected.some((existing) => {
|
|
871
|
+
const sameRange = existing.range.start === candidate.range.start &&
|
|
872
|
+
existing.range.end === candidate.range.end;
|
|
873
|
+
return !sameRange && rangesOverlap(existing.range, candidate.range);
|
|
874
|
+
});
|
|
875
|
+
if (!duplicate)
|
|
876
|
+
selected.push(candidate);
|
|
877
|
+
}
|
|
878
|
+
return selected;
|
|
879
|
+
}
|
|
880
|
+
function rangesOverlap(a, b) {
|
|
881
|
+
return a.start < b.end && b.start < a.end;
|
|
882
|
+
}
|
|
883
|
+
function isAllowedTool(candidateName, requireKnownTool, knownTools) {
|
|
884
|
+
if (!requireKnownTool)
|
|
885
|
+
return true;
|
|
886
|
+
return knownTools.size > 0 && knownTools.has(candidateName);
|
|
887
|
+
}
|
|
888
|
+
function removeRanges(text, ranges) {
|
|
889
|
+
const sorted = [...ranges].sort((a, b) => a.start - b.start);
|
|
890
|
+
let result = "";
|
|
891
|
+
let cursor = 0;
|
|
892
|
+
for (const range of sorted) {
|
|
893
|
+
result += text.slice(cursor, range.start);
|
|
894
|
+
cursor = Math.max(cursor, range.end);
|
|
895
|
+
}
|
|
896
|
+
result += text.slice(cursor);
|
|
897
|
+
return result
|
|
898
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
899
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
900
|
+
.trim();
|
|
901
|
+
}
|
|
902
|
+
function getPartText(part) {
|
|
903
|
+
if (isObject(part) && part.type === "text" && typeof part.text === "string")
|
|
904
|
+
return part.text;
|
|
905
|
+
if (isObject(part) &&
|
|
906
|
+
part.type === "thinking" &&
|
|
907
|
+
typeof part.thinking === "string")
|
|
908
|
+
return part.thinking;
|
|
909
|
+
return undefined;
|
|
910
|
+
}
|
|
911
|
+
function setPartText(part, text) {
|
|
912
|
+
if (isObject(part) && part.type === "text")
|
|
913
|
+
return { ...part, text };
|
|
914
|
+
if (isObject(part) && part.type === "thinking")
|
|
915
|
+
return { ...part, thinking: text };
|
|
916
|
+
return part;
|
|
917
|
+
}
|
|
918
|
+
function isToolCallContent(part) {
|
|
919
|
+
return (isObject(part) && part.type === "toolCall" && typeof part.name === "string");
|
|
920
|
+
}
|
|
921
|
+
function makeRecoveredToolCallId(grammar, index) {
|
|
922
|
+
return `tool_repair_${grammar.replace(/[^a-z0-9]/gi, "_")}_${Date.now().toString(36)}_${index}`;
|
|
923
|
+
}
|
|
924
|
+
function findPattern(text, pattern, from) {
|
|
925
|
+
pattern.lastIndex = 0;
|
|
926
|
+
const chunk = text.slice(from);
|
|
927
|
+
const match = pattern.exec(chunk);
|
|
928
|
+
return match
|
|
929
|
+
? { start: from + match.index, end: from + match.index + match[0].length }
|
|
930
|
+
: undefined;
|
|
931
|
+
}
|
|
932
|
+
function extractFirstBalancedJson(text) {
|
|
933
|
+
const start = text.search(/[[{]/);
|
|
934
|
+
if (start === -1)
|
|
935
|
+
return undefined;
|
|
936
|
+
const opener = text[start];
|
|
937
|
+
const closer = opener === "{" ? "}" : "]";
|
|
938
|
+
const end = findMatching(text, start, opener, closer);
|
|
939
|
+
if (end === undefined)
|
|
940
|
+
return undefined;
|
|
941
|
+
return { json: text.slice(start, end + 1), start, end: end + 1 };
|
|
942
|
+
}
|
|
943
|
+
function findMatching(text, openIndex, opener, closer) {
|
|
944
|
+
let depth = 0;
|
|
945
|
+
let quote;
|
|
946
|
+
let escaped = false;
|
|
947
|
+
for (let i = openIndex; i < text.length; i++) {
|
|
948
|
+
const ch = text[i];
|
|
949
|
+
if (quote) {
|
|
950
|
+
if (escaped) {
|
|
951
|
+
escaped = false;
|
|
952
|
+
}
|
|
953
|
+
else if (ch === "\\") {
|
|
954
|
+
escaped = true;
|
|
955
|
+
}
|
|
956
|
+
else if (ch === quote) {
|
|
957
|
+
quote = undefined;
|
|
958
|
+
}
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (ch === '"' || ch === "'") {
|
|
962
|
+
quote = ch;
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (ch === opener)
|
|
966
|
+
depth++;
|
|
967
|
+
if (ch === closer) {
|
|
968
|
+
depth--;
|
|
969
|
+
if (depth === 0)
|
|
970
|
+
return i;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return undefined;
|
|
974
|
+
}
|
|
975
|
+
function splitTopLevel(text, delimiter) {
|
|
976
|
+
const parts = [];
|
|
977
|
+
let start = 0;
|
|
978
|
+
let depth = 0;
|
|
979
|
+
let quote;
|
|
980
|
+
let escaped = false;
|
|
981
|
+
for (let i = 0; i < text.length; i++) {
|
|
982
|
+
const ch = text[i];
|
|
983
|
+
if (quote) {
|
|
984
|
+
if (escaped)
|
|
985
|
+
escaped = false;
|
|
986
|
+
else if (ch === "\\")
|
|
987
|
+
escaped = true;
|
|
988
|
+
else if (ch === quote)
|
|
989
|
+
quote = undefined;
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (ch === '"' || ch === "'") {
|
|
993
|
+
quote = ch;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
if ("([{".includes(ch))
|
|
997
|
+
depth++;
|
|
998
|
+
if (")]}".includes(ch))
|
|
999
|
+
depth--;
|
|
1000
|
+
if (ch === delimiter && depth === 0) {
|
|
1001
|
+
parts.push(text.slice(start, i));
|
|
1002
|
+
start = i + 1;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
parts.push(text.slice(start));
|
|
1006
|
+
return parts.map((part) => part.trim()).filter(Boolean);
|
|
1007
|
+
}
|
|
1008
|
+
function findTopLevelChar(text, target) {
|
|
1009
|
+
let depth = 0;
|
|
1010
|
+
let quote;
|
|
1011
|
+
let escaped = false;
|
|
1012
|
+
for (let i = 0; i < text.length; i++) {
|
|
1013
|
+
const ch = text[i];
|
|
1014
|
+
if (quote) {
|
|
1015
|
+
if (escaped)
|
|
1016
|
+
escaped = false;
|
|
1017
|
+
else if (ch === "\\")
|
|
1018
|
+
escaped = true;
|
|
1019
|
+
else if (ch === quote)
|
|
1020
|
+
quote = undefined;
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
if (ch === '"' || ch === "'") {
|
|
1024
|
+
quote = ch;
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
if ("([{".includes(ch))
|
|
1028
|
+
depth++;
|
|
1029
|
+
if (")]}".includes(ch))
|
|
1030
|
+
depth--;
|
|
1031
|
+
if (ch === target && depth === 0)
|
|
1032
|
+
return i;
|
|
1033
|
+
}
|
|
1034
|
+
return -1;
|
|
1035
|
+
}
|
|
1036
|
+
function findLineEnd(text, start) {
|
|
1037
|
+
const newline = text.indexOf("\n", start);
|
|
1038
|
+
return newline === -1 ? text.length : newline;
|
|
1039
|
+
}
|
|
1040
|
+
function isInsideCodeFence(text, index) {
|
|
1041
|
+
const before = text.slice(0, index);
|
|
1042
|
+
const fences = before.match(/```/g);
|
|
1043
|
+
return Boolean(fences && fences.length % 2 === 1);
|
|
1044
|
+
}
|
|
1045
|
+
function unwrapMarkdownFence(text) {
|
|
1046
|
+
const match = /^```\w*\s*([\s\S]*?)\s*```$/.exec(text);
|
|
1047
|
+
return match ? (match[1] ?? "") : text;
|
|
1048
|
+
}
|
|
1049
|
+
function isObject(value) {
|
|
1050
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1051
|
+
}
|
|
1052
|
+
//# sourceMappingURL=grammar-recovery.js.map
|