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.
Files changed (67) hide show
  1. package/README.md +7 -5
  2. package/dist/{agent-CGQajqtC.d.ts → agent-bKs7MRT2.d.ts} +429 -4
  3. package/dist/agent-bKs7MRT2.d.ts.map +1 -0
  4. package/dist/chat.d.ts +212 -58
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +2 -2
  7. package/dist/{errors-COmsomd5.js → errors-Byb0F8B9.js} +44 -2
  8. package/dist/errors-Byb0F8B9.js.map +1 -0
  9. package/dist/{index-DwbcFBr_.d.ts → index-BlMvPh9X.d.ts} +29 -3
  10. package/dist/index-BlMvPh9X.d.ts.map +1 -0
  11. package/dist/{index-BDP6mA3Y.d.ts → index-CTmNaIDb.d.ts} +2 -2
  12. package/dist/{index-BDP6mA3Y.d.ts.map → index-CTmNaIDb.d.ts.map} +1 -1
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +10 -10
  15. package/dist/{interpolate-BhmHKD6x.js → interpolate-ERgZUxgg.js} +2 -2
  16. package/dist/{interpolate-BhmHKD6x.js.map → interpolate-ERgZUxgg.js.map} +1 -1
  17. package/dist/{login-D7Tp-K5f.js → login-CNS9_8Ue.js} +3 -3
  18. package/dist/{login-D7Tp-K5f.js.map → login-CNS9_8Ue.js.map} +1 -1
  19. package/dist/{mcp-B1psg7jf.js → mcp-ZsSFo4Dp.js} +2 -2
  20. package/dist/{mcp-B1psg7jf.js.map → mcp-ZsSFo4Dp.js.map} +1 -1
  21. package/dist/mcp.d.ts +1 -1
  22. package/dist/mcp.js +1 -1
  23. package/dist/{messages-DsbMYNmt.js → messages-D0xT979U.js} +631 -68
  24. package/dist/messages-D0xT979U.js.map +1 -0
  25. package/dist/{presets-AgF0RFx1.js → presets-h5i3kpOP.js} +2 -2
  26. package/dist/{presets-AgF0RFx1.js.map → presets-h5i3kpOP.js.map} +1 -1
  27. package/dist/presets.d.ts +2 -2
  28. package/dist/presets.js +1 -1
  29. package/dist/{providers-v1Rn2rqG.js → providers-x3LZByR5.js} +38 -6
  30. package/dist/providers-x3LZByR5.js.map +1 -0
  31. package/dist/providers.d.ts +2 -2
  32. package/dist/providers.js +3 -3
  33. package/dist/session/sqlite.d.ts +1 -1
  34. package/dist/session/sqlite.js +1 -1
  35. package/dist/{session-DOJgRXvF.js → session-BHZwxmfr.js} +2 -2
  36. package/dist/{session-DOJgRXvF.js.map → session-BHZwxmfr.js.map} +1 -1
  37. package/dist/session.d.ts +1 -1
  38. package/dist/session.js +2 -2
  39. package/dist/skills.d.ts +2 -2
  40. package/dist/skills.js +1 -1
  41. package/dist/{tools-BRbbfdJh.js → tools-CWEDS2ZT.js} +251 -47
  42. package/dist/tools-CWEDS2ZT.js.map +1 -0
  43. package/dist/tools.d.ts +2 -2
  44. package/dist/tools.js +1 -1
  45. package/dist/{transcript-anchors-BBuIoU0x.d.ts → transcript-anchors-DOUqyvXR.d.ts} +28 -4
  46. package/dist/transcript-anchors-DOUqyvXR.d.ts.map +1 -0
  47. package/dist/tui.d.ts +29 -3
  48. package/dist/tui.d.ts.map +1 -1
  49. package/dist/tui.js +363 -28
  50. package/dist/tui.js.map +1 -1
  51. package/dist/{turn-operations-gJ0qtLPv.js → turn-operations-D9HvatsR.js} +396 -89
  52. package/dist/turn-operations-D9HvatsR.js.map +1 -0
  53. package/dist/types-IcokUOyC.js.map +1 -1
  54. package/dist/types.d.ts +2 -2
  55. package/dist/types.js +1 -1
  56. package/docs/ARCHITECTURE.md +3 -2
  57. package/docs/CHAT.md +55 -16
  58. package/docs/TUI.md +22 -2
  59. package/package.json +1 -1
  60. package/dist/agent-CGQajqtC.d.ts.map +0 -1
  61. package/dist/errors-COmsomd5.js.map +0 -1
  62. package/dist/index-DwbcFBr_.d.ts.map +0 -1
  63. package/dist/messages-DsbMYNmt.js.map +0 -1
  64. package/dist/providers-v1Rn2rqG.js.map +0 -1
  65. package/dist/tools-BRbbfdJh.js.map +0 -1
  66. package/dist/transcript-anchors-BBuIoU0x.d.ts.map +0 -1
  67. package/dist/turn-operations-gJ0qtLPv.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { o as errorMessage, s as matchesContextExceeded } from "./errors-COmsomd5.js";
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
- * Drop assistant `tool_call` blocks whose `id` has no matching
1032
- * `tool_result.callId` in the immediately-following user message, and drop
1033
- * user `tool_result` blocks whose `callId` has no match in the
1034
- * immediately-preceding assistant message.
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
- * Anthropic strictly requires every `tool_use` block to be paired with a
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
- * 1. Interrupted runs (signal, crash, network drop) between persisting the
1042
- * assistant turn (`turn:after`) and persisting its tool results turn
1043
- * (`tool-results:after`).
1044
- * 2. The schema-enforcement path a synthetic `__output__` assistant
1045
- * tool_call is appended without a matching tool_result. Any resume after
1046
- * that turn would 400 without this pass.
1047
- * 3. Consumer `context:transform` hooks that prune messages without
1048
- * preserving the `tool_use tool_result` pairing.
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
- * Strategy: drop the orphan blocks rather than synthesizing fake results —
1051
- * the model never sees a half-done call, so it can't confabulate on phantom
1052
- * outputs. Messages emptied by the cleanup are removed entirely (providers
1053
- * reject empty `content` arrays).
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 rules:
1056
- * - Assistant tool_calls are matched against the ORIGINAL next user message
1057
- * (the wire still has to satisfy the per-message adjacency contract).
1058
- * - User tool_results are matched against the previously-emitted message in
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 orphans are found.
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 sanitizeOrphanedToolCalls(messages) {
1412
+ function ensureToolResultPairing(messages, options = {}) {
1065
1413
  if (messages.length === 0) return messages;
1066
- const out = [];
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
- const callIds = collectIds(msg.content, "tool_call", (b) => b.id);
1072
- if (callIds.size === 0) {
1073
- out.push(msg);
1074
- continue;
1075
- }
1076
- const next = messages[i + 1];
1077
- const matched = next && next.role === "user" ? intersectIds(next.content, "tool_result", (b) => b.callId, callIds) : /* @__PURE__ */ new Set();
1078
- if (matched.size === callIds.size) {
1079
- out.push(msg);
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
- const cleaned = msg.content.filter((b) => b.type !== "tool_call" || matched.has(b.id));
1083
- changed = true;
1084
- if (cleaned.length > 0) out.push({
1085
- ...msg,
1086
- content: cleaned
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 (!msg.content.some((b) => b.type === "tool_result")) {
1091
- out.push(msg);
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
- const prev = out.length > 0 ? out[out.length - 1] : null;
1095
- const validIds = prev && prev.role === "assistant" ? collectIds(prev.content, "tool_call", (b) => b.id) : /* @__PURE__ */ new Set();
1096
- const cleaned = msg.content.filter((b) => b.type !== "tool_result" || validIds.has(b.callId));
1097
- if (cleaned.length === msg.content.length) {
1098
- out.push(msg);
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
- changed = true;
1102
- if (cleaned.length > 0) out.push({
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: cleaned
1594
+ content: [{
1595
+ type: "text",
1596
+ text: ORPHANED_TOOL_RESULT_MARKER
1597
+ }]
1105
1598
  });
1599
+ return;
1106
1600
  }
1107
- return changed ? out : messages;
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
- function intersectIds(content, type, getId, candidates) {
1115
- const matched = /* @__PURE__ */ new Set();
1116
- for (const block of content) {
1117
- if (block.type !== type) continue;
1118
- const id = getId(block);
1119
- if (candidates.has(id)) matched.add(id);
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 matched;
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 { toAnthropic as a, assistantMessage as c, openaiCompat as d, toolResultsMessage as f, sanitizeOrphanedToolCalls as i, classifyOpenAICompatError as l, fillEstimatedCost as m, fromAnthropic as n, toOpenAI as o, userMessage as p, fromOpenAI as r, OpenAICompatHttpError as s, autoDetectAndConvert as t, mapOAIFinishReason as u };
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-DsbMYNmt.js.map
1707
+ //# sourceMappingURL=messages-D0xT979U.js.map