zidane 5.2.1 → 5.3.1
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 +7 -5
- package/dist/{agent-CGQajqtC.d.ts → agent-bKs7MRT2.d.ts} +429 -4
- package/dist/agent-bKs7MRT2.d.ts.map +1 -0
- package/dist/chat.d.ts +212 -58
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +2 -2
- package/dist/{errors-COmsomd5.js → errors-Byb0F8B9.js} +44 -2
- package/dist/errors-Byb0F8B9.js.map +1 -0
- package/dist/{index-DwbcFBr_.d.ts → index-BlMvPh9X.d.ts} +29 -3
- package/dist/index-BlMvPh9X.d.ts.map +1 -0
- package/dist/{index-BDP6mA3Y.d.ts → index-CTmNaIDb.d.ts} +2 -2
- package/dist/{index-BDP6mA3Y.d.ts.map → index-CTmNaIDb.d.ts.map} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +10 -10
- package/dist/{interpolate-BhmHKD6x.js → interpolate-ERgZUxgg.js} +2 -2
- package/dist/{interpolate-BhmHKD6x.js.map → interpolate-ERgZUxgg.js.map} +1 -1
- package/dist/{login-D7Tp-K5f.js → login-CNS9_8Ue.js} +3 -3
- package/dist/{login-D7Tp-K5f.js.map → login-CNS9_8Ue.js.map} +1 -1
- package/dist/{mcp-B1psg7jf.js → mcp-ZsSFo4Dp.js} +2 -2
- package/dist/{mcp-B1psg7jf.js.map → mcp-ZsSFo4Dp.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{messages-DsbMYNmt.js → messages-D0xT979U.js} +631 -68
- package/dist/messages-D0xT979U.js.map +1 -0
- package/dist/{presets-AgF0RFx1.js → presets-h5i3kpOP.js} +2 -2
- package/dist/{presets-AgF0RFx1.js.map → presets-h5i3kpOP.js.map} +1 -1
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +1 -1
- package/dist/{providers-v1Rn2rqG.js → providers-x3LZByR5.js} +38 -6
- package/dist/providers-x3LZByR5.js.map +1 -0
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -3
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.js +1 -1
- package/dist/{session-DOJgRXvF.js → session-BHZwxmfr.js} +2 -2
- package/dist/{session-DOJgRXvF.js.map → session-BHZwxmfr.js.map} +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +1 -1
- package/dist/{tools-BRbbfdJh.js → tools-CWEDS2ZT.js} +251 -47
- package/dist/tools-CWEDS2ZT.js.map +1 -0
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +1 -1
- package/dist/{transcript-anchors-BBuIoU0x.d.ts → transcript-anchors-DOUqyvXR.d.ts} +28 -4
- package/dist/transcript-anchors-DOUqyvXR.d.ts.map +1 -0
- package/dist/tui.d.ts +29 -3
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +363 -28
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-gJ0qtLPv.js → turn-operations-D9HvatsR.js} +396 -89
- package/dist/turn-operations-D9HvatsR.js.map +1 -0
- package/dist/types-IcokUOyC.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CHAT.md +55 -16
- package/docs/TUI.md +22 -2
- package/package.json +1 -1
- package/dist/agent-CGQajqtC.d.ts.map +0 -1
- package/dist/errors-COmsomd5.js.map +0 -1
- package/dist/index-DwbcFBr_.d.ts.map +0 -1
- package/dist/messages-DsbMYNmt.js.map +0 -1
- package/dist/providers-v1Rn2rqG.js.map +0 -1
- package/dist/tools-BRbbfdJh.js.map +0 -1
- package/dist/transcript-anchors-BBuIoU0x.d.ts.map +0 -1
- package/dist/turn-operations-gJ0qtLPv.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as matchesContextExceeded, s as errorMessage } from "./errors-Byb0F8B9.js";
|
|
2
2
|
import { getModel } from "@mariozechner/pi-ai";
|
|
3
3
|
//#region src/providers/cost.ts
|
|
4
4
|
/**
|
|
@@ -36,6 +36,323 @@ function fillEstimatedCost(usage, provider) {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
//#endregion
|
|
39
|
+
//#region src/providers/schema-sanitize.ts
|
|
40
|
+
/** Max nesting depth before sub-schemas are replaced with `{}`. */
|
|
41
|
+
const MAX_DEPTH = 32;
|
|
42
|
+
/** Max `$ref` hop count before resolution gives up. */
|
|
43
|
+
const MAX_REF_HOPS = 16;
|
|
44
|
+
/** Keys that hold a single sub-schema. */
|
|
45
|
+
const SUBSCHEMA_KEYS = [
|
|
46
|
+
"items",
|
|
47
|
+
"additionalProperties",
|
|
48
|
+
"contains",
|
|
49
|
+
"not",
|
|
50
|
+
"if",
|
|
51
|
+
"then",
|
|
52
|
+
"else",
|
|
53
|
+
"propertyNames"
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Keys that hold a record of sub-schemas.
|
|
57
|
+
*
|
|
58
|
+
* `$defs` / `definitions` are intentionally NOT included — they're
|
|
59
|
+
* metaschema storage, only reached via `$ref` resolution (which has its
|
|
60
|
+
* own walker in `resolveRef`), and `enforceRoot` strips them at the
|
|
61
|
+
* wire layer once inlining is done. Recursing into them would cost
|
|
62
|
+
* cycles and produce warnings about sub-trees the provider never sees.
|
|
63
|
+
*/
|
|
64
|
+
const SUBSCHEMA_RECORD_KEYS = [
|
|
65
|
+
"properties",
|
|
66
|
+
"patternProperties",
|
|
67
|
+
"dependentSchemas"
|
|
68
|
+
];
|
|
69
|
+
/** Keys that hold an array of sub-schemas. */
|
|
70
|
+
const SUBSCHEMA_ARRAY_KEYS = [
|
|
71
|
+
"oneOf",
|
|
72
|
+
"anyOf",
|
|
73
|
+
"allOf",
|
|
74
|
+
"prefixItems"
|
|
75
|
+
];
|
|
76
|
+
function isPlainObject(value) {
|
|
77
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a JSON pointer like `#/$defs/Foo` against `root`. Returns
|
|
81
|
+
* `undefined` when the pointer doesn't resolve or hops past `MAX_REF_HOPS`.
|
|
82
|
+
* The caller treats `undefined` as "give up, leave as a permissive schema".
|
|
83
|
+
*/
|
|
84
|
+
function resolveRef(root, ref, hops = 0) {
|
|
85
|
+
if (hops > MAX_REF_HOPS) return void 0;
|
|
86
|
+
if (!ref.startsWith("#/")) return void 0;
|
|
87
|
+
const parts = ref.slice(2).split("/").map(decodeRefSegment);
|
|
88
|
+
let cursor = root;
|
|
89
|
+
for (const part of parts) {
|
|
90
|
+
if (!isPlainObject(cursor)) return void 0;
|
|
91
|
+
cursor = cursor[part];
|
|
92
|
+
}
|
|
93
|
+
if (!isPlainObject(cursor)) return void 0;
|
|
94
|
+
if (typeof cursor.$ref === "string") return resolveRef(root, cursor.$ref, hops + 1);
|
|
95
|
+
return cursor;
|
|
96
|
+
}
|
|
97
|
+
function decodeRefSegment(seg) {
|
|
98
|
+
return seg.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Recursively sanitize a sub-schema. Returns the **original** reference
|
|
102
|
+
* when nothing needed to change — every turn's `formatTools` re-runs the
|
|
103
|
+
* sanitizer, and the no-alloc fast path keeps the hot loop cheap when
|
|
104
|
+
* the registered tool set is already clean. Returns a fresh object when
|
|
105
|
+
* any rewrite happened; never mutates the input.
|
|
106
|
+
*
|
|
107
|
+
* The dirty-tracking is local to each frame so a clean sub-tree under a
|
|
108
|
+
* dirty parent doesn't get cloned uselessly.
|
|
109
|
+
*/
|
|
110
|
+
function sanitizeNode(node, ctx, depth, path) {
|
|
111
|
+
if (depth > MAX_DEPTH) {
|
|
112
|
+
ctx.warnings.push(`${ctx.prefix}schema nested deeper than ${MAX_DEPTH} levels at ${path || "$"} — replaced with permissive {}`);
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
if (!isPlainObject(node)) return {};
|
|
116
|
+
let dirty = false;
|
|
117
|
+
let working = node;
|
|
118
|
+
if (typeof working.$ref === "string") {
|
|
119
|
+
const ref = working.$ref;
|
|
120
|
+
const resolved = resolveRef(ctx.root, ref);
|
|
121
|
+
const { $ref: _drop, ...rest } = working;
|
|
122
|
+
if (resolved) {
|
|
123
|
+
ctx.warnings.push(`${ctx.prefix}inlined $ref "${ref}" at ${path || "$"}`);
|
|
124
|
+
working = {
|
|
125
|
+
...resolved,
|
|
126
|
+
...rest
|
|
127
|
+
};
|
|
128
|
+
} else {
|
|
129
|
+
ctx.warnings.push(`${ctx.prefix}dropped unresolvable $ref "${ref}" at ${path || "$"}`);
|
|
130
|
+
working = rest;
|
|
131
|
+
}
|
|
132
|
+
dirty = true;
|
|
133
|
+
}
|
|
134
|
+
if (working.nullable === true) {
|
|
135
|
+
if (!dirty) {
|
|
136
|
+
working = { ...working };
|
|
137
|
+
dirty = true;
|
|
138
|
+
}
|
|
139
|
+
const t = working.type;
|
|
140
|
+
if (typeof t === "string") {
|
|
141
|
+
working.type = [t, "null"];
|
|
142
|
+
ctx.warnings.push(`${ctx.prefix}converted nullable:true → type:[${t},null] at ${path || "$"}`);
|
|
143
|
+
} else if (Array.isArray(t) && !t.includes("null")) {
|
|
144
|
+
working.type = [...t, "null"];
|
|
145
|
+
ctx.warnings.push(`${ctx.prefix}converted nullable:true → type:[…,null] at ${path || "$"}`);
|
|
146
|
+
} else if (Array.isArray(t)) {} else ctx.warnings.push(`${ctx.prefix}stripped nullable:true at ${path || "$"} (no base type)`);
|
|
147
|
+
delete working.nullable;
|
|
148
|
+
}
|
|
149
|
+
for (const key of SUBSCHEMA_KEYS) {
|
|
150
|
+
const child = working[key];
|
|
151
|
+
if (isPlainObject(child)) {
|
|
152
|
+
const sanitized = sanitizeNode(child, ctx, depth + 1, `${path}/${key}`);
|
|
153
|
+
if (sanitized !== child) {
|
|
154
|
+
if (!dirty) {
|
|
155
|
+
working = { ...working };
|
|
156
|
+
dirty = true;
|
|
157
|
+
}
|
|
158
|
+
working[key] = sanitized;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const key of SUBSCHEMA_RECORD_KEYS) {
|
|
163
|
+
const rec = working[key];
|
|
164
|
+
if (isPlainObject(rec)) {
|
|
165
|
+
let recDirty = false;
|
|
166
|
+
let out = rec;
|
|
167
|
+
for (const [k, v] of Object.entries(rec)) {
|
|
168
|
+
const sanitized = sanitizeNode(v, ctx, depth + 1, `${path}/${key}/${k}`);
|
|
169
|
+
if (sanitized !== v) {
|
|
170
|
+
if (!recDirty) {
|
|
171
|
+
out = { ...rec };
|
|
172
|
+
recDirty = true;
|
|
173
|
+
}
|
|
174
|
+
out[k] = sanitized;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (recDirty) {
|
|
178
|
+
if (!dirty) {
|
|
179
|
+
working = { ...working };
|
|
180
|
+
dirty = true;
|
|
181
|
+
}
|
|
182
|
+
working[key] = out;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const key of SUBSCHEMA_ARRAY_KEYS) {
|
|
187
|
+
const arr = working[key];
|
|
188
|
+
if (Array.isArray(arr)) {
|
|
189
|
+
let arrDirty = false;
|
|
190
|
+
let out = arr;
|
|
191
|
+
for (let i = 0; i < arr.length; i++) {
|
|
192
|
+
const sanitized = sanitizeNode(arr[i], ctx, depth + 1, `${path}/${key}/${i}`);
|
|
193
|
+
if (sanitized !== arr[i]) {
|
|
194
|
+
if (!arrDirty) {
|
|
195
|
+
out = [...arr];
|
|
196
|
+
arrDirty = true;
|
|
197
|
+
}
|
|
198
|
+
out[i] = sanitized;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (arrDirty) {
|
|
202
|
+
if (!dirty) {
|
|
203
|
+
working = { ...working };
|
|
204
|
+
dirty = true;
|
|
205
|
+
}
|
|
206
|
+
working[key] = out;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return working;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Apply root-level coercions every supported provider expects:
|
|
214
|
+
*
|
|
215
|
+
* - `type === 'object'` (Anthropic requires it; OpenAI tolerates more
|
|
216
|
+
* but rejects unions at root in strict mode).
|
|
217
|
+
* - `properties` is a plain object (Anthropic 400s when missing).
|
|
218
|
+
* - No root-level `$ref` / `oneOf` / `anyOf` / `allOf` (Anthropic rejects;
|
|
219
|
+
* OpenAI behaviour varies by endpoint).
|
|
220
|
+
* - `$defs` / `definitions` stripped (recursive pass already inlined
|
|
221
|
+
* refs; the metaschema storage is dead weight at the wire layer).
|
|
222
|
+
*
|
|
223
|
+
* Applied for every profile because portability matters more than the
|
|
224
|
+
* marginal permissiveness of any one host. Profile-specific extras
|
|
225
|
+
* (currently `$schema` stripping on `anthropic`) are gated below.
|
|
226
|
+
*/
|
|
227
|
+
function enforceRoot(schema, ctx) {
|
|
228
|
+
let out = schema;
|
|
229
|
+
let dirty = false;
|
|
230
|
+
const dirtyOnce = () => {
|
|
231
|
+
if (!dirty) {
|
|
232
|
+
out = { ...schema };
|
|
233
|
+
dirty = true;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const t = out.type;
|
|
237
|
+
if (Array.isArray(t)) if (t.includes("object")) {
|
|
238
|
+
dirtyOnce();
|
|
239
|
+
out.type = "object";
|
|
240
|
+
if (t.length > 1) ctx.warnings.push(`${ctx.prefix}collapsed root type:[${t.join(",")}] to 'object'`);
|
|
241
|
+
} else {
|
|
242
|
+
dirtyOnce();
|
|
243
|
+
ctx.warnings.push(`${ctx.prefix}root type:[${t.join(",")}] does not include 'object' — coerced to 'object'`);
|
|
244
|
+
out.type = "object";
|
|
245
|
+
}
|
|
246
|
+
else if (typeof t === "string" && t !== "object") {
|
|
247
|
+
dirtyOnce();
|
|
248
|
+
ctx.warnings.push(`${ctx.prefix}coerced root type:'${t}' → 'object'`);
|
|
249
|
+
out.type = "object";
|
|
250
|
+
} else if (t === void 0) {
|
|
251
|
+
dirtyOnce();
|
|
252
|
+
out.type = "object";
|
|
253
|
+
}
|
|
254
|
+
if (!isPlainObject(out.properties)) {
|
|
255
|
+
if ("properties" in out) ctx.warnings.push(`${ctx.prefix}replaced non-object 'properties' at root with {}`);
|
|
256
|
+
dirtyOnce();
|
|
257
|
+
out.properties = {};
|
|
258
|
+
}
|
|
259
|
+
for (const key of [
|
|
260
|
+
"oneOf",
|
|
261
|
+
"anyOf",
|
|
262
|
+
"allOf"
|
|
263
|
+
]) if (key in out) {
|
|
264
|
+
dirtyOnce();
|
|
265
|
+
ctx.warnings.push(`${ctx.prefix}stripped root '${key}' (providers require a single object schema)`);
|
|
266
|
+
delete out[key];
|
|
267
|
+
}
|
|
268
|
+
if ("$ref" in out) {
|
|
269
|
+
dirtyOnce();
|
|
270
|
+
ctx.warnings.push(`${ctx.prefix}stripped root $ref`);
|
|
271
|
+
delete out.$ref;
|
|
272
|
+
}
|
|
273
|
+
if ("$defs" in out) {
|
|
274
|
+
dirtyOnce();
|
|
275
|
+
delete out.$defs;
|
|
276
|
+
}
|
|
277
|
+
if ("definitions" in out) {
|
|
278
|
+
dirtyOnce();
|
|
279
|
+
delete out.definitions;
|
|
280
|
+
}
|
|
281
|
+
if (ctx.profile === "anthropic" && "$schema" in out) {
|
|
282
|
+
dirtyOnce();
|
|
283
|
+
delete out.$schema;
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Sanitize a single tool's `inputSchema` for safe forwarding to the
|
|
289
|
+
* provider. Returns the rewritten schema + a list of warnings describing
|
|
290
|
+
* everything that changed.
|
|
291
|
+
*
|
|
292
|
+
* Never mutates the input. Returns the **same reference** when no rewrite
|
|
293
|
+
* was needed (clean-schema fast path) — `sanitizeToolSpecs` relies on
|
|
294
|
+
* this to keep the formatTools hot loop allocation-free across turns
|
|
295
|
+
* when the registered tool set is already wire-valid.
|
|
296
|
+
*/
|
|
297
|
+
function sanitizeToolSchema(input, options = {}) {
|
|
298
|
+
const profile = options.profile ?? "permissive";
|
|
299
|
+
const prefix = options.toolName ? `[tool:${options.toolName}] ` : "";
|
|
300
|
+
if (!isPlainObject(input)) return {
|
|
301
|
+
schema: {
|
|
302
|
+
type: "object",
|
|
303
|
+
properties: {}
|
|
304
|
+
},
|
|
305
|
+
warnings: []
|
|
306
|
+
};
|
|
307
|
+
const ctx = {
|
|
308
|
+
root: input,
|
|
309
|
+
warnings: [],
|
|
310
|
+
profile,
|
|
311
|
+
prefix
|
|
312
|
+
};
|
|
313
|
+
return {
|
|
314
|
+
schema: enforceRoot(sanitizeNode(input, ctx, 0, ""), ctx),
|
|
315
|
+
warnings: ctx.warnings
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Convenience: sanitize a batch of tools and emit a single de-duped
|
|
320
|
+
* `console.warn` per unique warning line. Returns the rewritten tools
|
|
321
|
+
* preserving original ordering and reference identity for clean schemas
|
|
322
|
+
* (no reallocation when nothing needed to change).
|
|
323
|
+
*
|
|
324
|
+
* The sanitiser runs every request, so log noise from a stable bad
|
|
325
|
+
* schema would multiply across turns; the de-dupe keeps the signal
|
|
326
|
+
* useful in production logs without dropping the first occurrence.
|
|
327
|
+
*/
|
|
328
|
+
function sanitizeToolSpecs(tools, options = {}) {
|
|
329
|
+
const seen = /* @__PURE__ */ new Set();
|
|
330
|
+
const out = [];
|
|
331
|
+
for (const tool of tools) {
|
|
332
|
+
const result = sanitizeToolSchema(tool.inputSchema, {
|
|
333
|
+
profile: options.profile,
|
|
334
|
+
toolName: tool.name
|
|
335
|
+
});
|
|
336
|
+
if (result.warnings.length === 0 && result.schema === tool.inputSchema) {
|
|
337
|
+
out.push(tool);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
for (const line of result.warnings) {
|
|
341
|
+
if (seen.has(line)) continue;
|
|
342
|
+
seen.add(line);
|
|
343
|
+
(options.onWarning ?? defaultWarn)(line);
|
|
344
|
+
}
|
|
345
|
+
out.push({
|
|
346
|
+
...tool,
|
|
347
|
+
inputSchema: result.schema
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
function defaultWarn(line) {
|
|
353
|
+
console.warn(`[zidane:schema] ${line}`);
|
|
354
|
+
}
|
|
355
|
+
//#endregion
|
|
39
356
|
//#region src/providers/openai-compat.ts
|
|
40
357
|
const TOOL_RESULTS_TAG = "__zidane_tool_results__";
|
|
41
358
|
const ASSISTANT_TOOL_CALLS_TAG = "__zidane_assistant_tc__";
|
|
@@ -415,7 +732,7 @@ function applyOAIToolCacheBreakpoint(tools) {
|
|
|
415
732
|
} : tool);
|
|
416
733
|
}
|
|
417
734
|
function formatTools(tools) {
|
|
418
|
-
return tools.map((t) => ({
|
|
735
|
+
return sanitizeToolSpecs(tools, { profile: "openai" }).map((t) => ({
|
|
419
736
|
type: "function",
|
|
420
737
|
function: {
|
|
421
738
|
name: t.name,
|
|
@@ -448,7 +765,8 @@ function toolResultsMessage(results) {
|
|
|
448
765
|
content: results.map((r) => ({
|
|
449
766
|
type: "tool_result",
|
|
450
767
|
callId: r.id,
|
|
451
|
-
output: r.content
|
|
768
|
+
output: r.content,
|
|
769
|
+
...r.isError ? { isError: true } : {}
|
|
452
770
|
}))
|
|
453
771
|
};
|
|
454
772
|
}
|
|
@@ -1028,97 +1346,342 @@ function toOpenAI(msg) {
|
|
|
1028
1346
|
};
|
|
1029
1347
|
}
|
|
1030
1348
|
/**
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1349
|
+
* Placeholder content inserted into a synthetic `tool_result` when the harness
|
|
1350
|
+
* has to repair an orphan `tool_use`. Exported so downstream consumers
|
|
1351
|
+
* (training-data collectors, HFI submission) can reject any payload containing
|
|
1352
|
+
* it — the marker satisfies the wire-level pairing contract structurally but
|
|
1353
|
+
* the content itself is fake and would poison fine-tuning data.
|
|
1354
|
+
*/
|
|
1355
|
+
const SYNTHETIC_TOOL_RESULT_PLACEHOLDER = "[Tool result missing due to internal error]";
|
|
1356
|
+
/**
|
|
1357
|
+
* Replacement text for an assistant message whose every content block was
|
|
1358
|
+
* stripped during pairing repair (e.g. the only blocks were orphan
|
|
1359
|
+
* `tool_call`s). Providers reject empty `content` arrays — the marker keeps
|
|
1360
|
+
* the turn shape valid while signaling "this assistant turn lost its
|
|
1361
|
+
* outputs" to the model so it can recover.
|
|
1362
|
+
*/
|
|
1363
|
+
const TOOL_USE_INTERRUPTED_MARKER = "[Tool use interrupted]";
|
|
1364
|
+
/**
|
|
1365
|
+
* Replacement text for a user message whose every content block was a
|
|
1366
|
+
* `tool_result` with no matching upstream `tool_call` (e.g. the assistant
|
|
1367
|
+
* pair was stripped by an earlier compaction). Same role as
|
|
1368
|
+
* {@link TOOL_USE_INTERRUPTED_MARKER} on the user side.
|
|
1369
|
+
*/
|
|
1370
|
+
const ORPHANED_TOOL_RESULT_MARKER = "[Orphaned tool result removed due to conversation resume]";
|
|
1371
|
+
/**
|
|
1372
|
+
* Defensive repair pass that rewrites a message list so it satisfies the
|
|
1373
|
+
* wire-level `tool_use` ↔ `tool_result` adjacency contract every modern
|
|
1374
|
+
* provider enforces.
|
|
1375
|
+
*
|
|
1376
|
+
* Anthropic 400s on orphans with `'tool_use' ids were found without
|
|
1377
|
+
* 'tool_result' blocks immediately after` (and its inverse, `tool_result
|
|
1378
|
+
* must be preceded by a tool_call with the same toolCallId`). OpenAI's
|
|
1379
|
+
* Chat Completions API rejects most mismatches with a 400 too.
|
|
1035
1380
|
*
|
|
1036
|
-
*
|
|
1037
|
-
* `tool_result` block in the next user message (`messages.N: 'tool_use' ids
|
|
1038
|
-
* were found without 'tool_result' blocks immediately after`). OpenAI is
|
|
1039
|
-
* looser but still rejects most mismatches. Orphans reach the wire layer from:
|
|
1381
|
+
* Six repair modes — modeled after Anthropic's Claude Code defenses:
|
|
1040
1382
|
*
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1383
|
+
* | # | Corruption | Repair |
|
|
1384
|
+
* |---|------------|--------|
|
|
1385
|
+
* | 1 | assistant `tool_use` whose next user msg lacks a matching `tool_result` | **Prepend** a synthetic `tool_result` block carrying {@link SYNTHETIC_TOOL_RESULT_PLACEHOLDER} with `isError: true` |
|
|
1386
|
+
* | 2 | assistant `tool_use` followed by nothing (or by a non-user msg) | **Insert** a new synthetic user message with the same placeholder |
|
|
1387
|
+
* | 3 | user `tool_result` with no preceding assistant `tool_call` | **Strip** the orphan block; if it empties the msg, replace with {@link ORPHANED_TOOL_RESULT_MARKER} text block |
|
|
1388
|
+
* | 4 | duplicate `tool_call.id` across assistant messages (CC-1212) | **Strip** later instances + their matching tool_results |
|
|
1389
|
+
* | 5 | duplicate `tool_result.callId` within a single user message | **Dedupe** by `callId`, keep first |
|
|
1390
|
+
* | 6 | assistant message emptied by mode-4 stripping | **Replace** content with {@link TOOL_USE_INTERRUPTED_MARKER} text block |
|
|
1049
1391
|
*
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
1392
|
+
* **Repair, not drop.** Earlier versions of this pass simply dropped orphan
|
|
1393
|
+
* blocks, which (a) silently rewrote history the model had reasoned over and
|
|
1394
|
+
* (b) cascaded — dropping an assistant tool_call would orphan its
|
|
1395
|
+
* tool_result, which would then get dropped too, removing any trace of
|
|
1396
|
+
* "what the model tried to do" from the transcript. The repair-based pass
|
|
1397
|
+
* preserves the model's tool_use shape and patches the dangling result with
|
|
1398
|
+
* an `is_error` placeholder so the model sees "I tried X, the result was
|
|
1399
|
+
* lost" and can retry intelligently.
|
|
1054
1400
|
*
|
|
1055
|
-
* Adjacency
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1058
|
-
*
|
|
1059
|
-
* the cleaned output, so a tool_result whose entire assistant prefix was
|
|
1060
|
-
* dropped cascades and gets dropped too.
|
|
1401
|
+
* Adjacency contract: `tool_result` blocks must live in the user message
|
|
1402
|
+
* IMMEDIATELY following the assistant message that emitted the matching
|
|
1403
|
+
* `tool_use`. The pass enforces strict adjacency — a tool_result two
|
|
1404
|
+
* messages downstream of its tool_call is still an orphan.
|
|
1061
1405
|
*
|
|
1062
|
-
* Idempotent: returns the input reference when no
|
|
1406
|
+
* Idempotent: returns the input reference unchanged when no repairs were
|
|
1407
|
+
* necessary. Re-running on already-repaired output is a no-op.
|
|
1408
|
+
*
|
|
1409
|
+
* Pure: does not mutate input messages or their content arrays — every
|
|
1410
|
+
* repair allocates a fresh array / object.
|
|
1063
1411
|
*/
|
|
1064
|
-
function
|
|
1412
|
+
function ensureToolResultPairing(messages, options = {}) {
|
|
1065
1413
|
if (messages.length === 0) return messages;
|
|
1066
|
-
const
|
|
1414
|
+
const fireRepair = options.onRepair ?? (() => {});
|
|
1415
|
+
const firstSeenAt = /* @__PURE__ */ new Map();
|
|
1416
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1417
|
+
const msg = messages[i];
|
|
1418
|
+
if (msg.role !== "assistant") continue;
|
|
1419
|
+
for (const block of msg.content) if (block.type === "tool_call" && !firstSeenAt.has(block.id)) firstSeenAt.set(block.id, i);
|
|
1420
|
+
}
|
|
1421
|
+
const pendingOrphansByUserIndex = /* @__PURE__ */ new Map();
|
|
1067
1422
|
let changed = false;
|
|
1423
|
+
const markChanged = () => {
|
|
1424
|
+
changed = true;
|
|
1425
|
+
};
|
|
1426
|
+
const out = [];
|
|
1068
1427
|
for (let i = 0; i < messages.length; i++) {
|
|
1069
1428
|
const msg = messages[i];
|
|
1070
|
-
if (msg.role === "assistant") {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1429
|
+
if (msg.role === "assistant") processAssistantMessage({
|
|
1430
|
+
index: i,
|
|
1431
|
+
msg,
|
|
1432
|
+
messages,
|
|
1433
|
+
firstSeenAt,
|
|
1434
|
+
pendingOrphansByUserIndex,
|
|
1435
|
+
out,
|
|
1436
|
+
fireRepair,
|
|
1437
|
+
markChanged
|
|
1438
|
+
});
|
|
1439
|
+
else processUserMessage({
|
|
1440
|
+
index: i,
|
|
1441
|
+
msg,
|
|
1442
|
+
pendingOrphansByUserIndex,
|
|
1443
|
+
out,
|
|
1444
|
+
fireRepair,
|
|
1445
|
+
markChanged
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
return changed ? out : messages;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Process one assistant message: strip mode-4 duplicates, queue mode-1 / 2
|
|
1452
|
+
* orphan-tool-use repairs for the next user message (or insert a synthetic
|
|
1453
|
+
* one), and emit a mode-6 marker when dedup empties the message.
|
|
1454
|
+
*/
|
|
1455
|
+
function processAssistantMessage(args) {
|
|
1456
|
+
const { index, msg, messages, firstSeenAt, pendingOrphansByUserIndex, out, fireRepair, markChanged } = args;
|
|
1457
|
+
const dedupedContent = [];
|
|
1458
|
+
const seenHere = /* @__PURE__ */ new Set();
|
|
1459
|
+
let dedupedChanged = false;
|
|
1460
|
+
for (const block of msg.content) {
|
|
1461
|
+
if (block.type === "tool_call") {
|
|
1462
|
+
const isCrossMsgDup = firstSeenAt.get(block.id) !== index;
|
|
1463
|
+
const isIntraMsgDup = seenHere.has(block.id);
|
|
1464
|
+
if (isCrossMsgDup || isIntraMsgDup) {
|
|
1465
|
+
dedupedChanged = true;
|
|
1466
|
+
fireRepair({
|
|
1467
|
+
mode: "duplicate-tool-use-strip",
|
|
1468
|
+
callId: block.id,
|
|
1469
|
+
messageIndex: index
|
|
1470
|
+
});
|
|
1080
1471
|
continue;
|
|
1081
1472
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1473
|
+
seenHere.add(block.id);
|
|
1474
|
+
}
|
|
1475
|
+
dedupedContent.push(block);
|
|
1476
|
+
}
|
|
1477
|
+
const surviving = collectIds(dedupedContent, "tool_call", (b) => b.id);
|
|
1478
|
+
const next = messages[index + 1];
|
|
1479
|
+
const nextIsUser = next && next.role === "user";
|
|
1480
|
+
const nextResultIds = nextIsUser ? collectResultIds(next.content) : /* @__PURE__ */ new Set();
|
|
1481
|
+
const orphans = [];
|
|
1482
|
+
for (const id of surviving) if (!nextResultIds.has(id)) orphans.push(id);
|
|
1483
|
+
emitAssistantAfterDedup({
|
|
1484
|
+
index,
|
|
1485
|
+
msg,
|
|
1486
|
+
dedupedContent,
|
|
1487
|
+
dedupedChanged,
|
|
1488
|
+
out,
|
|
1489
|
+
fireRepair,
|
|
1490
|
+
markChanged
|
|
1491
|
+
});
|
|
1492
|
+
if (orphans.length === 0) return;
|
|
1493
|
+
if (nextIsUser) {
|
|
1494
|
+
markChanged();
|
|
1495
|
+
pendingOrphansByUserIndex.set(index + 1, orphans);
|
|
1496
|
+
for (const callId of orphans) fireRepair({
|
|
1497
|
+
mode: "orphan-tool-use-prepend",
|
|
1498
|
+
callId,
|
|
1499
|
+
messageIndex: index
|
|
1500
|
+
});
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
markChanged();
|
|
1504
|
+
out.push({
|
|
1505
|
+
role: "user",
|
|
1506
|
+
content: orphans.map((callId) => syntheticResultBlock(callId))
|
|
1507
|
+
});
|
|
1508
|
+
for (const callId of orphans) fireRepair({
|
|
1509
|
+
mode: "orphan-tool-use-append",
|
|
1510
|
+
callId,
|
|
1511
|
+
messageIndex: index
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
function emitAssistantAfterDedup(args) {
|
|
1515
|
+
const { index, msg, dedupedContent, dedupedChanged, out, fireRepair, markChanged } = args;
|
|
1516
|
+
if (!dedupedChanged) {
|
|
1517
|
+
out.push(msg);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
markChanged();
|
|
1521
|
+
if (dedupedContent.length === 0) {
|
|
1522
|
+
out.push({
|
|
1523
|
+
...msg,
|
|
1524
|
+
content: [{
|
|
1525
|
+
type: "text",
|
|
1526
|
+
text: TOOL_USE_INTERRUPTED_MARKER
|
|
1527
|
+
}]
|
|
1528
|
+
});
|
|
1529
|
+
fireRepair({
|
|
1530
|
+
mode: "empty-assistant-marker",
|
|
1531
|
+
messageIndex: index
|
|
1532
|
+
});
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
out.push({
|
|
1536
|
+
...msg,
|
|
1537
|
+
content: dedupedContent
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Process one user message: dedup mode-5 duplicate tool_results, strip
|
|
1542
|
+
* mode-3 orphans whose tool_call is missing from the previously-emitted
|
|
1543
|
+
* assistant message, and prepend any synthetic results queued by the prior
|
|
1544
|
+
* assistant's mode-1 repair.
|
|
1545
|
+
*/
|
|
1546
|
+
function processUserMessage(args) {
|
|
1547
|
+
const { index, msg, pendingOrphansByUserIndex, out, fireRepair, markChanged } = args;
|
|
1548
|
+
const queuedOrphans = pendingOrphansByUserIndex.get(index);
|
|
1549
|
+
pendingOrphansByUserIndex.delete(index);
|
|
1550
|
+
const prev = out.length > 0 ? out[out.length - 1] : null;
|
|
1551
|
+
const validIds = prev && prev.role === "assistant" ? collectIds(prev.content, "tool_call", (b) => b.id) : /* @__PURE__ */ new Set();
|
|
1552
|
+
const seenCallIds = /* @__PURE__ */ new Set();
|
|
1553
|
+
let modifiedHere = false;
|
|
1554
|
+
const afterDedup = [];
|
|
1555
|
+
for (const block of msg.content) {
|
|
1556
|
+
if (block.type !== "tool_result") {
|
|
1557
|
+
afterDedup.push(block);
|
|
1088
1558
|
continue;
|
|
1089
1559
|
}
|
|
1090
|
-
if (
|
|
1091
|
-
|
|
1560
|
+
if (seenCallIds.has(block.callId)) {
|
|
1561
|
+
modifiedHere = true;
|
|
1562
|
+
fireRepair({
|
|
1563
|
+
mode: "duplicate-tool-result-strip",
|
|
1564
|
+
callId: block.callId,
|
|
1565
|
+
messageIndex: index
|
|
1566
|
+
});
|
|
1092
1567
|
continue;
|
|
1093
1568
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1569
|
+
seenCallIds.add(block.callId);
|
|
1570
|
+
afterDedup.push(block);
|
|
1571
|
+
}
|
|
1572
|
+
const afterStrip = [];
|
|
1573
|
+
for (const block of afterDedup) {
|
|
1574
|
+
if (block.type === "tool_result" && !validIds.has(block.callId)) {
|
|
1575
|
+
modifiedHere = true;
|
|
1576
|
+
fireRepair({
|
|
1577
|
+
mode: "orphan-tool-result-strip",
|
|
1578
|
+
callId: block.callId,
|
|
1579
|
+
messageIndex: index
|
|
1580
|
+
});
|
|
1099
1581
|
continue;
|
|
1100
1582
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1583
|
+
afterStrip.push(block);
|
|
1584
|
+
}
|
|
1585
|
+
const finalContent = queuedOrphans ? [...queuedOrphans.map(syntheticResultBlock), ...afterStrip] : afterStrip;
|
|
1586
|
+
if (!modifiedHere && queuedOrphans === void 0) {
|
|
1587
|
+
out.push(msg);
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
markChanged();
|
|
1591
|
+
if (finalContent.length === 0) {
|
|
1592
|
+
out.push({
|
|
1103
1593
|
...msg,
|
|
1104
|
-
content:
|
|
1594
|
+
content: [{
|
|
1595
|
+
type: "text",
|
|
1596
|
+
text: ORPHANED_TOOL_RESULT_MARKER
|
|
1597
|
+
}]
|
|
1105
1598
|
});
|
|
1599
|
+
return;
|
|
1106
1600
|
}
|
|
1107
|
-
|
|
1601
|
+
out.push({
|
|
1602
|
+
...msg,
|
|
1603
|
+
content: finalContent
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
function syntheticResultBlock(callId) {
|
|
1607
|
+
return {
|
|
1608
|
+
type: "tool_result",
|
|
1609
|
+
callId,
|
|
1610
|
+
output: SYNTHETIC_TOOL_RESULT_PLACEHOLDER,
|
|
1611
|
+
isError: true
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
function collectResultIds(content) {
|
|
1615
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1616
|
+
for (const block of content) if (block.type === "tool_result") ids.add(block.callId);
|
|
1617
|
+
return ids;
|
|
1108
1618
|
}
|
|
1109
1619
|
function collectIds(content, type, getId) {
|
|
1110
1620
|
const ids = /* @__PURE__ */ new Set();
|
|
1111
1621
|
for (const block of content) if (block.type === type) ids.add(getId(block));
|
|
1112
1622
|
return ids;
|
|
1113
1623
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1624
|
+
/**
|
|
1625
|
+
* Drop ASSISTANT turns whose every `tool_call` block is unresolved
|
|
1626
|
+
* (no matching `tool_result` block anywhere later in the transcript).
|
|
1627
|
+
* Tool_call blocks with at least one matching tool_result are kept — modes
|
|
1628
|
+
* 1/3 of {@link ensureToolResultPairing} handle the partial-pair case at
|
|
1629
|
+
* wire-send time.
|
|
1630
|
+
*
|
|
1631
|
+
* Use case: session resume. A turn that emitted three `tool_use` blocks but
|
|
1632
|
+
* never persisted the matching `tool_result` turn (process death, crash,
|
|
1633
|
+
* `kill -9`) leaves the transcript with an orphan that Anthropic rejects on
|
|
1634
|
+
* the FIRST API call after reload. Dropping the whole assistant turn — not
|
|
1635
|
+
* just the orphan blocks — preserves text-only turns that legitimately
|
|
1636
|
+
* carry reasoning the model wants to see again.
|
|
1637
|
+
*
|
|
1638
|
+
* Does NOT mint fresh ids: re-id'ing on every resume would cause
|
|
1639
|
+
* exponential transcript growth across repeated resumes of an interrupted
|
|
1640
|
+
* session.
|
|
1641
|
+
*
|
|
1642
|
+
* Pure: returns the input reference unchanged when no turn was dropped.
|
|
1643
|
+
*/
|
|
1644
|
+
function filterUnresolvedToolUses(turns) {
|
|
1645
|
+
if (turns.length === 0) return turns;
|
|
1646
|
+
const resolvedIds = /* @__PURE__ */ new Set();
|
|
1647
|
+
for (const turn of turns) for (const block of turn.content) if (block.type === "tool_result") resolvedIds.add(block.callId);
|
|
1648
|
+
let changed = false;
|
|
1649
|
+
const out = [];
|
|
1650
|
+
for (const turn of turns) {
|
|
1651
|
+
if (turn.role !== "assistant") {
|
|
1652
|
+
out.push(turn);
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
const toolCalls = turn.content.filter((b) => b.type === "tool_call");
|
|
1656
|
+
if (toolCalls.length === 0) {
|
|
1657
|
+
out.push(turn);
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
if (toolCalls.every((b) => b.type === "tool_call" && !resolvedIds.has(b.id))) {
|
|
1661
|
+
changed = true;
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
out.push(turn);
|
|
1120
1665
|
}
|
|
1121
|
-
return
|
|
1666
|
+
return changed ? out : turns;
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Inspect a session's trailing turn to classify whether the run was
|
|
1670
|
+
* interrupted mid-tool-call. Pure / synchronous — does not modify the input.
|
|
1671
|
+
*
|
|
1672
|
+
* Pair with {@link filterUnresolvedToolUses} on the resume path: filter
|
|
1673
|
+
* first so the result reflects the post-cleanup state (a turn whose orphans
|
|
1674
|
+
* were stripped reads as 'completed', not 'interrupted').
|
|
1675
|
+
*/
|
|
1676
|
+
function detectTurnInterruption(turns) {
|
|
1677
|
+
if (turns.length === 0) return "clean";
|
|
1678
|
+
const last = turns[turns.length - 1];
|
|
1679
|
+
if (last.role === "user") return "clean";
|
|
1680
|
+
if (last.role !== "assistant") return "clean";
|
|
1681
|
+
const resolvedIds = /* @__PURE__ */ new Set();
|
|
1682
|
+
for (const turn of turns) for (const block of turn.content) if (block.type === "tool_result") resolvedIds.add(block.callId);
|
|
1683
|
+
for (const block of last.content) if (block.type === "tool_call" && !resolvedIds.has(block.id)) return "interrupted";
|
|
1684
|
+
return "completed";
|
|
1122
1685
|
}
|
|
1123
1686
|
function autoDetectAndConvert(msg) {
|
|
1124
1687
|
const c = msg.content;
|
|
@@ -1139,6 +1702,6 @@ function autoDetectAndConvert(msg) {
|
|
|
1139
1702
|
return fromAnthropic(msg);
|
|
1140
1703
|
}
|
|
1141
1704
|
//#endregion
|
|
1142
|
-
export {
|
|
1705
|
+
export { toolResultsMessage as _, detectTurnInterruption as a, sanitizeToolSpecs as b, fromAnthropic as c, toOpenAI as d, OpenAICompatHttpError as f, openaiCompat as g, mapOAIFinishReason as h, autoDetectAndConvert as i, fromOpenAI as l, classifyOpenAICompatError as m, SYNTHETIC_TOOL_RESULT_PLACEHOLDER as n, ensureToolResultPairing as o, assistantMessage as p, TOOL_USE_INTERRUPTED_MARKER as r, filterUnresolvedToolUses as s, ORPHANED_TOOL_RESULT_MARKER as t, toAnthropic as u, userMessage as v, fillEstimatedCost as x, sanitizeToolSchema as y };
|
|
1143
1706
|
|
|
1144
|
-
//# sourceMappingURL=messages-
|
|
1707
|
+
//# sourceMappingURL=messages-D0xT979U.js.map
|