@caupulican/pi-ai 0.81.37 → 0.81.39

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 (32) hide show
  1. package/dist/image-models.generated.d.ts +60 -0
  2. package/dist/image-models.generated.d.ts.map +1 -1
  3. package/dist/image-models.generated.js +60 -0
  4. package/dist/image-models.generated.js.map +1 -1
  5. package/dist/models.generated.d.ts +163 -213
  6. package/dist/models.generated.d.ts.map +1 -1
  7. package/dist/models.generated.js +250 -272
  8. package/dist/models.generated.js.map +1 -1
  9. package/dist/providers/openai-completions.d.ts +1 -1
  10. package/dist/providers/openai-completions.d.ts.map +1 -1
  11. package/dist/providers/openai-completions.js +68 -19
  12. package/dist/providers/openai-completions.js.map +1 -1
  13. package/dist/stream.d.ts.map +1 -1
  14. package/dist/stream.js +145 -2
  15. package/dist/stream.js.map +1 -1
  16. package/dist/utils/tool-repair/repairer.d.ts +13 -1
  17. package/dist/utils/tool-repair/repairer.d.ts.map +1 -1
  18. package/dist/utils/tool-repair/repairer.js +44 -28
  19. package/dist/utils/tool-repair/repairer.js.map +1 -1
  20. package/dist/utils/tool-repair/text-protocol-history.d.ts +17 -0
  21. package/dist/utils/tool-repair/text-protocol-history.d.ts.map +1 -0
  22. package/dist/utils/tool-repair/text-protocol-history.js +22 -0
  23. package/dist/utils/tool-repair/text-protocol-history.js.map +1 -0
  24. package/dist/utils/tool-repair/text-protocol.d.ts +7 -1
  25. package/dist/utils/tool-repair/text-protocol.d.ts.map +1 -1
  26. package/dist/utils/tool-repair/text-protocol.js +44 -18
  27. package/dist/utils/tool-repair/text-protocol.js.map +1 -1
  28. package/dist/utils/validation.d.ts +9 -0
  29. package/dist/utils/validation.d.ts.map +1 -1
  30. package/dist/utils/validation.js +8 -1
  31. package/dist/utils/validation.js.map +1 -1
  32. package/package.json +1 -1
package/dist/stream.js CHANGED
@@ -44,7 +44,7 @@ function withTextToolProtocolContext(context, options) {
44
44
  content: [
45
45
  {
46
46
  type: "text",
47
- text: `Tool-call instructions for this conversation:\n${primer}\n\nDo not answer this instruction. Apply it to subsequent user requests. If the next user request asks to read a file, your first response must be a read tool call and nothing else.`,
47
+ text: "Tool-call instructions for this conversation are defined in the system prompt above. Do not answer this instruction; apply the system-prompt tool-call format to subsequent user requests. If the next user request asks to read a file, your first response must be a read tool call and nothing else.",
48
48
  },
49
49
  ],
50
50
  timestamp: 0,
@@ -66,6 +66,148 @@ async function notifyTextToolProtocolParse(options, event) {
66
66
  // Parse telemetry must not change provider stream semantics.
67
67
  }
68
68
  }
69
+ // Priority order matters: a fenced "```tool_call" block must be matched before the shorter
70
+ // "```tool" opener it starts with, or the scanner would treat "_call" as the fence body.
71
+ const TEXT_PROTOCOL_ENVELOPE_PATTERNS = [
72
+ { opener: "<pi:call", closer: "</pi:call>" },
73
+ { opener: "<tool_call", closer: "</tool_call>" },
74
+ { opener: "```tool_call", closer: "```" },
75
+ { opener: "```tool", closer: "```" },
76
+ { opener: "```json", closer: "```" },
77
+ { opener: "<function", closer: "</function>" },
78
+ ];
79
+ // All recognized openers start with one of these characters; texts without either can never
80
+ // contain (or be growing into) an envelope, so the scan can short-circuit immediately.
81
+ const TEXT_PROTOCOL_ENVELOPE_TRIGGER = /[<`]/;
82
+ function findEarliestEnvelopeOpener(text, fromIndex) {
83
+ let best;
84
+ for (const pattern of TEXT_PROTOCOL_ENVELOPE_PATTERNS) {
85
+ const index = text.indexOf(pattern.opener, fromIndex);
86
+ if (index === -1)
87
+ continue;
88
+ if (!best || index < best.index)
89
+ best = { index, pattern };
90
+ }
91
+ return best;
92
+ }
93
+ // The tail of `text` (from fromIndex to the end) is where new streamed characters land next.
94
+ // If that tail is itself a strict prefix of some opener, it might still grow into a full
95
+ // envelope tag, so it is not yet safe to reveal.
96
+ function longestPendingOpenerPrefixLength(text, fromIndex) {
97
+ const tail = text.slice(fromIndex);
98
+ let longest = 0;
99
+ for (const pattern of TEXT_PROTOCOL_ENVELOPE_PATTERNS) {
100
+ const maxLength = Math.min(tail.length, pattern.opener.length - 1);
101
+ for (let length = maxLength; length > 0; length--) {
102
+ if (pattern.opener.startsWith(tail.slice(tail.length - length))) {
103
+ longest = Math.max(longest, length);
104
+ break;
105
+ }
106
+ }
107
+ }
108
+ return longest;
109
+ }
110
+ // Walks confirmed prose/envelope spans left to right over the FULL text seen so far and
111
+ // returns the PROSE-ONLY text that is safe to reveal live: a closed envelope span is
112
+ // permanently dropped (never later flushed as its own raw markup), while a still-open or
113
+ // merely-pending span halts the scan, leaving everything from there on unrevealed until the
114
+ // stream proves what it is. Recomputing over the whole text on every event (rather than
115
+ // tracking incremental cursor state) makes the scan self-correcting: a false-positive opener
116
+ // candidate (e.g. "```typescript") is released the moment more characters prove it does not
117
+ // match, and the result is always a prefix-extension of the previous call's result, so a
118
+ // simple length-based diff against the last computed value yields the next delta to forward.
119
+ function computeVisibleProseText(text) {
120
+ if (!TEXT_PROTOCOL_ENVELOPE_TRIGGER.test(text))
121
+ return text;
122
+ let pos = 0;
123
+ let visible = "";
124
+ while (pos < text.length) {
125
+ const match = findEarliestEnvelopeOpener(text, pos);
126
+ if (!match) {
127
+ const pendingLength = longestPendingOpenerPrefixLength(text, pos);
128
+ return visible + text.slice(pos, text.length - pendingLength);
129
+ }
130
+ visible += text.slice(pos, match.index);
131
+ const bodyStart = match.index + match.pattern.opener.length;
132
+ const closerIndex = text.indexOf(match.pattern.closer, bodyStart);
133
+ if (closerIndex === -1)
134
+ return visible; // envelope still open: nothing further confirmed
135
+ pos = closerIndex + match.pattern.closer.length; // envelope resolved+suppressed: keep scanning
136
+ }
137
+ return visible;
138
+ }
139
+ // Rewrites any text content block whose revealed prose is still behind its true streamed text
140
+ // so a forwarded event's `partial` snapshot never exposes held-back or suppressed envelope
141
+ // markup, even on events unrelated to the block being held (e.g. a later thinking/toolcall
142
+ // event still carries the earlier text block in its `partial.content`).
143
+ function redactHeldText(partial, revealedText) {
144
+ if (revealedText.size === 0)
145
+ return partial;
146
+ let changed = false;
147
+ const content = partial.content.map((block, index) => {
148
+ if (block.type !== "text")
149
+ return block;
150
+ const revealed = revealedText.get(index);
151
+ if (revealed === undefined || block.text === revealed)
152
+ return block;
153
+ changed = true;
154
+ return { ...block, text: revealed };
155
+ });
156
+ return changed ? { ...partial, content } : partial;
157
+ }
158
+ // Forwards one non-"done" stream event, holding back streamed text that falls inside (or
159
+ // might still grow into) a text-protocol envelope so raw markup like "<pi:call" never reaches
160
+ // a live consumer. Prose outside envelopes is forwarded as soon as it is provably safe; the
161
+ // "done" event swap remains the sole authority for final content.
162
+ function forwardTextProtocolStreamEvent(wrapped, event, revealedText) {
163
+ if (event.type === "error") {
164
+ wrapped.push(event);
165
+ return;
166
+ }
167
+ if (event.type === "text_start") {
168
+ revealedText.set(event.contentIndex, "");
169
+ wrapped.push({ ...event, partial: redactHeldText(event.partial, revealedText) });
170
+ return;
171
+ }
172
+ if (event.type === "text_delta") {
173
+ const block = event.partial.content[event.contentIndex];
174
+ const fullText = block?.type === "text" ? block.text : "";
175
+ const flushed = revealedText.get(event.contentIndex) ?? "";
176
+ const visible = computeVisibleProseText(fullText);
177
+ if (visible.length <= flushed.length)
178
+ return; // held: possibly-envelope text never streams live
179
+ revealedText.set(event.contentIndex, visible);
180
+ wrapped.push({
181
+ ...event,
182
+ delta: visible.slice(flushed.length),
183
+ partial: redactHeldText(event.partial, revealedText),
184
+ });
185
+ return;
186
+ }
187
+ if (event.type === "text_end") {
188
+ const flushed = revealedText.get(event.contentIndex) ?? "";
189
+ const visible = computeVisibleProseText(event.content);
190
+ if (visible.length > flushed.length) {
191
+ // Catch up any newly-provable-safe tail (e.g. a suspected opener prefix that never
192
+ // completed) so the deltas actually forwarded still sum to what text_end reports.
193
+ revealedText.set(event.contentIndex, visible);
194
+ wrapped.push({
195
+ type: "text_delta",
196
+ contentIndex: event.contentIndex,
197
+ delta: visible.slice(flushed.length),
198
+ partial: redactHeldText(event.partial, revealedText),
199
+ });
200
+ }
201
+ const finalVisible = revealedText.get(event.contentIndex) ?? visible;
202
+ wrapped.push({
203
+ ...event,
204
+ content: finalVisible,
205
+ partial: redactHeldText(event.partial, revealedText),
206
+ });
207
+ return;
208
+ }
209
+ wrapped.push({ ...event, partial: redactHeldText(event.partial, revealedText) });
210
+ }
69
211
  function withTextToolProtocolResult(stream, model, context, options) {
70
212
  const protocolOptions = normalizeTextToolProtocolOptions(options?.textToolCallProtocol);
71
213
  const tools = context.tools ?? [];
@@ -73,9 +215,10 @@ function withTextToolProtocolResult(stream, model, context, options) {
73
215
  return stream;
74
216
  const wrapped = new AssistantMessageEventStream();
75
217
  void (async () => {
218
+ const revealedText = new Map();
76
219
  for await (const event of stream) {
77
220
  if (event.type !== "done") {
78
- wrapped.push(event);
221
+ forwardTextProtocolStreamEvent(wrapped, event, revealedText);
79
222
  continue;
80
223
  }
81
224
  const message = event.message;
@@ -1 +1 @@
1
- {"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,kCAAkC,CAAC;AAE1C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAYjD,OAAO,EAAE,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,EACN,8BAA8B,EAC9B,gCAAgC,EAChC,kBAAkB,GAClB,MAAM,sCAAsC,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,SAAS,iBAAiB,CAAC,MAA0B,EAAoB;IACxE,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAAA,CAC9D;AAED,SAAS,aAAa,CACrB,KAAiB,EACjB,OAA6B,EACN;IACvB,IAAI,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC;QAAE,OAAO,OAAO,CAAC;IACvD,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAC;IAC5B,OAAO,EAAE,GAAG,OAAO,EAAE,MAAM,EAAc,CAAC;AAAA,CAC1C;AAED,SAAS,4BAA4B,CAAC,OAAgB,EAAW;IAChE,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,OAAO,CAAC;IAC5C,MAAM,QAAQ,GACb,wIAAwI,CAAC;IAC1I,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ;QAAE,OAAO,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,OAAO,QAAQ,EAAE,EAAE,CAAC;IAC7G,OAAO,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;AAAA,CACvF;AAED,SAAS,6BAA6B,CAAC,QAA4B,EAAa;IAC/E,gFAAgF;IAChF,iFAAiF;IACjF,4DAA4D;IAC5D,OAAO,QAAQ,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;AAAA,CAClD;AAED,SAAS,2BAA2B,CAAC,OAAgB,EAAE,OAAkC,EAAW;IACnG,MAAM,eAAe,GAAG,gCAAgC,CAAC,OAAO,EAAE,oBAAoB,CAAC,CAAC;IACxF,IAAI,CAAC,eAAe,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM;QAAE,OAAO,OAAO,CAAC;IAC/D,MAAM,MAAM,GAAG,8BAA8B,CAAC,OAAO,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;IAC9E,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAC;IAC5B,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,eAAe,EAAE,GAAG,OAAO,CAAC;IACtD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM;QACvC,CAAC,CAAC;YACA;gBACC,IAAI,EAAE,MAAe;gBACrB,OAAO,EAAE;oBACR;wBACC,IAAI,EAAE,MAAe;wBACrB,IAAI,EAAE,kDAAkD,MAAM,wLAAwL;qBACtP;iBACD;gBACD,SAAS,EAAE,CAAC;aACZ;YACD,GAAG,6BAA6B,CAAC,OAAO,CAAC,QAAQ,CAAC;SAClD;QACF,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;IACpB,OAAO;QACN,GAAG,eAAe;QAClB,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,YAAY,OAAO,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM;QACpF,QAAQ;KACR,CAAC;AAAA,CACF;AAED,KAAK,UAAU,2BAA2B,CACzC,OAAkC,EAClC,KAAiC,EACjB;IAChB,IAAI,CAAC;QACJ,MAAM,OAAO,EAAE,uBAAuB,EAAE,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACR,6DAA6D;IAC9D,CAAC;AAAA,CACD;AAED,SAAS,0BAA0B,CAClC,MAAmC,EACnC,KAAiB,EACjB,OAAgB,EAChB,OAAkC,EACJ;IAC9B,MAAM,eAAe,GAAG,gCAAgC,CAAC,OAAO,EAAE,oBAAoB,CAAC,CAAC;IACxF,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;IAClC,IAAI,CAAC,eAAe,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IAC1D,MAAM,OAAO,GAAG,IAAI,2BAA2B,EAAE,CAAC;IAClD,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;QACjB,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAClC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBAC3B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACpB,SAAS;YACV,CAAC;YACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;YAC9B,MAAM,OAAO,GAAgC,EAAE,CAAC;YAChD,IAAI,SAAS,GAAG,CAAC,CAAC;YAClB,IAAI,UAAU,GAAG,CAAC,CAAC;YACnB,IAAI,OAAyD,CAAC;YAC9D,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;gBACrC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;oBAC3B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBACpB,SAAS;gBACV,CAAC;gBACD,UAAU,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;gBAChC,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBACrD,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC/B,IAAI,MAAM,CAAC,OAAO;wBAAE,OAAO,KAAK,MAAM,CAAC,OAAO,CAAC;oBAC/C,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBACpB,SAAS;gBACV,CAAC;gBACD,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;gBACjC,IAAI,MAAM,CAAC,IAAI;oBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC/D,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;YAC/B,CAAC;YACD,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;gBACrB,IAAI,OAAO,EAAE,CAAC;oBACb,MAAM,2BAA2B,CAAC,OAAO,EAAE;wBAC1C,QAAQ,EAAE,KAAK,CAAC,QAAQ;wBACxB,KAAK,EAAE,KAAK,CAAC,EAAE;wBACf,OAAO,EAAE,eAAe,CAAC,OAAO,IAAI,UAAU;wBAC9C,MAAM,EAAE,QAAQ;wBAChB,SAAS,EAAE,CAAC;wBACZ,UAAU;wBACV,MAAM,EAAE,OAAO;qBACf,CAAC,CAAC;gBACJ,CAAC;gBACD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACpB,SAAS;YACV,CAAC;YACD,MAAM,2BAA2B,CAAC,OAAO,EAAE;gBAC1C,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,KAAK,EAAE,KAAK,CAAC,EAAE;gBACf,OAAO,EAAE,eAAe,CAAC,OAAO,IAAI,UAAU;gBAC9C,MAAM,EAAE,QAAQ;gBAChB,SAAS;gBACT,UAAU;aACV,CAAC,CAAC;YACH,OAAO,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,MAAM;gBACZ,MAAM,EAAE,SAAS;gBACjB,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE;aACvD,CAAC,CAAC;QACJ,CAAC;IAAA,CACD,CAAC,EAAE,CAAC;IACL,OAAO,OAAO,CAAC;AAAA,CACf;AAED,SAAS,kBAAkB,CAAC,GAAQ,EAAE;IACrC,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,uCAAuC,GAAG,EAAE,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED,MAAM,UAAU,MAAM,CACrB,KAAkB,EAClB,OAAgB,EAChB,OAA+B,EACD;IAC9B,MAAM,QAAQ,GAAG,kBAAkB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/C,MAAM,eAAe,GAAG,aAAa,CAAC,KAAK,EAAE,OAAO,CAAkB,CAAC;IACvE,MAAM,eAAe,GAAG,2BAA2B,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;IAC9E,OAAO,0BAA0B,CAChC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,eAAe,EAAE,eAAe,CAAC,EACxD,KAAK,EACL,OAAO,EACP,eAAe,CACf,CAAC;AAAA,CACF;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC7B,KAAkB,EAClB,OAAgB,EAChB,OAA+B,EACH;IAC5B,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAC1C,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;AAAA,CAClB;AAED,MAAM,UAAU,YAAY,CAC3B,KAAkB,EAClB,OAAgB,EAChB,OAA6B,EACC;IAC9B,MAAM,QAAQ,GAAG,kBAAkB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/C,MAAM,eAAe,GAAG,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACtD,MAAM,eAAe,GAAG,2BAA2B,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;IAC9E,OAAO,0BAA0B,CAChC,QAAQ,CAAC,YAAY,CAAC,KAAK,EAAE,eAAe,EAAE,eAAe,CAAC,EAC9D,KAAK,EACL,OAAO,EACP,eAAe,CACf,CAAC;AAAA,CACF;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CACnC,KAAkB,EAClB,OAAgB,EAChB,OAA6B,EACD;IAC5B,MAAM,CAAC,GAAG,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAChD,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;AAAA,CAClB","sourcesContent":["import \"./providers/register-builtins.ts\";\n\nimport { getApiProvider } from \"./api-registry.ts\";\nimport { getEnvApiKey } from \"./env-api-keys.ts\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tContext,\n\tMessage,\n\tModel,\n\tProviderStreamOptions,\n\tSimpleStreamOptions,\n\tStreamOptions,\n\tTextToolProtocolParseEvent,\n} from \"./types.ts\";\nimport { AssistantMessageEventStream } from \"./utils/event-stream.ts\";\nimport {\n\tgenerateTextToolProtocolPrimer,\n\tnormalizeTextToolProtocolOptions,\n\tparseTextToolCalls,\n} from \"./utils/tool-repair/text-protocol.ts\";\n\nexport { getEnvApiKey } from \"./env-api-keys.ts\";\n\nfunction hasExplicitApiKey(apiKey: string | undefined): apiKey is string {\n\treturn typeof apiKey === \"string\" && apiKey.trim().length > 0;\n}\n\nfunction withEnvApiKey<TOptions extends StreamOptions>(\n\tmodel: Model<Api>,\n\toptions: TOptions | undefined,\n): TOptions | undefined {\n\tif (hasExplicitApiKey(options?.apiKey)) return options;\n\tconst apiKey = getEnvApiKey(model.provider);\n\tif (!apiKey) return options;\n\treturn { ...options, apiKey } as TOptions;\n}\n\nfunction withTextProtocolUserReminder(message: Message): Message {\n\tif (message.role !== \"user\") return message;\n\tconst reminder =\n\t\t\"Tool-use reminder: if this request asks to read a file, call exactly one tool named read before answering; do not guess file contents.\";\n\tif (typeof message.content === \"string\") return { ...message, content: `${message.content}\\n\\n${reminder}` };\n\treturn { ...message, content: [...message.content, { type: \"text\", text: reminder }] };\n}\n\nfunction withTextProtocolUserReminders(messages: readonly Message[]): Message[] {\n\t// Apply the same transformation to historical user turns. A reminder added only\n\t// to the live turn disappears when that turn becomes history, changing the prior\n\t// request prefix and defeating provider prompt-cache reuse.\n\treturn messages.map(withTextProtocolUserReminder);\n}\n\nfunction withTextToolProtocolContext(context: Context, options: StreamOptions | undefined): Context {\n\tconst protocolOptions = normalizeTextToolProtocolOptions(options?.textToolCallProtocol);\n\tif (!protocolOptions || !context.tools?.length) return context;\n\tconst primer = generateTextToolProtocolPrimer(context.tools, protocolOptions);\n\tif (!primer) return context;\n\tconst { tools: _tools, ...providerContext } = context;\n\tconst messages = context.messages.length\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\trole: \"user\" as const,\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Tool-call instructions for this conversation:\\n${primer}\\n\\nDo not answer this instruction. Apply it to subsequent user requests. If the next user request asks to read a file, your first response must be a read tool call and nothing else.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\ttimestamp: 0,\n\t\t\t\t},\n\t\t\t\t...withTextProtocolUserReminders(context.messages),\n\t\t\t]\n\t\t: context.messages;\n\treturn {\n\t\t...providerContext,\n\t\tsystemPrompt: context.systemPrompt ? `${context.systemPrompt}\\n\\n${primer}` : primer,\n\t\tmessages,\n\t};\n}\n\nasync function notifyTextToolProtocolParse(\n\toptions: StreamOptions | undefined,\n\tevent: TextToolProtocolParseEvent,\n): Promise<void> {\n\ttry {\n\t\tawait options?.onTextToolProtocolParse?.(event);\n\t} catch {\n\t\t// Parse telemetry must not change provider stream semantics.\n\t}\n}\n\nfunction withTextToolProtocolResult(\n\tstream: AssistantMessageEventStream,\n\tmodel: Model<Api>,\n\tcontext: Context,\n\toptions: StreamOptions | undefined,\n): AssistantMessageEventStream {\n\tconst protocolOptions = normalizeTextToolProtocolOptions(options?.textToolCallProtocol);\n\tconst tools = context.tools ?? [];\n\tif (!protocolOptions || tools.length === 0) return stream;\n\tconst wrapped = new AssistantMessageEventStream();\n\tvoid (async () => {\n\t\tfor await (const event of stream) {\n\t\t\tif (event.type !== \"done\") {\n\t\t\t\twrapped.push(event);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst message = event.message;\n\t\t\tconst content: AssistantMessage[\"content\"] = [];\n\t\t\tlet callCount = 0;\n\t\t\tlet textLength = 0;\n\t\t\tlet failure: TextToolProtocolParseEvent[\"reason\"] | undefined;\n\t\t\tfor (const block of message.content) {\n\t\t\t\tif (block.type !== \"text\") {\n\t\t\t\t\tcontent.push(block);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\ttextLength += block.text.length;\n\t\t\t\tconst parsed = parseTextToolCalls(block.text, tools);\n\t\t\t\tif (parsed.calls.length === 0) {\n\t\t\t\t\tif (parsed.failure) failure ??= parsed.failure;\n\t\t\t\t\tcontent.push(block);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tcallCount += parsed.calls.length;\n\t\t\t\tif (parsed.text) content.push({ ...block, text: parsed.text });\n\t\t\t\tcontent.push(...parsed.calls);\n\t\t\t}\n\t\t\tif (callCount === 0) {\n\t\t\t\tif (failure) {\n\t\t\t\t\tawait notifyTextToolProtocolParse(options, {\n\t\t\t\t\t\tprovider: model.provider,\n\t\t\t\t\t\tmodel: model.id,\n\t\t\t\t\t\tvariant: protocolOptions.variant ?? \"tool-tag\",\n\t\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\t\tcallCount: 0,\n\t\t\t\t\t\ttextLength,\n\t\t\t\t\t\treason: failure,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\twrapped.push(event);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tawait notifyTextToolProtocolParse(options, {\n\t\t\t\tprovider: model.provider,\n\t\t\t\tmodel: model.id,\n\t\t\t\tvariant: protocolOptions.variant ?? \"tool-tag\",\n\t\t\t\tstatus: \"parsed\",\n\t\t\t\tcallCount,\n\t\t\t\ttextLength,\n\t\t\t});\n\t\t\twrapped.push({\n\t\t\t\ttype: \"done\",\n\t\t\t\treason: \"toolUse\",\n\t\t\t\tmessage: { ...message, content, stopReason: \"toolUse\" },\n\t\t\t});\n\t\t}\n\t})();\n\treturn wrapped;\n}\n\nfunction resolveApiProvider(api: Api) {\n\tconst provider = getApiProvider(api);\n\tif (!provider) {\n\t\tthrow new Error(`No API provider registered for api: ${api}`);\n\t}\n\treturn provider;\n}\n\nexport function stream<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: ProviderStreamOptions,\n): AssistantMessageEventStream {\n\tconst provider = resolveApiProvider(model.api);\n\tconst resolvedOptions = withEnvApiKey(model, options) as StreamOptions;\n\tconst protocolContext = withTextToolProtocolContext(context, resolvedOptions);\n\treturn withTextToolProtocolResult(\n\t\tprovider.stream(model, protocolContext, resolvedOptions),\n\t\tmodel,\n\t\tcontext,\n\t\tresolvedOptions,\n\t);\n}\n\nexport async function complete<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: ProviderStreamOptions,\n): Promise<AssistantMessage> {\n\tconst s = stream(model, context, options);\n\treturn s.result();\n}\n\nexport function streamSimple<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream {\n\tconst provider = resolveApiProvider(model.api);\n\tconst resolvedOptions = withEnvApiKey(model, options);\n\tconst protocolContext = withTextToolProtocolContext(context, resolvedOptions);\n\treturn withTextToolProtocolResult(\n\t\tprovider.streamSimple(model, protocolContext, resolvedOptions),\n\t\tmodel,\n\t\tcontext,\n\t\tresolvedOptions,\n\t);\n}\n\nexport async function completeSimple<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): Promise<AssistantMessage> {\n\tconst s = streamSimple(model, context, options);\n\treturn s.result();\n}\n"]}
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"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","sourcesContent":["import \"./providers/register-builtins.ts\";\n\nimport { getApiProvider } from \"./api-registry.ts\";\nimport { getEnvApiKey } from \"./env-api-keys.ts\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tAssistantMessageEvent,\n\tContext,\n\tMessage,\n\tModel,\n\tProviderStreamOptions,\n\tSimpleStreamOptions,\n\tStreamOptions,\n\tTextToolProtocolParseEvent,\n} from \"./types.ts\";\nimport { AssistantMessageEventStream } from \"./utils/event-stream.ts\";\nimport {\n\tgenerateTextToolProtocolPrimer,\n\tnormalizeTextToolProtocolOptions,\n\tparseTextToolCalls,\n} from \"./utils/tool-repair/text-protocol.ts\";\n\nexport { getEnvApiKey } from \"./env-api-keys.ts\";\n\nfunction hasExplicitApiKey(apiKey: string | undefined): apiKey is string {\n\treturn typeof apiKey === \"string\" && apiKey.trim().length > 0;\n}\n\nfunction withEnvApiKey<TOptions extends StreamOptions>(\n\tmodel: Model<Api>,\n\toptions: TOptions | undefined,\n): TOptions | undefined {\n\tif (hasExplicitApiKey(options?.apiKey)) return options;\n\tconst apiKey = getEnvApiKey(model.provider);\n\tif (!apiKey) return options;\n\treturn { ...options, apiKey } as TOptions;\n}\n\nfunction withTextProtocolUserReminder(message: Message): Message {\n\tif (message.role !== \"user\") return message;\n\tconst reminder =\n\t\t\"Tool-use reminder: if this request asks to read a file, call exactly one tool named read before answering; do not guess file contents.\";\n\tif (typeof message.content === \"string\") return { ...message, content: `${message.content}\\n\\n${reminder}` };\n\treturn { ...message, content: [...message.content, { type: \"text\", text: reminder }] };\n}\n\nfunction withTextProtocolUserReminders(messages: readonly Message[]): Message[] {\n\t// Apply the same transformation to historical user turns. A reminder added only\n\t// to the live turn disappears when that turn becomes history, changing the prior\n\t// request prefix and defeating provider prompt-cache reuse.\n\treturn messages.map(withTextProtocolUserReminder);\n}\n\nfunction withTextToolProtocolContext(context: Context, options: StreamOptions | undefined): Context {\n\tconst protocolOptions = normalizeTextToolProtocolOptions(options?.textToolCallProtocol);\n\tif (!protocolOptions || !context.tools?.length) return context;\n\tconst primer = generateTextToolProtocolPrimer(context.tools, protocolOptions);\n\tif (!primer) return context;\n\tconst { tools: _tools, ...providerContext } = context;\n\tconst messages = context.messages.length\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\trole: \"user\" as const,\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: \"Tool-call instructions for this conversation are defined in the system prompt above. Do not answer this instruction; apply the system-prompt tool-call format to subsequent user requests. If the next user request asks to read a file, your first response must be a read tool call and nothing else.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\ttimestamp: 0,\n\t\t\t\t},\n\t\t\t\t...withTextProtocolUserReminders(context.messages),\n\t\t\t]\n\t\t: context.messages;\n\treturn {\n\t\t...providerContext,\n\t\tsystemPrompt: context.systemPrompt ? `${context.systemPrompt}\\n\\n${primer}` : primer,\n\t\tmessages,\n\t};\n}\n\nasync function notifyTextToolProtocolParse(\n\toptions: StreamOptions | undefined,\n\tevent: TextToolProtocolParseEvent,\n): Promise<void> {\n\ttry {\n\t\tawait options?.onTextToolProtocolParse?.(event);\n\t} catch {\n\t\t// Parse telemetry must not change provider stream semantics.\n\t}\n}\n\ninterface TextProtocolEnvelopePattern {\n\topener: string;\n\tcloser: string;\n}\n\n// Priority order matters: a fenced \"```tool_call\" block must be matched before the shorter\n// \"```tool\" opener it starts with, or the scanner would treat \"_call\" as the fence body.\nconst TEXT_PROTOCOL_ENVELOPE_PATTERNS: readonly TextProtocolEnvelopePattern[] = [\n\t{ opener: \"<pi:call\", closer: \"</pi:call>\" },\n\t{ opener: \"<tool_call\", closer: \"</tool_call>\" },\n\t{ opener: \"```tool_call\", closer: \"```\" },\n\t{ opener: \"```tool\", closer: \"```\" },\n\t{ opener: \"```json\", closer: \"```\" },\n\t{ opener: \"<function\", closer: \"</function>\" },\n];\n\n// All recognized openers start with one of these characters; texts without either can never\n// contain (or be growing into) an envelope, so the scan can short-circuit immediately.\nconst TEXT_PROTOCOL_ENVELOPE_TRIGGER = /[<`]/;\n\nfunction findEarliestEnvelopeOpener(\n\ttext: string,\n\tfromIndex: number,\n): { index: number; pattern: TextProtocolEnvelopePattern } | undefined {\n\tlet best: { index: number; pattern: TextProtocolEnvelopePattern } | undefined;\n\tfor (const pattern of TEXT_PROTOCOL_ENVELOPE_PATTERNS) {\n\t\tconst index = text.indexOf(pattern.opener, fromIndex);\n\t\tif (index === -1) continue;\n\t\tif (!best || index < best.index) best = { index, pattern };\n\t}\n\treturn best;\n}\n\n// The tail of `text` (from fromIndex to the end) is where new streamed characters land next.\n// If that tail is itself a strict prefix of some opener, it might still grow into a full\n// envelope tag, so it is not yet safe to reveal.\nfunction longestPendingOpenerPrefixLength(text: string, fromIndex: number): number {\n\tconst tail = text.slice(fromIndex);\n\tlet longest = 0;\n\tfor (const pattern of TEXT_PROTOCOL_ENVELOPE_PATTERNS) {\n\t\tconst maxLength = Math.min(tail.length, pattern.opener.length - 1);\n\t\tfor (let length = maxLength; length > 0; length--) {\n\t\t\tif (pattern.opener.startsWith(tail.slice(tail.length - length))) {\n\t\t\t\tlongest = Math.max(longest, length);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\treturn longest;\n}\n\n// Walks confirmed prose/envelope spans left to right over the FULL text seen so far and\n// returns the PROSE-ONLY text that is safe to reveal live: a closed envelope span is\n// permanently dropped (never later flushed as its own raw markup), while a still-open or\n// merely-pending span halts the scan, leaving everything from there on unrevealed until the\n// stream proves what it is. Recomputing over the whole text on every event (rather than\n// tracking incremental cursor state) makes the scan self-correcting: a false-positive opener\n// candidate (e.g. \"```typescript\") is released the moment more characters prove it does not\n// match, and the result is always a prefix-extension of the previous call's result, so a\n// simple length-based diff against the last computed value yields the next delta to forward.\nfunction computeVisibleProseText(text: string): string {\n\tif (!TEXT_PROTOCOL_ENVELOPE_TRIGGER.test(text)) return text;\n\tlet pos = 0;\n\tlet visible = \"\";\n\twhile (pos < text.length) {\n\t\tconst match = findEarliestEnvelopeOpener(text, pos);\n\t\tif (!match) {\n\t\t\tconst pendingLength = longestPendingOpenerPrefixLength(text, pos);\n\t\t\treturn visible + text.slice(pos, text.length - pendingLength);\n\t\t}\n\t\tvisible += text.slice(pos, match.index);\n\t\tconst bodyStart = match.index + match.pattern.opener.length;\n\t\tconst closerIndex = text.indexOf(match.pattern.closer, bodyStart);\n\t\tif (closerIndex === -1) return visible; // envelope still open: nothing further confirmed\n\t\tpos = closerIndex + match.pattern.closer.length; // envelope resolved+suppressed: keep scanning\n\t}\n\treturn visible;\n}\n\n// Rewrites any text content block whose revealed prose is still behind its true streamed text\n// so a forwarded event's `partial` snapshot never exposes held-back or suppressed envelope\n// markup, even on events unrelated to the block being held (e.g. a later thinking/toolcall\n// event still carries the earlier text block in its `partial.content`).\nfunction redactHeldText(partial: AssistantMessage, revealedText: ReadonlyMap<number, string>): AssistantMessage {\n\tif (revealedText.size === 0) return partial;\n\tlet changed = false;\n\tconst content = partial.content.map((block, index) => {\n\t\tif (block.type !== \"text\") return block;\n\t\tconst revealed = revealedText.get(index);\n\t\tif (revealed === undefined || block.text === revealed) return block;\n\t\tchanged = true;\n\t\treturn { ...block, text: revealed };\n\t});\n\treturn changed ? { ...partial, content } : partial;\n}\n\n// Forwards one non-\"done\" stream event, holding back streamed text that falls inside (or\n// might still grow into) a text-protocol envelope so raw markup like \"<pi:call\" never reaches\n// a live consumer. Prose outside envelopes is forwarded as soon as it is provably safe; the\n// \"done\" event swap remains the sole authority for final content.\nfunction forwardTextProtocolStreamEvent(\n\twrapped: AssistantMessageEventStream,\n\tevent: Exclude<AssistantMessageEvent, { type: \"done\" }>,\n\trevealedText: Map<number, string>,\n): void {\n\tif (event.type === \"error\") {\n\t\twrapped.push(event);\n\t\treturn;\n\t}\n\tif (event.type === \"text_start\") {\n\t\trevealedText.set(event.contentIndex, \"\");\n\t\twrapped.push({ ...event, partial: redactHeldText(event.partial, revealedText) });\n\t\treturn;\n\t}\n\tif (event.type === \"text_delta\") {\n\t\tconst block = event.partial.content[event.contentIndex];\n\t\tconst fullText = block?.type === \"text\" ? block.text : \"\";\n\t\tconst flushed = revealedText.get(event.contentIndex) ?? \"\";\n\t\tconst visible = computeVisibleProseText(fullText);\n\t\tif (visible.length <= flushed.length) return; // held: possibly-envelope text never streams live\n\t\trevealedText.set(event.contentIndex, visible);\n\t\twrapped.push({\n\t\t\t...event,\n\t\t\tdelta: visible.slice(flushed.length),\n\t\t\tpartial: redactHeldText(event.partial, revealedText),\n\t\t});\n\t\treturn;\n\t}\n\tif (event.type === \"text_end\") {\n\t\tconst flushed = revealedText.get(event.contentIndex) ?? \"\";\n\t\tconst visible = computeVisibleProseText(event.content);\n\t\tif (visible.length > flushed.length) {\n\t\t\t// Catch up any newly-provable-safe tail (e.g. a suspected opener prefix that never\n\t\t\t// completed) so the deltas actually forwarded still sum to what text_end reports.\n\t\t\trevealedText.set(event.contentIndex, visible);\n\t\t\twrapped.push({\n\t\t\t\ttype: \"text_delta\",\n\t\t\t\tcontentIndex: event.contentIndex,\n\t\t\t\tdelta: visible.slice(flushed.length),\n\t\t\t\tpartial: redactHeldText(event.partial, revealedText),\n\t\t\t});\n\t\t}\n\t\tconst finalVisible = revealedText.get(event.contentIndex) ?? visible;\n\t\twrapped.push({\n\t\t\t...event,\n\t\t\tcontent: finalVisible,\n\t\t\tpartial: redactHeldText(event.partial, revealedText),\n\t\t});\n\t\treturn;\n\t}\n\twrapped.push({ ...event, partial: redactHeldText(event.partial, revealedText) });\n}\n\nfunction withTextToolProtocolResult(\n\tstream: AssistantMessageEventStream,\n\tmodel: Model<Api>,\n\tcontext: Context,\n\toptions: StreamOptions | undefined,\n): AssistantMessageEventStream {\n\tconst protocolOptions = normalizeTextToolProtocolOptions(options?.textToolCallProtocol);\n\tconst tools = context.tools ?? [];\n\tif (!protocolOptions || tools.length === 0) return stream;\n\tconst wrapped = new AssistantMessageEventStream();\n\tvoid (async () => {\n\t\tconst revealedText = new Map<number, string>();\n\t\tfor await (const event of stream) {\n\t\t\tif (event.type !== \"done\") {\n\t\t\t\tforwardTextProtocolStreamEvent(wrapped, event, revealedText);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst message = event.message;\n\t\t\tconst content: AssistantMessage[\"content\"] = [];\n\t\t\tlet callCount = 0;\n\t\t\tlet textLength = 0;\n\t\t\tlet failure: TextToolProtocolParseEvent[\"reason\"] | undefined;\n\t\t\tfor (const block of message.content) {\n\t\t\t\tif (block.type !== \"text\") {\n\t\t\t\t\tcontent.push(block);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\ttextLength += block.text.length;\n\t\t\t\tconst parsed = parseTextToolCalls(block.text, tools);\n\t\t\t\tif (parsed.calls.length === 0) {\n\t\t\t\t\tif (parsed.failure) failure ??= parsed.failure;\n\t\t\t\t\tcontent.push(block);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tcallCount += parsed.calls.length;\n\t\t\t\tif (parsed.text) content.push({ ...block, text: parsed.text });\n\t\t\t\tcontent.push(...parsed.calls);\n\t\t\t}\n\t\t\tif (callCount === 0) {\n\t\t\t\tif (failure) {\n\t\t\t\t\tawait notifyTextToolProtocolParse(options, {\n\t\t\t\t\t\tprovider: model.provider,\n\t\t\t\t\t\tmodel: model.id,\n\t\t\t\t\t\tvariant: protocolOptions.variant ?? \"tool-tag\",\n\t\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\t\tcallCount: 0,\n\t\t\t\t\t\ttextLength,\n\t\t\t\t\t\treason: failure,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\twrapped.push(event);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tawait notifyTextToolProtocolParse(options, {\n\t\t\t\tprovider: model.provider,\n\t\t\t\tmodel: model.id,\n\t\t\t\tvariant: protocolOptions.variant ?? \"tool-tag\",\n\t\t\t\tstatus: \"parsed\",\n\t\t\t\tcallCount,\n\t\t\t\ttextLength,\n\t\t\t});\n\t\t\twrapped.push({\n\t\t\t\ttype: \"done\",\n\t\t\t\treason: \"toolUse\",\n\t\t\t\tmessage: { ...message, content, stopReason: \"toolUse\" },\n\t\t\t});\n\t\t}\n\t})();\n\treturn wrapped;\n}\n\nfunction resolveApiProvider(api: Api) {\n\tconst provider = getApiProvider(api);\n\tif (!provider) {\n\t\tthrow new Error(`No API provider registered for api: ${api}`);\n\t}\n\treturn provider;\n}\n\nexport function stream<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: ProviderStreamOptions,\n): AssistantMessageEventStream {\n\tconst provider = resolveApiProvider(model.api);\n\tconst resolvedOptions = withEnvApiKey(model, options) as StreamOptions;\n\tconst protocolContext = withTextToolProtocolContext(context, resolvedOptions);\n\treturn withTextToolProtocolResult(\n\t\tprovider.stream(model, protocolContext, resolvedOptions),\n\t\tmodel,\n\t\tcontext,\n\t\tresolvedOptions,\n\t);\n}\n\nexport async function complete<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: ProviderStreamOptions,\n): Promise<AssistantMessage> {\n\tconst s = stream(model, context, options);\n\treturn s.result();\n}\n\nexport function streamSimple<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream {\n\tconst provider = resolveApiProvider(model.api);\n\tconst resolvedOptions = withEnvApiKey(model, options);\n\tconst protocolContext = withTextToolProtocolContext(context, resolvedOptions);\n\treturn withTextToolProtocolResult(\n\t\tprovider.streamSimple(model, protocolContext, resolvedOptions),\n\t\tmodel,\n\t\tcontext,\n\t\tresolvedOptions,\n\t);\n}\n\nexport async function completeSimple<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): Promise<AssistantMessage> {\n\tconst s = streamSimple(model, context, options);\n\treturn s.result();\n}\n"]}
@@ -5,12 +5,24 @@ export interface AppliedToolRepair {
5
5
  name: ToolRepairModeName;
6
6
  path: string;
7
7
  }
8
+ /**
9
+ * Bound on the repairer's transform passes (decision D1, tool-call-repair doctrine). Each pass
10
+ * re-checks the whole args once, so this is also the max number of extra `Check` calls a failed
11
+ * call can cost. Measured deepest tested cascade (outer json-string-parse revealing a nested
12
+ * property-case + scalar-coercion pass) converges in 2 passes; this bound is that plus one
13
+ * margin layer, not an arbitrary ceiling.
14
+ */
15
+ export declare const MAX_REPAIR_PASSES = 3;
8
16
  export interface ToolRepairResult {
9
17
  args: Record<string, unknown>;
10
18
  repairsApplied: ToolRepairModeName[];
11
19
  repairs: AppliedToolRepair[];
12
20
  }
13
- export declare function repairToolArguments(toolName: string, schema: Tool["parameters"], args: Record<string, unknown>, errors: readonly ValidationErrorLike[], checkWholeArgs: (candidate: Record<string, unknown>) => boolean): ToolRepairResult | undefined;
21
+ export interface RepairToolArgumentsOptions {
22
+ /** Test-only instrumentation: called once per attempted pass with its zero-based index. */
23
+ onPass?: (pass: number) => void;
24
+ }
25
+ export declare function repairToolArguments(toolName: string, schema: Tool["parameters"], args: Record<string, unknown>, errors: readonly ValidationErrorLike[], checkWholeArgs: (candidate: Record<string, unknown>) => boolean, options?: RepairToolArgumentsOptions): ToolRepairResult | undefined;
14
26
  export declare function formatRepairSummary(repairsApplied: readonly ToolRepairModeName[]): string;
15
27
  export declare function getRepairPathText(path: readonly string[]): string;
16
28
  //# sourceMappingURL=repairer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"repairer.d.ts","sourceRoot":"","sources":["../../../src/utils/tool-repair/repairer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAUN,KAAK,mBAAmB,EACxB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,kBAAkB,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACb;AAKD,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,cAAc,EAAE,kBAAkB,EAAE,CAAC;IACrC,OAAO,EAAE,iBAAiB,EAAE,CAAC;CAC7B;AA0TD,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,EAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,SAAS,mBAAmB,EAAE,EACtC,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,GAC7D,gBAAgB,GAAG,SAAS,CAyC9B;AAED,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,SAAS,kBAAkB,EAAE,GAAG,MAAM,CAEzF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAEjE","sourcesContent":["import { Compile } from \"typebox/compile\";\nimport type { Tool } from \"../../types.ts\";\nimport {\n\tanalyzeToolArgumentErrors,\n\tformatRepairPath,\n\tgetEnumValues,\n\tgetSchemaTypes,\n\tgetValueAtPath,\n\tisJsonSchemaObject,\n\tisRecord,\n\ttype JsonSchemaObject,\n\ttype ToolRepairIssue,\n\ttype ValidationErrorLike,\n} from \"./analyzer.ts\";\nimport type { ToolRepairModeName } from \"./registry.ts\";\n\nexport interface AppliedToolRepair {\n\tname: ToolRepairModeName;\n\tpath: string;\n}\n\nconst schemaValidatorCache = new WeakMap<object, ReturnType<typeof Compile>>();\nconst bashCommandWrapperKeys = new Set([\"cmd\", \"command\", \"script\"]);\n\nexport interface ToolRepairResult {\n\targs: Record<string, unknown>;\n\trepairsApplied: ToolRepairModeName[];\n\trepairs: AppliedToolRepair[];\n}\n\nfunction getSchemaValidator(schema: JsonSchemaObject): ReturnType<typeof Compile> | undefined {\n\ttry {\n\t\tconst key = schema as object;\n\t\tconst cached = schemaValidatorCache.get(key);\n\t\tif (cached) return cached;\n\t\tconst validator = Compile(schema as Tool[\"parameters\"]);\n\t\tschemaValidatorCache.set(key, validator);\n\t\treturn validator;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction setValueAtPath(target: Record<string, unknown>, path: readonly string[], value: unknown): boolean {\n\tif (path.length === 0) return false;\n\tlet cursor: unknown = target;\n\tfor (let index = 0; index < path.length - 1; index++) {\n\t\tconst segment = path[index];\n\t\tif (Array.isArray(cursor)) {\n\t\t\tconst arrayIndex = Number(segment);\n\t\t\tif (!Number.isInteger(arrayIndex)) return false;\n\t\t\tcursor = cursor[arrayIndex];\n\t\t} else if (isRecord(cursor)) {\n\t\t\tcursor = cursor[segment];\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tconst finalSegment = path[path.length - 1];\n\tif (Array.isArray(cursor)) {\n\t\tconst arrayIndex = Number(finalSegment);\n\t\tif (!Number.isInteger(arrayIndex)) return false;\n\t\tcursor[arrayIndex] = value;\n\t\treturn true;\n\t}\n\tif (isRecord(cursor)) {\n\t\tcursor[finalSegment] = value;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nfunction deleteValueAtPath(target: Record<string, unknown>, path: readonly string[]): boolean {\n\tif (path.length === 0) return false;\n\tlet cursor: unknown = target;\n\tfor (let index = 0; index < path.length - 1; index++) {\n\t\tconst segment = path[index];\n\t\tif (Array.isArray(cursor)) {\n\t\t\tconst arrayIndex = Number(segment);\n\t\t\tif (!Number.isInteger(arrayIndex)) return false;\n\t\t\tcursor = cursor[arrayIndex];\n\t\t} else if (isRecord(cursor)) {\n\t\t\tcursor = cursor[segment];\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tconst finalSegment = path[path.length - 1];\n\tif (Array.isArray(cursor)) {\n\t\tconst arrayIndex = Number(finalSegment);\n\t\tif (!Number.isInteger(arrayIndex)) return false;\n\t\tcursor.splice(arrayIndex, 1);\n\t\treturn true;\n\t}\n\tif (isRecord(cursor)) {\n\t\tdelete cursor[finalSegment];\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nfunction normalizeEnumValue(value: string): string {\n\treturn value.trim().replace(/\\s+/g, \" \").toLowerCase();\n}\n\nfunction findNormalizedEnumValue(schema: JsonSchemaObject, value: string): string | undefined {\n\tconst normalized = normalizeEnumValue(value);\n\tconst matches = getEnumValues(schema).filter((candidate) => normalizeEnumValue(candidate) === normalized);\n\treturn matches.length === 1 ? matches[0] : undefined;\n}\n\nfunction parsedContainerMatchesSchemaType(parsed: unknown, schema: JsonSchemaObject): boolean {\n\tconst expectedTypes = getSchemaTypes(schema);\n\tif (expectedTypes.includes(\"array\") && !Array.isArray(parsed)) return false;\n\tif (expectedTypes.includes(\"object\") && (!isRecord(parsed) || Array.isArray(parsed))) return false;\n\treturn true;\n}\n\nfunction normalizeJsonQuoteDrift(value: string): string | undefined {\n\tif (!/[“”]/.test(value)) return undefined;\n\tconst normalizedQuotes = value.replaceAll(\"“\", '\"').replaceAll(\"”\", '\"');\n\treturn normalizedQuotes.replace(/\"([A-Za-z_][A-Za-z0-9_-]*)\\s*:\\s*\"/g, '\"$1\":\"');\n}\n\nfunction escapeRegExp(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction parseJsonLiteralToken(token: string): unknown | undefined {\n\ttry {\n\t\treturn JSON.parse(token) as unknown;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction salvageDeclaredObjectProperties(value: string, schema: JsonSchemaObject): Record<string, unknown> | undefined {\n\tconst properties = schema.properties;\n\tif (!properties) return undefined;\n\tconst trimmed = value.trim();\n\tif (!trimmed.startsWith(\"{\") || !trimmed.endsWith(\"}\")) return undefined;\n\tconst normalized = normalizeJsonQuoteDrift(trimmed) ?? trimmed;\n\tconst salvaged: Record<string, unknown> = {};\n\tfor (const key of Object.keys(properties)) {\n\t\tconst pattern = new RegExp(\n\t\t\t`\"${escapeRegExp(key)}\"\\\\s*:\\\\s*(\"(?:\\\\\\\\.|[^\"\\\\\\\\])*\"|-?\\\\d+(?:\\\\.\\\\d+)?(?:[eE][+-]?\\\\d+)?|true|false|null)`,\n\t\t\t\"g\",\n\t\t);\n\t\tconst matches = [...normalized.matchAll(pattern)];\n\t\tif (matches.length > 1) return undefined;\n\t\tconst token = matches[0]?.[1];\n\t\tif (token === undefined) continue;\n\t\tconst parsed = parseJsonLiteralToken(token);\n\t\tif (parsed === undefined && token !== \"null\") return undefined;\n\t\tsalvaged[key] = parsed;\n\t}\n\treturn Object.keys(salvaged).length > 0 ? salvaged : undefined;\n}\n\nfunction parseJsonContainer(value: string, schema: JsonSchemaObject): unknown | undefined {\n\tconst trimmed = value.trim();\n\tif (!trimmed.startsWith(\"[\") && !trimmed.startsWith(\"{\")) return undefined;\n\tfor (const candidate of [trimmed, normalizeJsonQuoteDrift(trimmed)]) {\n\t\tif (candidate === undefined) continue;\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(candidate);\n\t\t\tif (!parsedContainerMatchesSchemaType(parsed, schema)) continue;\n\t\t\treturn parsed;\n\t\t} catch {}\n\t}\n\treturn undefined;\n}\n\nfunction parseNumber(value: string, schema: JsonSchemaObject): number | undefined {\n\tif (value.trim() === \"\") return undefined;\n\tconst numberValue = Number(value);\n\tif (!Number.isFinite(numberValue)) return undefined;\n\treturn getSchemaTypes(schema).includes(\"integer\") && !Number.isInteger(numberValue) ? undefined : numberValue;\n}\n\nfunction coerceStringifiedNumberArray(value: unknown, schema: JsonSchemaObject): unknown[] | undefined {\n\tif (!Array.isArray(value) || !isJsonSchemaObject(schema.items)) return undefined;\n\tconst itemTypes = getSchemaTypes(schema.items);\n\tif (!itemTypes.some((type) => type === \"number\" || type === \"integer\")) return undefined;\n\tconst numbers: number[] = [];\n\tfor (const item of value) {\n\t\tif (typeof item !== \"string\") return undefined;\n\t\tconst parsed = parseNumber(item, schema.items);\n\t\tif (parsed === undefined) return undefined;\n\t\tnumbers.push(parsed);\n\t}\n\treturn numbers;\n}\n\nfunction unwrapBashCommand(value: unknown): string | undefined {\n\tif (!isRecord(value) || Object.keys(value).length !== 1) return undefined;\n\tconst [key] = Object.keys(value);\n\tif (!bashCommandWrapperKeys.has(key)) return undefined;\n\tconst command = value[key];\n\treturn typeof command === \"string\" ? command : undefined;\n}\n\nfunction transformValue(\n\tissue: ToolRepairIssue,\n\tmode: ToolRepairModeName,\n): { type: \"set\"; value: unknown } | { type: \"delete\" } | undefined {\n\tswitch (mode) {\n\t\tcase \"jsonStringParse\": {\n\t\t\treturn typeof issue.value === \"string\"\n\t\t\t\t? { type: \"set\", value: parseJsonContainer(issue.value, issue.schema) }\n\t\t\t\t: undefined;\n\t\t}\n\t\tcase \"jsonObjectPropertySalvage\":\n\t\t\treturn typeof issue.value === \"string\"\n\t\t\t\t? { type: \"set\", value: salvageDeclaredObjectProperties(issue.value, issue.schema) }\n\t\t\t\t: undefined;\n\t\tcase \"singleObjectWrap\":\n\t\t\treturn isRecord(issue.value) ? { type: \"set\", value: [issue.value] } : undefined;\n\t\tcase \"bareScalarWrap\":\n\t\t\treturn [\"string\", \"number\", \"boolean\"].includes(typeof issue.value)\n\t\t\t\t? { type: \"set\", value: [issue.value] }\n\t\t\t\t: undefined;\n\t\tcase \"emptyObjectPlaceholder\":\n\t\t\treturn !issue.required && isRecord(issue.value) && Object.keys(issue.value).length === 0\n\t\t\t\t? { type: \"delete\" }\n\t\t\t\t: undefined;\n\t\tcase \"numberFromString\": {\n\t\t\tconst parsed = typeof issue.value === \"string\" ? parseNumber(issue.value, issue.schema) : undefined;\n\t\t\treturn parsed === undefined ? undefined : { type: \"set\", value: parsed };\n\t\t}\n\t\tcase \"boolFromString\": {\n\t\t\tconst normalized = typeof issue.value === \"string\" ? issue.value.trim().toLowerCase() : undefined;\n\t\t\tif (normalized === \"true\") return { type: \"set\", value: true };\n\t\t\tif (normalized === \"false\") return { type: \"set\", value: false };\n\t\t\treturn undefined;\n\t\t}\n\t\tcase \"enumCaseNormalize\": {\n\t\t\tconst enumValue =\n\t\t\t\ttypeof issue.value === \"string\" ? findNormalizedEnumValue(issue.schema, issue.value) : undefined;\n\t\t\treturn enumValue === undefined ? undefined : { type: \"set\", value: enumValue };\n\t\t}\n\t\tcase \"singleElementUnwrap\":\n\t\t\treturn Array.isArray(issue.value) && issue.value.length === 1\n\t\t\t\t? { type: \"set\", value: issue.value[0] }\n\t\t\t\t: undefined;\n\t\tcase \"stringifiedNumberInArray\": {\n\t\t\tconst coerced = coerceStringifiedNumberArray(issue.value, issue.schema);\n\t\t\treturn coerced === undefined ? undefined : { type: \"set\", value: coerced };\n\t\t}\n\t\tcase \"nullOptionalDrop\":\n\t\t\treturn !issue.required && issue.value === null ? { type: \"delete\" } : undefined;\n\t\tcase \"nullRequiredBounce\":\n\t\t\treturn undefined;\n\t\tcase \"bashCommandArgvJoin\":\n\t\t\treturn Array.isArray(issue.value) && issue.value.every((item) => typeof item === \"string\")\n\t\t\t\t? { type: \"set\", value: issue.value.join(\" \") }\n\t\t\t\t: undefined;\n\t\tcase \"bashCommandUnwrap\": {\n\t\t\tconst command = unwrapBashCommand(issue.value);\n\t\t\treturn command === undefined ? undefined : { type: \"set\", value: command };\n\t\t}\n\t}\n}\n\nfunction applyTransform(\n\tcandidate: Record<string, unknown>,\n\tissue: ToolRepairIssue,\n\tmode: ToolRepairModeName,\n): Record<string, unknown> | undefined {\n\tconst transform = transformValue(issue, mode);\n\tif (!transform) return undefined;\n\tif (transform.type === \"delete\") {\n\t\treturn deleteValueAtPath(candidate, issue.path) ? candidate : undefined;\n\t}\n\tif (transform.value === undefined) return undefined;\n\tif (issue.path.length === 0) {\n\t\treturn isRecord(transform.value) ? transform.value : undefined;\n\t}\n\treturn setValueAtPath(candidate, issue.path, transform.value) ? candidate : undefined;\n}\n\nfunction refreshIssueValue(candidate: Record<string, unknown>, issue: ToolRepairIssue): ToolRepairIssue {\n\treturn { ...issue, value: getValueAtPath(candidate, issue.path) };\n}\n\nfunction normalizePropertyCaseAtPath(\n\tvalue: unknown,\n\tschema: JsonSchemaObject,\n\tpath: readonly string[],\n\trepairs: AppliedToolRepair[],\n): void {\n\tif (Array.isArray(value)) {\n\t\tif (!isJsonSchemaObject(schema.items)) return;\n\t\tfor (let index = 0; index < value.length; index++) {\n\t\t\tnormalizePropertyCaseAtPath(value[index], schema.items, [...path, String(index)], repairs);\n\t\t}\n\t\treturn;\n\t}\n\tif (!isRecord(value) || !schema.properties) return;\n\n\tconst canonicalByLower = new Map<string, string[]>();\n\tfor (const key of Object.keys(schema.properties)) {\n\t\tconst lower = key.toLowerCase();\n\t\tcanonicalByLower.set(lower, [...(canonicalByLower.get(lower) ?? []), key]);\n\t}\n\tfor (const key of Object.keys(value)) {\n\t\tconst matches = canonicalByLower.get(key.toLowerCase()) ?? [];\n\t\tconst canonical = matches.length === 1 ? matches[0] : undefined;\n\t\tif (!canonical || canonical === key || canonical in value) continue;\n\t\tvalue[canonical] = value[key];\n\t\tdelete value[key];\n\t\trepairs.push({ name: \"propertyCaseNormalize\", path: formatRepairPath([...path, canonical]) });\n\t}\n\n\tfor (const [key, propertySchema] of Object.entries(schema.properties)) {\n\t\tif (!(key in value)) continue;\n\t\tnormalizePropertyCaseAtPath(value[key], propertySchema, [...path, key], repairs);\n\t}\n}\n\nfunction normalizePropertyCase(args: Record<string, unknown>, schema: Tool[\"parameters\"]): AppliedToolRepair[] {\n\tif (!isRecord(args) || !isJsonSchemaObject(schema)) return [];\n\tconst repairs: AppliedToolRepair[] = [];\n\tnormalizePropertyCaseAtPath(args, schema, [], repairs);\n\treturn repairs;\n}\n\nfunction validationErrorsForCandidate(\n\tschema: Tool[\"parameters\"],\n\tcandidate: Record<string, unknown>,\n): ValidationErrorLike[] | undefined {\n\tconst validator = getSchemaValidator(schema as JsonSchemaObject);\n\tif (!validator) return undefined;\n\treturn [...validator.Errors(candidate)].map((error) => ({\n\t\tinstancePath: error.instancePath,\n\t\tkeyword: error.keyword,\n\t\tmessage: error.message,\n\t}));\n}\n\nexport function repairToolArguments(\n\ttoolName: string,\n\tschema: Tool[\"parameters\"],\n\targs: Record<string, unknown>,\n\terrors: readonly ValidationErrorLike[],\n\tcheckWholeArgs: (candidate: Record<string, unknown>) => boolean,\n): ToolRepairResult | undefined {\n\tlet candidate = structuredClone(args);\n\tconst repairsApplied: ToolRepairModeName[] = [];\n\tconst repairs: AppliedToolRepair[] = [];\n\tlet currentErrors: readonly ValidationErrorLike[] = errors;\n\tconst maxPasses = 8;\n\n\tfor (let pass = 0; pass < maxPasses; pass++) {\n\t\tconst caseRepairs = normalizePropertyCase(candidate, schema);\n\t\tif (caseRepairs.length > 0) {\n\t\t\trepairsApplied.push(...caseRepairs.map((repair) => repair.name));\n\t\t\trepairs.push(...caseRepairs);\n\t\t\tif (checkWholeArgs(candidate)) return { args: candidate, repairsApplied, repairs };\n\t\t\tcurrentErrors = validationErrorsForCandidate(schema, candidate) ?? currentErrors;\n\t\t}\n\n\t\tconst issues = analyzeToolArgumentErrors(toolName, schema, candidate, currentErrors);\n\t\tlet transformed = false;\n\t\tfor (const issue of issues) {\n\t\t\tif (issue.modes.includes(\"nullRequiredBounce\")) continue;\n\t\t\tconst currentIssue = refreshIssueValue(candidate, issue);\n\t\t\tfor (const mode of currentIssue.modes) {\n\t\t\t\tif (mode === \"nullRequiredBounce\") continue;\n\t\t\t\tconst nextCandidate = applyTransform(candidate, currentIssue, mode);\n\t\t\t\tif (!nextCandidate) continue;\n\t\t\t\tcandidate = nextCandidate;\n\t\t\t\trepairsApplied.push(mode);\n\t\t\t\trepairs.push({ name: mode, path: formatRepairPath(currentIssue.path) });\n\t\t\t\ttransformed = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (checkWholeArgs(candidate)) return { args: candidate, repairsApplied, repairs };\n\t\tif (!transformed) return undefined;\n\t\tconst nextErrors = validationErrorsForCandidate(schema, candidate);\n\t\tif (!nextErrors || nextErrors.length === 0) return undefined;\n\t\tcurrentErrors = nextErrors;\n\t}\n\n\treturn undefined;\n}\n\nexport function formatRepairSummary(repairsApplied: readonly ToolRepairModeName[]): string {\n\treturn repairsApplied.length === 0 ? \"none\" : repairsApplied.join(\", \");\n}\n\nexport function getRepairPathText(path: readonly string[]): string {\n\treturn formatRepairPath(path);\n}\n"]}
1
+ {"version":3,"file":"repairer.d.ts","sourceRoot":"","sources":["../../../src/utils/tool-repair/repairer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAE3C,OAAO,EAUN,KAAK,mBAAmB,EACxB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,kBAAkB,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAInC,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,cAAc,EAAE,kBAAkB,EAAE,CAAC;IACrC,OAAO,EAAE,iBAAiB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IAC1C,2FAA2F;IAC3F,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AA2UD,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC,EAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,SAAS,mBAAmB,EAAE,EACtC,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,EAC/D,OAAO,CAAC,EAAE,0BAA0B,GAClC,gBAAgB,GAAG,SAAS,CAyC9B;AAED,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,SAAS,kBAAkB,EAAE,GAAG,MAAM,CAEzF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAEjE","sourcesContent":["import type { Tool } from \"../../types.ts\";\nimport { getValidator } from \"../validation.ts\";\nimport {\n\tanalyzeToolArgumentErrors,\n\tformatRepairPath,\n\tgetEnumValues,\n\tgetSchemaTypes,\n\tgetValueAtPath,\n\tisJsonSchemaObject,\n\tisRecord,\n\ttype JsonSchemaObject,\n\ttype ToolRepairIssue,\n\ttype ValidationErrorLike,\n} from \"./analyzer.ts\";\nimport type { ToolRepairModeName } from \"./registry.ts\";\n\nexport interface AppliedToolRepair {\n\tname: ToolRepairModeName;\n\tpath: string;\n}\n\n/**\n * Bound on the repairer's transform passes (decision D1, tool-call-repair doctrine). Each pass\n * re-checks the whole args once, so this is also the max number of extra `Check` calls a failed\n * call can cost. Measured deepest tested cascade (outer json-string-parse revealing a nested\n * property-case + scalar-coercion pass) converges in 2 passes; this bound is that plus one\n * margin layer, not an arbitrary ceiling.\n */\nexport const MAX_REPAIR_PASSES = 3;\n\nconst bashCommandWrapperKeys = new Set([\"cmd\", \"command\", \"script\"]);\n\nexport interface ToolRepairResult {\n\targs: Record<string, unknown>;\n\trepairsApplied: ToolRepairModeName[];\n\trepairs: AppliedToolRepair[];\n}\n\nexport interface RepairToolArgumentsOptions {\n\t/** Test-only instrumentation: called once per attempted pass with its zero-based index. */\n\tonPass?: (pass: number) => void;\n}\n\nfunction setValueAtPath(target: Record<string, unknown>, path: readonly string[], value: unknown): boolean {\n\tif (path.length === 0) return false;\n\tlet cursor: unknown = target;\n\tfor (let index = 0; index < path.length - 1; index++) {\n\t\tconst segment = path[index];\n\t\tif (Array.isArray(cursor)) {\n\t\t\tconst arrayIndex = Number(segment);\n\t\t\tif (!Number.isInteger(arrayIndex)) return false;\n\t\t\tcursor = cursor[arrayIndex];\n\t\t} else if (isRecord(cursor)) {\n\t\t\tcursor = cursor[segment];\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tconst finalSegment = path[path.length - 1];\n\tif (Array.isArray(cursor)) {\n\t\tconst arrayIndex = Number(finalSegment);\n\t\tif (!Number.isInteger(arrayIndex)) return false;\n\t\tcursor[arrayIndex] = value;\n\t\treturn true;\n\t}\n\tif (isRecord(cursor)) {\n\t\tcursor[finalSegment] = value;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nfunction deleteValueAtPath(target: Record<string, unknown>, path: readonly string[]): boolean {\n\tif (path.length === 0) return false;\n\tlet cursor: unknown = target;\n\tfor (let index = 0; index < path.length - 1; index++) {\n\t\tconst segment = path[index];\n\t\tif (Array.isArray(cursor)) {\n\t\t\tconst arrayIndex = Number(segment);\n\t\t\tif (!Number.isInteger(arrayIndex)) return false;\n\t\t\tcursor = cursor[arrayIndex];\n\t\t} else if (isRecord(cursor)) {\n\t\t\tcursor = cursor[segment];\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tconst finalSegment = path[path.length - 1];\n\tif (Array.isArray(cursor)) {\n\t\tconst arrayIndex = Number(finalSegment);\n\t\tif (!Number.isInteger(arrayIndex)) return false;\n\t\tcursor.splice(arrayIndex, 1);\n\t\treturn true;\n\t}\n\tif (isRecord(cursor)) {\n\t\tdelete cursor[finalSegment];\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nfunction normalizeEnumValue(value: string): string {\n\treturn value.trim().replace(/\\s+/g, \" \").toLowerCase();\n}\n\nfunction findNormalizedEnumValue(schema: JsonSchemaObject, value: string): string | undefined {\n\tconst normalized = normalizeEnumValue(value);\n\tconst matches = getEnumValues(schema).filter((candidate) => normalizeEnumValue(candidate) === normalized);\n\treturn matches.length === 1 ? matches[0] : undefined;\n}\n\nfunction parsedContainerMatchesSchemaType(parsed: unknown, schema: JsonSchemaObject): boolean {\n\tconst expectedTypes = getSchemaTypes(schema);\n\tif (expectedTypes.includes(\"array\") && !Array.isArray(parsed)) return false;\n\tif (expectedTypes.includes(\"object\") && (!isRecord(parsed) || Array.isArray(parsed))) return false;\n\treturn true;\n}\n\nfunction normalizeJsonQuoteDrift(value: string): string | undefined {\n\tif (!/[“”]/.test(value)) return undefined;\n\tconst normalizedQuotes = value.replaceAll(\"“\", '\"').replaceAll(\"”\", '\"');\n\treturn normalizedQuotes.replace(/\"([A-Za-z_][A-Za-z0-9_-]*)\\s*:\\s*\"/g, '\"$1\":\"');\n}\n\nfunction escapeRegExp(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction parseJsonLiteralToken(token: string): unknown | undefined {\n\ttry {\n\t\treturn JSON.parse(token) as unknown;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Per-schema cache of the declared-property salvage matchers (decision D4, tool-call-repair\n * doctrine): one `RegExp` per declared property, compiled once per schema object and reused\n * across every salvage attempt against that schema instead of rebuilding it per attempt. Each\n * regex keeps its global flag but is only ever driven via `matchAll`, which iterates over a\n * spec-cloned copy of the regex and never mutates the cached instance's `lastIndex` - safe to share.\n */\nconst salvageMatcherCache = new WeakMap<object, Map<string, RegExp>>();\n\nfunction getSalvageMatchers(\n\tschema: JsonSchemaObject,\n\tproperties: Record<string, JsonSchemaObject>,\n): Map<string, RegExp> {\n\tconst key = schema as object;\n\tconst cached = salvageMatcherCache.get(key);\n\tif (cached) return cached;\n\tconst matchers = new Map<string, RegExp>();\n\tfor (const propertyKey of Object.keys(properties)) {\n\t\tmatchers.set(\n\t\t\tpropertyKey,\n\t\t\tnew RegExp(\n\t\t\t\t`\"${escapeRegExp(propertyKey)}\"\\\\s*:\\\\s*(\"(?:\\\\\\\\.|[^\"\\\\\\\\])*\"|-?\\\\d+(?:\\\\.\\\\d+)?(?:[eE][+-]?\\\\d+)?|true|false|null)`,\n\t\t\t\t\"g\",\n\t\t\t),\n\t\t);\n\t}\n\tsalvageMatcherCache.set(key, matchers);\n\treturn matchers;\n}\n\nfunction salvageDeclaredObjectProperties(value: string, schema: JsonSchemaObject): Record<string, unknown> | undefined {\n\tconst properties = schema.properties;\n\tif (!properties) return undefined;\n\tconst trimmed = value.trim();\n\tif (!trimmed.startsWith(\"{\") || !trimmed.endsWith(\"}\")) return undefined;\n\tconst normalized = normalizeJsonQuoteDrift(trimmed) ?? trimmed;\n\tconst matchers = getSalvageMatchers(schema, properties);\n\tconst salvaged: Record<string, unknown> = {};\n\tfor (const [key, pattern] of matchers) {\n\t\tconst matches = [...normalized.matchAll(pattern)];\n\t\tif (matches.length > 1) return undefined;\n\t\tconst token = matches[0]?.[1];\n\t\tif (token === undefined) continue;\n\t\tconst parsed = parseJsonLiteralToken(token);\n\t\tif (parsed === undefined && token !== \"null\") return undefined;\n\t\tsalvaged[key] = parsed;\n\t}\n\treturn Object.keys(salvaged).length > 0 ? salvaged : undefined;\n}\n\nfunction parseJsonContainer(value: string, schema: JsonSchemaObject): unknown | undefined {\n\tconst trimmed = value.trim();\n\tif (!trimmed.startsWith(\"[\") && !trimmed.startsWith(\"{\")) return undefined;\n\tfor (const candidate of [trimmed, normalizeJsonQuoteDrift(trimmed)]) {\n\t\tif (candidate === undefined) continue;\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(candidate);\n\t\t\tif (!parsedContainerMatchesSchemaType(parsed, schema)) continue;\n\t\t\treturn parsed;\n\t\t} catch {}\n\t}\n\treturn undefined;\n}\n\nfunction parseNumber(value: string, schema: JsonSchemaObject): number | undefined {\n\tif (value.trim() === \"\") return undefined;\n\tconst numberValue = Number(value);\n\tif (!Number.isFinite(numberValue)) return undefined;\n\treturn getSchemaTypes(schema).includes(\"integer\") && !Number.isInteger(numberValue) ? undefined : numberValue;\n}\n\nfunction coerceStringifiedNumberArray(value: unknown, schema: JsonSchemaObject): unknown[] | undefined {\n\tif (!Array.isArray(value) || !isJsonSchemaObject(schema.items)) return undefined;\n\tconst itemTypes = getSchemaTypes(schema.items);\n\tif (!itemTypes.some((type) => type === \"number\" || type === \"integer\")) return undefined;\n\tconst numbers: number[] = [];\n\tfor (const item of value) {\n\t\tif (typeof item !== \"string\") return undefined;\n\t\tconst parsed = parseNumber(item, schema.items);\n\t\tif (parsed === undefined) return undefined;\n\t\tnumbers.push(parsed);\n\t}\n\treturn numbers;\n}\n\nfunction unwrapBashCommand(value: unknown): string | undefined {\n\tif (!isRecord(value) || Object.keys(value).length !== 1) return undefined;\n\tconst [key] = Object.keys(value);\n\tif (!bashCommandWrapperKeys.has(key)) return undefined;\n\tconst command = value[key];\n\treturn typeof command === \"string\" ? command : undefined;\n}\n\nfunction transformValue(\n\tissue: ToolRepairIssue,\n\tmode: ToolRepairModeName,\n): { type: \"set\"; value: unknown } | { type: \"delete\" } | undefined {\n\tswitch (mode) {\n\t\tcase \"jsonStringParse\": {\n\t\t\treturn typeof issue.value === \"string\"\n\t\t\t\t? { type: \"set\", value: parseJsonContainer(issue.value, issue.schema) }\n\t\t\t\t: undefined;\n\t\t}\n\t\tcase \"jsonObjectPropertySalvage\":\n\t\t\treturn typeof issue.value === \"string\"\n\t\t\t\t? { type: \"set\", value: salvageDeclaredObjectProperties(issue.value, issue.schema) }\n\t\t\t\t: undefined;\n\t\tcase \"singleObjectWrap\":\n\t\t\treturn isRecord(issue.value) ? { type: \"set\", value: [issue.value] } : undefined;\n\t\tcase \"bareScalarWrap\":\n\t\t\treturn [\"string\", \"number\", \"boolean\"].includes(typeof issue.value)\n\t\t\t\t? { type: \"set\", value: [issue.value] }\n\t\t\t\t: undefined;\n\t\tcase \"emptyObjectPlaceholder\":\n\t\t\treturn !issue.required && isRecord(issue.value) && Object.keys(issue.value).length === 0\n\t\t\t\t? { type: \"delete\" }\n\t\t\t\t: undefined;\n\t\tcase \"numberFromString\": {\n\t\t\tconst parsed = typeof issue.value === \"string\" ? parseNumber(issue.value, issue.schema) : undefined;\n\t\t\treturn parsed === undefined ? undefined : { type: \"set\", value: parsed };\n\t\t}\n\t\tcase \"boolFromString\": {\n\t\t\tconst normalized = typeof issue.value === \"string\" ? issue.value.trim().toLowerCase() : undefined;\n\t\t\tif (normalized === \"true\") return { type: \"set\", value: true };\n\t\t\tif (normalized === \"false\") return { type: \"set\", value: false };\n\t\t\treturn undefined;\n\t\t}\n\t\tcase \"enumCaseNormalize\": {\n\t\t\tconst enumValue =\n\t\t\t\ttypeof issue.value === \"string\" ? findNormalizedEnumValue(issue.schema, issue.value) : undefined;\n\t\t\treturn enumValue === undefined ? undefined : { type: \"set\", value: enumValue };\n\t\t}\n\t\tcase \"singleElementUnwrap\":\n\t\t\treturn Array.isArray(issue.value) && issue.value.length === 1\n\t\t\t\t? { type: \"set\", value: issue.value[0] }\n\t\t\t\t: undefined;\n\t\tcase \"stringifiedNumberInArray\": {\n\t\t\tconst coerced = coerceStringifiedNumberArray(issue.value, issue.schema);\n\t\t\treturn coerced === undefined ? undefined : { type: \"set\", value: coerced };\n\t\t}\n\t\tcase \"nullOptionalDrop\":\n\t\t\treturn !issue.required && issue.value === null ? { type: \"delete\" } : undefined;\n\t\tcase \"nullRequiredBounce\":\n\t\t\treturn undefined;\n\t\tcase \"bashCommandArgvJoin\":\n\t\t\treturn Array.isArray(issue.value) && issue.value.every((item) => typeof item === \"string\")\n\t\t\t\t? { type: \"set\", value: issue.value.join(\" \") }\n\t\t\t\t: undefined;\n\t\tcase \"bashCommandUnwrap\": {\n\t\t\tconst command = unwrapBashCommand(issue.value);\n\t\t\treturn command === undefined ? undefined : { type: \"set\", value: command };\n\t\t}\n\t}\n}\n\nfunction applyTransform(\n\tcandidate: Record<string, unknown>,\n\tissue: ToolRepairIssue,\n\tmode: ToolRepairModeName,\n): Record<string, unknown> | undefined {\n\tconst transform = transformValue(issue, mode);\n\tif (!transform) return undefined;\n\tif (transform.type === \"delete\") {\n\t\treturn deleteValueAtPath(candidate, issue.path) ? candidate : undefined;\n\t}\n\tif (transform.value === undefined) return undefined;\n\tif (issue.path.length === 0) {\n\t\treturn isRecord(transform.value) ? transform.value : undefined;\n\t}\n\treturn setValueAtPath(candidate, issue.path, transform.value) ? candidate : undefined;\n}\n\nfunction refreshIssueValue(candidate: Record<string, unknown>, issue: ToolRepairIssue): ToolRepairIssue {\n\treturn { ...issue, value: getValueAtPath(candidate, issue.path) };\n}\n\nfunction normalizePropertyCaseAtPath(\n\tvalue: unknown,\n\tschema: JsonSchemaObject,\n\tpath: readonly string[],\n\trepairs: AppliedToolRepair[],\n): void {\n\tif (Array.isArray(value)) {\n\t\tif (!isJsonSchemaObject(schema.items)) return;\n\t\tfor (let index = 0; index < value.length; index++) {\n\t\t\tnormalizePropertyCaseAtPath(value[index], schema.items, [...path, String(index)], repairs);\n\t\t}\n\t\treturn;\n\t}\n\tif (!isRecord(value) || !schema.properties) return;\n\n\tconst canonicalByLower = new Map<string, string[]>();\n\tfor (const key of Object.keys(schema.properties)) {\n\t\tconst lower = key.toLowerCase();\n\t\tcanonicalByLower.set(lower, [...(canonicalByLower.get(lower) ?? []), key]);\n\t}\n\tfor (const key of Object.keys(value)) {\n\t\tconst matches = canonicalByLower.get(key.toLowerCase()) ?? [];\n\t\tconst canonical = matches.length === 1 ? matches[0] : undefined;\n\t\tif (!canonical || canonical === key || canonical in value) continue;\n\t\tvalue[canonical] = value[key];\n\t\tdelete value[key];\n\t\trepairs.push({ name: \"propertyCaseNormalize\", path: formatRepairPath([...path, canonical]) });\n\t}\n\n\tfor (const [key, propertySchema] of Object.entries(schema.properties)) {\n\t\tif (!(key in value)) continue;\n\t\tnormalizePropertyCaseAtPath(value[key], propertySchema, [...path, key], repairs);\n\t}\n}\n\nfunction normalizePropertyCase(args: Record<string, unknown>, schema: Tool[\"parameters\"]): AppliedToolRepair[] {\n\tif (!isRecord(args) || !isJsonSchemaObject(schema)) return [];\n\tconst repairs: AppliedToolRepair[] = [];\n\tnormalizePropertyCaseAtPath(args, schema, [], repairs);\n\treturn repairs;\n}\n\nfunction validationErrorsForCandidate(\n\tschema: Tool[\"parameters\"],\n\tcandidate: Record<string, unknown>,\n): ValidationErrorLike[] | undefined {\n\ttry {\n\t\tconst validator = getValidator(schema);\n\t\treturn [...validator.Errors(candidate)].map((error) => ({\n\t\t\tinstancePath: error.instancePath,\n\t\t\tkeyword: error.keyword,\n\t\t\tmessage: error.message,\n\t\t}));\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function repairToolArguments(\n\ttoolName: string,\n\tschema: Tool[\"parameters\"],\n\targs: Record<string, unknown>,\n\terrors: readonly ValidationErrorLike[],\n\tcheckWholeArgs: (candidate: Record<string, unknown>) => boolean,\n\toptions?: RepairToolArgumentsOptions,\n): ToolRepairResult | undefined {\n\tlet candidate = structuredClone(args);\n\tconst repairsApplied: ToolRepairModeName[] = [];\n\tconst repairs: AppliedToolRepair[] = [];\n\tlet currentErrors: readonly ValidationErrorLike[] = errors;\n\n\tfor (let pass = 0; pass < MAX_REPAIR_PASSES; pass++) {\n\t\toptions?.onPass?.(pass);\n\t\tconst caseRepairs = normalizePropertyCase(candidate, schema);\n\t\tif (caseRepairs.length > 0) {\n\t\t\trepairsApplied.push(...caseRepairs.map((repair) => repair.name));\n\t\t\trepairs.push(...caseRepairs);\n\t\t\tif (checkWholeArgs(candidate)) return { args: candidate, repairsApplied, repairs };\n\t\t\tcurrentErrors = validationErrorsForCandidate(schema, candidate) ?? currentErrors;\n\t\t}\n\n\t\tconst issues = analyzeToolArgumentErrors(toolName, schema, candidate, currentErrors);\n\t\tlet transformed = false;\n\t\tfor (const issue of issues) {\n\t\t\tif (issue.modes.includes(\"nullRequiredBounce\")) continue;\n\t\t\tconst currentIssue = refreshIssueValue(candidate, issue);\n\t\t\tfor (const mode of currentIssue.modes) {\n\t\t\t\tif (mode === \"nullRequiredBounce\") continue;\n\t\t\t\tconst nextCandidate = applyTransform(candidate, currentIssue, mode);\n\t\t\t\tif (!nextCandidate) continue;\n\t\t\t\tcandidate = nextCandidate;\n\t\t\t\trepairsApplied.push(mode);\n\t\t\t\trepairs.push({ name: mode, path: formatRepairPath(currentIssue.path) });\n\t\t\t\ttransformed = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (checkWholeArgs(candidate)) return { args: candidate, repairsApplied, repairs };\n\t\tif (!transformed) return undefined;\n\t\tconst nextErrors = validationErrorsForCandidate(schema, candidate);\n\t\tif (!nextErrors || nextErrors.length === 0) return undefined;\n\t\tcurrentErrors = nextErrors;\n\t}\n\n\treturn undefined;\n}\n\nexport function formatRepairSummary(repairsApplied: readonly ToolRepairModeName[]): string {\n\treturn repairsApplied.length === 0 ? \"none\" : repairsApplied.join(\", \");\n}\n\nexport function getRepairPathText(path: readonly string[]): string {\n\treturn formatRepairPath(path);\n}\n"]}
@@ -1,21 +1,14 @@
1
- import { Compile } from "typebox/compile";
1
+ import { getValidator } from "../validation.js";
2
2
  import { analyzeToolArgumentErrors, formatRepairPath, getEnumValues, getSchemaTypes, getValueAtPath, isJsonSchemaObject, isRecord, } from "./analyzer.js";
3
- const schemaValidatorCache = new WeakMap();
3
+ /**
4
+ * Bound on the repairer's transform passes (decision D1, tool-call-repair doctrine). Each pass
5
+ * re-checks the whole args once, so this is also the max number of extra `Check` calls a failed
6
+ * call can cost. Measured deepest tested cascade (outer json-string-parse revealing a nested
7
+ * property-case + scalar-coercion pass) converges in 2 passes; this bound is that plus one
8
+ * margin layer, not an arbitrary ceiling.
9
+ */
10
+ export const MAX_REPAIR_PASSES = 3;
4
11
  const bashCommandWrapperKeys = new Set(["cmd", "command", "script"]);
5
- function getSchemaValidator(schema) {
6
- try {
7
- const key = schema;
8
- const cached = schemaValidatorCache.get(key);
9
- if (cached)
10
- return cached;
11
- const validator = Compile(schema);
12
- schemaValidatorCache.set(key, validator);
13
- return validator;
14
- }
15
- catch {
16
- return undefined;
17
- }
18
- }
19
12
  function setValueAtPath(target, path, value) {
20
13
  if (path.length === 0)
21
14
  return false;
@@ -115,6 +108,26 @@ function parseJsonLiteralToken(token) {
115
108
  return undefined;
116
109
  }
117
110
  }
111
+ /**
112
+ * Per-schema cache of the declared-property salvage matchers (decision D4, tool-call-repair
113
+ * doctrine): one `RegExp` per declared property, compiled once per schema object and reused
114
+ * across every salvage attempt against that schema instead of rebuilding it per attempt. Each
115
+ * regex keeps its global flag but is only ever driven via `matchAll`, which iterates over a
116
+ * spec-cloned copy of the regex and never mutates the cached instance's `lastIndex` - safe to share.
117
+ */
118
+ const salvageMatcherCache = new WeakMap();
119
+ function getSalvageMatchers(schema, properties) {
120
+ const key = schema;
121
+ const cached = salvageMatcherCache.get(key);
122
+ if (cached)
123
+ return cached;
124
+ const matchers = new Map();
125
+ for (const propertyKey of Object.keys(properties)) {
126
+ matchers.set(propertyKey, new RegExp(`"${escapeRegExp(propertyKey)}"\\s*:\\s*("(?:\\\\.|[^"\\\\])*"|-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?|true|false|null)`, "g"));
127
+ }
128
+ salvageMatcherCache.set(key, matchers);
129
+ return matchers;
130
+ }
118
131
  function salvageDeclaredObjectProperties(value, schema) {
119
132
  const properties = schema.properties;
120
133
  if (!properties)
@@ -123,9 +136,9 @@ function salvageDeclaredObjectProperties(value, schema) {
123
136
  if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
124
137
  return undefined;
125
138
  const normalized = normalizeJsonQuoteDrift(trimmed) ?? trimmed;
139
+ const matchers = getSalvageMatchers(schema, properties);
126
140
  const salvaged = {};
127
- for (const key of Object.keys(properties)) {
128
- const pattern = new RegExp(`"${escapeRegExp(key)}"\\s*:\\s*("(?:\\\\.|[^"\\\\])*"|-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?|true|false|null)`, "g");
141
+ for (const [key, pattern] of matchers) {
129
142
  const matches = [...normalized.matchAll(pattern)];
130
143
  if (matches.length > 1)
131
144
  return undefined;
@@ -305,22 +318,25 @@ function normalizePropertyCase(args, schema) {
305
318
  return repairs;
306
319
  }
307
320
  function validationErrorsForCandidate(schema, candidate) {
308
- const validator = getSchemaValidator(schema);
309
- if (!validator)
321
+ try {
322
+ const validator = getValidator(schema);
323
+ return [...validator.Errors(candidate)].map((error) => ({
324
+ instancePath: error.instancePath,
325
+ keyword: error.keyword,
326
+ message: error.message,
327
+ }));
328
+ }
329
+ catch {
310
330
  return undefined;
311
- return [...validator.Errors(candidate)].map((error) => ({
312
- instancePath: error.instancePath,
313
- keyword: error.keyword,
314
- message: error.message,
315
- }));
331
+ }
316
332
  }
317
- export function repairToolArguments(toolName, schema, args, errors, checkWholeArgs) {
333
+ export function repairToolArguments(toolName, schema, args, errors, checkWholeArgs, options) {
318
334
  let candidate = structuredClone(args);
319
335
  const repairsApplied = [];
320
336
  const repairs = [];
321
337
  let currentErrors = errors;
322
- const maxPasses = 8;
323
- for (let pass = 0; pass < maxPasses; pass++) {
338
+ for (let pass = 0; pass < MAX_REPAIR_PASSES; pass++) {
339
+ options?.onPass?.(pass);
324
340
  const caseRepairs = normalizePropertyCase(candidate, schema);
325
341
  if (caseRepairs.length > 0) {
326
342
  repairsApplied.push(...caseRepairs.map((repair) => repair.name));