opencode-zen-freex 0.2.15 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MAINTAINING.md +8 -0
- package/NOTICE +4 -3
- package/README.md +38 -98
- package/limits.mjs +8 -2
- package/models.mjs +105 -8
- package/package.json +1 -1
- package/provider.mjs +250 -15
- package/stream.mjs +244 -40
- package/tool-response.mjs +438 -54
package/tool-response.mjs
CHANGED
|
@@ -60,10 +60,38 @@ function foldsTo(declared, key) {
|
|
|
60
60
|
return folded;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// The names the policy says the agent declared. A field that is missing or
|
|
64
|
+
// renamed used to read as an empty set — `new Set(undefined)` does not throw —
|
|
65
|
+
// so every call of every reply became "a tool this agent doesn't have", with
|
|
66
|
+
// nothing thrown and nothing logged to say why. A policy this guard cannot
|
|
67
|
+
// read is a caller's mistake and is refused by name instead: the Set this
|
|
68
|
+
// package's own producer builds (provider.mjs's normalizeTools), or an array
|
|
69
|
+
// of names. It is a TypeError, not a GuardError: the reply is not what is
|
|
70
|
+
// wrong, the caller is.
|
|
71
|
+
function declaredNames(policy) {
|
|
72
|
+
const held = policy?.declared;
|
|
73
|
+
const names = held instanceof Set ? [...held] : held;
|
|
74
|
+
// A field that is not one of the two shapes gets here as itself: `names` is
|
|
75
|
+
// the field's own value unless it was a Set, so an array never reaches this
|
|
76
|
+
// message (the old `Array.isArray(held) ? "an array of those"` arm could not
|
|
77
|
+
// be read at all).
|
|
78
|
+
if (!Array.isArray(names))
|
|
79
|
+
throw new TypeError(
|
|
80
|
+
`policy.declared must be a Set or an array of tool names, not ${
|
|
81
|
+
held === null ? "null" : typeof held
|
|
82
|
+
}`,
|
|
83
|
+
);
|
|
84
|
+
for (const name of names) {
|
|
85
|
+
if (typeof name !== "string")
|
|
86
|
+
throw new TypeError(`policy.declared must hold tool names, and holds ${typeof name}`);
|
|
87
|
+
}
|
|
88
|
+
return new Set(names);
|
|
89
|
+
}
|
|
90
|
+
|
|
63
91
|
// nameGuard answers the agent's name for a call, or null for a call of no
|
|
64
92
|
// tool the agent has (a missing name included).
|
|
65
93
|
function nameGuard(policy) {
|
|
66
|
-
const declared =
|
|
94
|
+
const declared = declaredNames(policy);
|
|
67
95
|
const lower = (name) => name.toLowerCase();
|
|
68
96
|
const bare = (name) => name.toLowerCase().replace(separators, "");
|
|
69
97
|
const byCase = foldsTo(declared, lower);
|
|
@@ -158,6 +186,16 @@ function isCall(protocol, value) {
|
|
|
158
186
|
);
|
|
159
187
|
}
|
|
160
188
|
|
|
189
|
+
// What a call is named by, when it is named at all: a non-empty string. ""
|
|
190
|
+
// names nothing a client can key by — the empty id this route was measured
|
|
191
|
+
// putting on a call — and neither does a number or a boolean. Every place
|
|
192
|
+
// that asks whether a call has an identity asks it here, or two calls of one
|
|
193
|
+
// reply share one item (measured: two `"call_id": ""` calls each became the
|
|
194
|
+
// item `msg_`, and their arguments frames could not be matched to either).
|
|
195
|
+
function identity(value) {
|
|
196
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
161
199
|
// Counts the ids invented for calls the reply gives nothing to place: a
|
|
162
200
|
// counter cannot be stable, but it is the only thing that keeps two of them
|
|
163
201
|
// apart (see inPlaceOf).
|
|
@@ -166,8 +204,11 @@ let synthesized = 0;
|
|
|
166
204
|
// What a call left out of a Responses or Messages reply becomes: text
|
|
167
205
|
// saying so, in the call's place, so no later index moves. `at` is where the
|
|
168
206
|
// call sits in the reply as far as the caller can tell — a frame's
|
|
169
|
-
// output_index, or the index the walk found it at.
|
|
170
|
-
|
|
207
|
+
// output_index, or the index the walk found it at. `placed` is the id a
|
|
208
|
+
// caller that follows the whole stream has already answered for this call
|
|
209
|
+
// (see snapshotStream): without it every rewrite of that call invented a new
|
|
210
|
+
// id.
|
|
211
|
+
function inPlaceOf(protocol, call, note, at, placed) {
|
|
171
212
|
if (protocol === "anthropic") return { type: "text", text: note };
|
|
172
213
|
// An item id is what a Responses client keys the item by, and what the
|
|
173
214
|
// streaming path suppresses arguments by. One call is replaced more than
|
|
@@ -175,16 +216,24 @@ function inPlaceOf(protocol, call, note, at) {
|
|
|
175
216
|
// the terminal response's output — so the invented id has to be the same
|
|
176
217
|
// every time: a counter made one call into several items to the client, and
|
|
177
218
|
// left leftIDs unable to match its arguments frames. A call's place is the
|
|
178
|
-
// same in all three, and two calls cannot share one, so the id names it
|
|
219
|
+
// same in all three, and two calls cannot share one, so the id names it —
|
|
220
|
+
// and a call the reply places nowhere takes the id its own stream settled
|
|
221
|
+
// on for it.
|
|
179
222
|
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
223
|
+
// The id a stream already answered comes first, before the place the caller
|
|
224
|
+
// knows. The id the call *itself* carries comes last, and only behind the
|
|
225
|
+
// `msg_` the invented kind is named under: a word is not the guard's to hand
|
|
226
|
+
// out twice. A reply can name two items by one word (this route was measured
|
|
227
|
+
// settling on an id only after the frame that opened the call, and nothing
|
|
228
|
+
// stops the same id naming a call the reply keeps), and the note taking that
|
|
229
|
+
// id verbatim would key two items — the note and the item the reply kept —
|
|
230
|
+
// by one word, which is the failure this arm exists to prevent, read back at
|
|
231
|
+
// the other end. Behind the prefix the word cannot collide with an item of
|
|
232
|
+
// the reply, and a call_id the reply used is still named in the id it
|
|
233
|
+
// answers, as it was.
|
|
184
234
|
const id =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
: `msg_${call.call_id ?? (at === undefined ? `anon_${++synthesized}` : `dropped_${at}`)}`;
|
|
235
|
+
placed ??
|
|
236
|
+
`msg_${identity(call.call_id) ?? (at === undefined ? `anon_${++synthesized}` : `dropped_${at}`)}`;
|
|
188
237
|
return {
|
|
189
238
|
type: "message",
|
|
190
239
|
id,
|
|
@@ -206,18 +255,38 @@ function inPlaceOf(protocol, call, note, at) {
|
|
|
206
255
|
// the wire leaves with an id: the last-resort kind, process-unique under the
|
|
207
256
|
// same counter the invented item ids use, so two invented calls cannot share
|
|
208
257
|
// one.
|
|
258
|
+
//
|
|
259
|
+
// An invented id is only process-unique, and the counter is shared with every
|
|
260
|
+
// earlier reply, so it can reach a word the same reply already placed: an
|
|
261
|
+
// upstream that names its calls call_1, call_2, … names the very ids the
|
|
262
|
+
// counter mints, and a call left unnamed beside those used to be handed one of
|
|
263
|
+
// them — two calls of one reply under one id, which is the same failure the
|
|
264
|
+
// counter exists to prevent, one step further out. The ids the calls being
|
|
265
|
+
// flushed already carry are read first, and an invented id misses all of them.
|
|
209
266
|
function ensureCallIDs(calls) {
|
|
267
|
+
const held = new Set();
|
|
268
|
+
for (const call of calls) {
|
|
269
|
+
const id = identity(call.id);
|
|
270
|
+
if (id !== undefined) held.add(id);
|
|
271
|
+
}
|
|
210
272
|
for (const call of calls) {
|
|
211
|
-
if (
|
|
212
|
-
|
|
273
|
+
if (identity(call.id) !== undefined) continue;
|
|
274
|
+
let id;
|
|
275
|
+
do id = `call_${++synthesized}`;
|
|
276
|
+
while (held.has(id));
|
|
277
|
+
held.add(id);
|
|
278
|
+
call.id = id;
|
|
213
279
|
}
|
|
214
280
|
}
|
|
215
281
|
|
|
216
282
|
// checkSnapshots checks every call in a reply or event: renamed to the
|
|
217
283
|
// agent's tool, or (Responses, Messages) put in place of by a note. It
|
|
218
284
|
// answers whether data changed, how many calls it kept and how many it
|
|
219
|
-
// left out.
|
|
220
|
-
|
|
285
|
+
// left out. `placeOf` is the stream's own answer for a call whose frames
|
|
286
|
+
// carry no output_index and no item id — a token already invented for it, so
|
|
287
|
+
// all of its rewrites answer one item id (snapshotStream); a caller reading a
|
|
288
|
+
// whole reply has no such state and leaves it out.
|
|
289
|
+
function checkSnapshots(data, protocol, checkName, placeOf) {
|
|
221
290
|
let changed = false;
|
|
222
291
|
if (protocol === "chat") {
|
|
223
292
|
for (const choice of list(data.choices)) {
|
|
@@ -236,15 +305,15 @@ function checkSnapshots(data, protocol, checkName) {
|
|
|
236
305
|
let kept = 0;
|
|
237
306
|
let left = 0;
|
|
238
307
|
if (!object(data)) throw new GuardError("Invalid upstream tool snapshot");
|
|
239
|
-
// Each entry is a value, where it sits,
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
// output_index the frame carries
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
const stack = [[data, null, null, object(data) ? data.output_index : undefined]];
|
|
308
|
+
// Each entry is a value, where it sits, the place it holds in the reply as
|
|
309
|
+
// far as the walk knows, and whether the walk reached it as an item of a
|
|
310
|
+
// list (an item of an output array, as opposed to a frame's own `item`,
|
|
311
|
+
// which keeps the output_index the frame carries). A call left out is
|
|
312
|
+
// replaced by an item whose invented id names that place, so the added,
|
|
313
|
+
// done and terminal copies of one call are one item (inPlaceOf).
|
|
314
|
+
const stack = [[data, null, null, object(data) ? data.output_index : undefined, false]];
|
|
246
315
|
while (stack.length) {
|
|
247
|
-
const [value, holder, key, at] = stack.pop();
|
|
316
|
+
const [value, holder, key, at, listed] = stack.pop();
|
|
248
317
|
if (!object(value)) throw new GuardError("Invalid upstream tool snapshot");
|
|
249
318
|
if (
|
|
250
319
|
protocol === "responses" &&
|
|
@@ -258,7 +327,13 @@ function checkSnapshots(data, protocol, checkName) {
|
|
|
258
327
|
const name = checkName(value.name);
|
|
259
328
|
if (name == null) {
|
|
260
329
|
if (!holder) throw new GuardError(droppedNote(value.name), "undeclared_tool");
|
|
261
|
-
holder[key] = inPlaceOf(
|
|
330
|
+
holder[key] = inPlaceOf(
|
|
331
|
+
protocol,
|
|
332
|
+
value,
|
|
333
|
+
droppedNote(value.name),
|
|
334
|
+
at,
|
|
335
|
+
placeOf?.(value, at, listed),
|
|
336
|
+
);
|
|
262
337
|
changed = true;
|
|
263
338
|
left++;
|
|
264
339
|
continue;
|
|
@@ -276,10 +351,10 @@ function checkSnapshots(data, protocol, checkName) {
|
|
|
276
351
|
// Text content is not a snapshot container.
|
|
277
352
|
if (typeof value[name] === "string") continue;
|
|
278
353
|
const items = list(value[name]);
|
|
279
|
-
for (let i = 0; i < items.length; i++) stack.push([items[i], items, i, i]);
|
|
354
|
+
for (let i = 0; i < items.length; i++) stack.push([items[i], items, i, i, true]);
|
|
280
355
|
}
|
|
281
356
|
for (const name of objects) {
|
|
282
|
-
if (value[name] != null) stack.push([value[name], value, name, at]);
|
|
357
|
+
if (value[name] != null) stack.push([value[name], value, name, at, false]);
|
|
283
358
|
}
|
|
284
359
|
}
|
|
285
360
|
if (
|
|
@@ -294,6 +369,11 @@ function checkSnapshots(data, protocol, checkName) {
|
|
|
294
369
|
return { changed, kept, left };
|
|
295
370
|
}
|
|
296
371
|
|
|
372
|
+
// The slot a fragmented call or a choice sits in. A frame that names no index
|
|
373
|
+
// is read as the first one, which is what the collapse path reads it as too
|
|
374
|
+
// (stream.mjs's `index`): a fragment carries no other place it could hold, and
|
|
375
|
+
// the two paths over one stream must answer one reply (pinned by the chat test
|
|
376
|
+
// "a fragment that names no index is read the same by both paths").
|
|
297
377
|
function position(value) {
|
|
298
378
|
const result = value ?? 0;
|
|
299
379
|
if (!Number.isSafeInteger(result) || result < 0)
|
|
@@ -403,7 +483,16 @@ function chatBuffer(checkName) {
|
|
|
403
483
|
const delta = choice.delta;
|
|
404
484
|
if (!object(delta)) continue;
|
|
405
485
|
if (typeof delta.content === "string" && delta.content) wrote.add(position(choice.index));
|
|
406
|
-
|
|
486
|
+
// A `tool_calls` that is there but empty is a call of nothing: a
|
|
487
|
+
// gateway re-emitting the shape with a role-only delta sends one, and
|
|
488
|
+
// no call is being waited for. Reading it as a call opened a round
|
|
489
|
+
// with nothing in it — the choice's `ended` mark was dropped with it,
|
|
490
|
+
// a choice the upstream never sent was flushed at [DONE], and a stream
|
|
491
|
+
// that then closed without the marker failed the whole turn "a tool
|
|
492
|
+
// call unfinished" while nothing was pending. It is left where it is,
|
|
493
|
+
// as a field the guard has nothing to say about.
|
|
494
|
+
const fragments = list(delta.tool_calls);
|
|
495
|
+
if (delta.function_call == null && !fragments.length) continue;
|
|
407
496
|
changed = true;
|
|
408
497
|
// A call buffered now opens a round of its own for this choice: a
|
|
409
498
|
// choice that finished as a plain stop because every call of an
|
|
@@ -411,7 +500,7 @@ function chatBuffer(checkName) {
|
|
|
411
500
|
const at = position(choice.index);
|
|
412
501
|
ended.delete(at);
|
|
413
502
|
const state = stateFor(at, data);
|
|
414
|
-
for (const fragment of
|
|
503
|
+
for (const fragment of fragments) {
|
|
415
504
|
if (!object(fragment) || (fragment.type != null && fragment.type !== "function")) {
|
|
416
505
|
throw new GuardError("Unsupported upstream tool delta");
|
|
417
506
|
}
|
|
@@ -494,28 +583,269 @@ function chatBuffer(checkName) {
|
|
|
494
583
|
// as text carrying the note, and its arguments are never sent. A message
|
|
495
584
|
// whose every call was left out stops as a turn's end, not as tool use.
|
|
496
585
|
function snapshotStream(protocol, checkName) {
|
|
497
|
-
|
|
498
|
-
|
|
586
|
+
// Where a call left out was replaced, by its output_index (or a Messages
|
|
587
|
+
// block's index, which stores true: the id it answers is the text's, not an
|
|
588
|
+
// item's). A copy of the call that lands on the same place answers the id
|
|
589
|
+
// recorded there rather than the one the upstream names it by later.
|
|
590
|
+
const left = new Map();
|
|
591
|
+
// The places the items the reply kept hold. A place is a fact about the
|
|
592
|
+
// reply, so an arguments frame that gives one belongs to whoever holds it —
|
|
593
|
+
// the item kept there, or the call left out there — whatever words either
|
|
594
|
+
// was named by. Reading the place first is what makes the answer the same
|
|
595
|
+
// whichever order the two calls arrived in: two sets subtracted from each
|
|
596
|
+
// other answered by arrival order instead (a kept call named X before a
|
|
597
|
+
// dropped one named X let the dropped call's frames through, and the same
|
|
598
|
+
// reply with the two swapped did not).
|
|
599
|
+
const keptPlaces = new Set();
|
|
600
|
+
// The words the items the reply kept were named by, for the one case a place
|
|
601
|
+
// cannot answer: two items stating one index, one of them replaced by a note
|
|
602
|
+
// (an upstream at odds with itself). There the frame's own word is all that
|
|
603
|
+
// says whose it is, and a word a kept item was named by is that item's —
|
|
604
|
+
// reading the place alone would swallow the declared call's frames with the
|
|
605
|
+
// dropped call's.
|
|
606
|
+
const keptWords = new Set();
|
|
607
|
+
// Every word the calls this stream left out are named by: the ids the guard
|
|
608
|
+
// invented for them, and the ids and call_ids the upstream named them by. A
|
|
609
|
+
// frame that gives no place and arrives outside any item is read by its
|
|
610
|
+
// word, and a word a call left out was named by is never forwarded —
|
|
611
|
+
// reaching the client it would describe an item that was replaced by a note.
|
|
612
|
+
const leftIDs = new Set();
|
|
613
|
+
// The calls this stream placed nowhere — its frames carried no output_index
|
|
614
|
+
// and no item id — in the order it opened them. Nothing in those frames says
|
|
615
|
+
// which item is which, so the order is all the guard has: the k-th item a
|
|
616
|
+
// stream opens without a place of its own is the k-th item of a reply that
|
|
617
|
+
// places nothing, which is where the terminal response's copy of it sits.
|
|
618
|
+
// One call is replaced three times — the frame that opens it, the frame that
|
|
619
|
+
// closes it, and that copy — and all three answer the same invented id
|
|
620
|
+
// (inPlaceOf), which is also what its arguments frames are matched by. An
|
|
621
|
+
// entry is settled once the stream has left the call.
|
|
622
|
+
const nowhere = []; // { token, name, settled }
|
|
623
|
+
// What each word the reply has used for a call it left out answers: the id
|
|
624
|
+
// the guard invented for that call, under every word the reply names it by.
|
|
625
|
+
// One call is replaced more than once, and the client keys an item by the id
|
|
626
|
+
// it was handed first — the frames of the measured route carry no id at all
|
|
627
|
+
// until their arguments frames name one, so the id the upstream settled on
|
|
628
|
+
// arrives after the replacement, and answering it then would make one call
|
|
629
|
+
// two items (inPlaceOf).
|
|
630
|
+
const minted = new Map(); // a word the call is named by → the id it answers
|
|
631
|
+
// The item ids the reply has named on an item of its own, and the words of
|
|
632
|
+
// the item the stream is inside. A frame that names one of the latter is that
|
|
633
|
+
// item's — the reply naming the same word elsewhere says nothing about it —
|
|
634
|
+
// while a frame naming one of the former belongs to that other item, which
|
|
635
|
+
// is what keeps an item the guard kept from being read as the one it is
|
|
636
|
+
// inside.
|
|
637
|
+
const seen = new Set();
|
|
638
|
+
const insideWords = new Set();
|
|
639
|
+
let open = -1; // the placeless call the stream is inside, or -1
|
|
640
|
+
// The item the stream is inside, as far as an arguments frame that gives no
|
|
641
|
+
// place of its own is concerned: the id the call left out there answers (and
|
|
642
|
+
// the name it was called), or undefined with `insideKept` for an item the
|
|
643
|
+
// reply kept. `closingID`/`closingName` are the same for the frame that
|
|
644
|
+
// closes an item: a copy of a call can arrive with nothing to name it by,
|
|
645
|
+
// and the call being closed is what it is.
|
|
646
|
+
let insideID;
|
|
647
|
+
let insideName;
|
|
648
|
+
let insideKept = false;
|
|
649
|
+
let closingID;
|
|
650
|
+
let closingName;
|
|
499
651
|
let kept = 0;
|
|
500
652
|
let dropped = 0;
|
|
653
|
+
// What a frame says the reply's items are called. A frame that names none of
|
|
654
|
+
// them — the shape this tracking exists for — records nothing.
|
|
655
|
+
function see(data) {
|
|
656
|
+
const values = [
|
|
657
|
+
data.item,
|
|
658
|
+
...(Array.isArray(data.output) ? data.output : []),
|
|
659
|
+
...(Array.isArray(data.response?.output) ? data.response.output : []),
|
|
660
|
+
];
|
|
661
|
+
for (const value of values) {
|
|
662
|
+
if (object(value) && typeof value.id === "string" && value.id) seen.add(value.id);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// Opens a call the reply placed nowhere — a frame with no output_index — and
|
|
666
|
+
// answers the entry that names it. Its id is what the guard answered for the
|
|
667
|
+
// call, filled in by the caller once it knows: the entry is what a later copy
|
|
668
|
+
// of the same call is recognised by, and that copy can carry nothing at all —
|
|
669
|
+
// no word of its own and no index (measured: the frame that closes the call
|
|
670
|
+
// drops the call_id the frame that opened it carried), so the call's name
|
|
671
|
+
// among the entries is the only thing left to read it by.
|
|
672
|
+
function entering(name, settled = false) {
|
|
673
|
+
const entry = { name, id: undefined, settled };
|
|
674
|
+
nowhere.push(entry);
|
|
675
|
+
open = nowhere.length - 1;
|
|
676
|
+
return entry;
|
|
677
|
+
}
|
|
678
|
+
// The words a frame can name this call by: the item id and the call_id, a
|
|
679
|
+
// non-empty string each (identity), and both name the one call.
|
|
680
|
+
function names(call) {
|
|
681
|
+
return [identity(call.id), identity(call.call_id)].filter((value) => value !== undefined);
|
|
682
|
+
}
|
|
683
|
+
// Whether the call carries a word of its own to be placed by — the test
|
|
684
|
+
// inPlaceOf and ensureCallIDs make, so all three read "" the same way.
|
|
685
|
+
function identified(call) {
|
|
686
|
+
return identity(call.id) !== undefined || identity(call.call_id) !== undefined;
|
|
687
|
+
}
|
|
688
|
+
// Records that this call answers `id`, under every word the reply has for it:
|
|
689
|
+
// the copies of the call that follow answer the same one.
|
|
690
|
+
function mintedFor(call, id) {
|
|
691
|
+
minted.set(id, id);
|
|
692
|
+
for (const word of names(call)) minted.set(word, id);
|
|
693
|
+
return id;
|
|
694
|
+
}
|
|
695
|
+
// The stream's answer for a call it leaves out: the id this call already
|
|
696
|
+
// answers, or undefined when the stream has none and the call keeps its own.
|
|
697
|
+
// The walk hands every call it leaves out through here.
|
|
698
|
+
function placeOf(call, at, listed) {
|
|
699
|
+
if (protocol !== "responses") return undefined;
|
|
700
|
+
// A word the reply has already used for a call it left out names that
|
|
701
|
+
// call: answer the id it was handed.
|
|
702
|
+
const bound = names(call)
|
|
703
|
+
.map((word) => minted.get(word))
|
|
704
|
+
.find((id) => id !== undefined);
|
|
705
|
+
if (bound !== undefined) return bound;
|
|
706
|
+
if (listed) {
|
|
707
|
+
// The copy inside a snapshot or the terminal response's output: it sits
|
|
708
|
+
// where the walk found it, and a call the stream already replaced at
|
|
709
|
+
// that place has answered an id there. A reply that places nothing is
|
|
710
|
+
// what the rest can be read from, so the entry is matched by the place
|
|
711
|
+
// it holds when the names agree, and by its name when they do not — the
|
|
712
|
+
// same call is copied into several snapshots, and every one of them has
|
|
713
|
+
// to answer the same id. Two calls of one name cannot be told apart
|
|
714
|
+
// here, and the last one the stream opened answers for both, and a call
|
|
715
|
+
// that carries a word of its own is not guessed at from a name.
|
|
716
|
+
const held = left.get(at);
|
|
717
|
+
if (typeof held === "string") return held;
|
|
718
|
+
if (identified(call)) return undefined;
|
|
719
|
+
const entry =
|
|
720
|
+
(nowhere[at]?.name === call.name ? nowhere[at] : undefined) ??
|
|
721
|
+
nowhere.findLast((value) => value.name === call.name);
|
|
722
|
+
return entry?.id;
|
|
723
|
+
}
|
|
724
|
+
// A frame's own item. One that gave an index is placed by it: the call the
|
|
725
|
+
// stream already replaced at that place answered an id there, and the
|
|
726
|
+
// dropped_<at> token inPlaceOf mints is what a call the stream never
|
|
727
|
+
// replaced gets.
|
|
728
|
+
if (at !== undefined) {
|
|
729
|
+
const held = left.get(at);
|
|
730
|
+
return typeof held === "string" ? held : undefined;
|
|
731
|
+
}
|
|
732
|
+
const inside = nowhere[open] && !nowhere[open].settled ? nowhere[open] : undefined;
|
|
733
|
+
if (identified(call)) {
|
|
734
|
+
// The frame that closes a call the stream opened with nothing to place it
|
|
735
|
+
// by can be the first of its frames to carry an id (measured). It is the
|
|
736
|
+
// call the stream is inside, and the id already answered is what the
|
|
737
|
+
// client has.
|
|
738
|
+
if (!inside) return undefined;
|
|
739
|
+
inside.settled = true;
|
|
740
|
+
return mintedFor(call, inside.id);
|
|
741
|
+
}
|
|
742
|
+
// A frame with nothing at all to name it by: the call the stream opened
|
|
743
|
+
// last of that name, which is the one it is closing — the id it answered
|
|
744
|
+
// is the one the client was handed, and a later copy of the call has to
|
|
745
|
+
// answer it again rather than mint one of its own.
|
|
746
|
+
const entry = nowhere.findLast((value) => value.name === call.name) ?? inside;
|
|
747
|
+
if (entry) {
|
|
748
|
+
entry.settled = true;
|
|
749
|
+
return mintedFor(call, entry.id);
|
|
750
|
+
}
|
|
751
|
+
// The call the stream is closing, in a frame that carries nothing to name
|
|
752
|
+
// it by: the id the guard answered for it is what this copy answers, so
|
|
753
|
+
// the call's rewrites stay one item. A frame of another call that only
|
|
754
|
+
// looks like it — same name, no words — is read the same way, and that is
|
|
755
|
+
// the closest thing to a word either of them has.
|
|
756
|
+
if (closingID !== undefined && closingName === call.name) return mintedFor(call, closingID);
|
|
757
|
+
// A done frame whose added frame was never seen. It is a call of its own,
|
|
758
|
+
// and the terminal's copy of it still answers this id by name.
|
|
759
|
+
const opened = entering(call.name, true);
|
|
760
|
+
opened.id = `msg_anon_${++synthesized}`;
|
|
761
|
+
return mintedFor(call, opened.id);
|
|
762
|
+
}
|
|
501
763
|
return {
|
|
502
764
|
apply(data, type) {
|
|
503
765
|
if (protocol === "responses") {
|
|
766
|
+
see(data);
|
|
504
767
|
const item = data.item;
|
|
505
|
-
|
|
506
|
-
|
|
768
|
+
const opens = type === "response.output_item.added";
|
|
769
|
+
const closes = type === "response.output_item.done";
|
|
770
|
+
// A frame that opens or closes an item ends the window the last one
|
|
771
|
+
// had: the stream is not inside that item any more, so an arguments
|
|
772
|
+
// frame that gives no place of its own is not that item's to answer
|
|
773
|
+
// for (below). The item this frame opens is entered just after this,
|
|
774
|
+
// in the frame's own branch.
|
|
775
|
+
if (opens || closes) {
|
|
776
|
+
// The item the stream was inside, for the frame that closes it: a
|
|
777
|
+
// copy of a call can arrive with nothing at all to name it by — no
|
|
778
|
+
// word of its own and no index — and the call being closed is the
|
|
779
|
+
// one whose id it answers.
|
|
780
|
+
closingID = closes ? insideID : undefined;
|
|
781
|
+
closingName = closes ? insideName : undefined;
|
|
782
|
+
insideID = undefined;
|
|
783
|
+
insideName = undefined;
|
|
784
|
+
insideKept = false;
|
|
785
|
+
insideWords.clear();
|
|
786
|
+
if (opens && nowhere[open]) nowhere[open].settled = true;
|
|
787
|
+
}
|
|
788
|
+
// The item this frame carries, when it is a call of no tool the agent
|
|
789
|
+
// has: the walk below replaces it — opens or closes it, the copy is
|
|
790
|
+
// the same call — and its words are the ones a note takes the place
|
|
791
|
+
// of. Every other item the frame names is one the reply keeps, and the
|
|
792
|
+
// place it holds is a fact the suppression below reads before any word.
|
|
793
|
+
const leaving =
|
|
794
|
+
(opens || closes) &&
|
|
507
795
|
object(item) &&
|
|
508
796
|
item.type === "function_call" &&
|
|
509
|
-
checkName(item.name) == null
|
|
510
|
-
|
|
797
|
+
checkName(item.name) == null;
|
|
798
|
+
// A copy of a call left out — the frame that closes it, and every copy
|
|
799
|
+
// of it a snapshot or the terminal carries — holds no place of its own:
|
|
800
|
+
// the place the call holds is the one the frame that opened it was
|
|
801
|
+
// answered at, and a reply at odds with itself (one copy numbered and
|
|
802
|
+
// the next not, or two copies numbered differently) reads back as one
|
|
803
|
+
// item only when the copies leave the placing to the stream's own
|
|
804
|
+
// answer. The id they all answer is what the collapse path finds them
|
|
805
|
+
// by. The frame that opens the call keeps its index: that is where the
|
|
806
|
+
// place comes from.
|
|
807
|
+
if (closes && leaving && data.output_index !== undefined) delete data.output_index;
|
|
808
|
+
if ((opens || closes) && object(item) && !leaving) {
|
|
809
|
+
if (data.output_index !== undefined) keptPlaces.add(data.output_index);
|
|
810
|
+
for (const id of names(item)) keptWords.add(id);
|
|
811
|
+
// A function_call the reply kept: an arguments frame that gives no
|
|
812
|
+
// place of its own is this call's while the stream is inside it.
|
|
813
|
+
if (opens && item.type === "function_call") {
|
|
814
|
+
insideKept = true;
|
|
815
|
+
for (const id of names(item)) insideWords.add(id);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (opens && leaving) {
|
|
511
819
|
const note = droppedNote(item.name);
|
|
512
820
|
// The frame's output_index is this call's place in the reply, which
|
|
513
821
|
// is the id its later frames (done, the terminal's output) invent
|
|
514
|
-
// for it again (inPlaceOf).
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
822
|
+
// for it again (inPlaceOf). A frame that gives no index leaves the
|
|
823
|
+
// place to the stream: the entry opened here is what its later
|
|
824
|
+
// frames — and its arguments frames — are matched by, and it is
|
|
825
|
+
// opened whether or not the frame carried a word, because the frame
|
|
826
|
+
// that closes the call can carry none (measured: the call_id the
|
|
827
|
+
// added frame named is gone from the done frame).
|
|
828
|
+
const entry = data.output_index === undefined ? entering(item.name) : undefined;
|
|
829
|
+
const message = inPlaceOf(protocol, item, note, data.output_index, entry?.id);
|
|
830
|
+
if (entry) entry.id = message.id;
|
|
831
|
+
// Every word this call can be named by later is recorded, whichever
|
|
832
|
+
// shape its frames have: the id the guard answered for it (with the
|
|
833
|
+
// place in it), and the id and call_id the upstream gave it — the
|
|
834
|
+
// arguments frames of a call that gave an index name it by one of
|
|
835
|
+
// those alone, so a set of invented ids would let them through,
|
|
836
|
+
// describing an item the client is told was replaced by a note. The
|
|
837
|
+
// place it answered at is recorded too, so the copies of it that
|
|
838
|
+
// follow answer the same id rather than the one the upstream settles
|
|
839
|
+
// on after them.
|
|
840
|
+
if (data.output_index !== undefined) left.set(data.output_index, message.id);
|
|
841
|
+
for (const id of [message.id, ...names(item)]) leftIDs.add(id);
|
|
842
|
+
mintedFor(item, message.id);
|
|
843
|
+
// And the stream is inside it: its arguments frames carry no place
|
|
844
|
+
// of their own, and this window is what says they are its frames
|
|
845
|
+
// rather than an item the reply never opened.
|
|
846
|
+
insideID = message.id;
|
|
847
|
+
insideName = item.name;
|
|
848
|
+
for (const id of [message.id, ...names(item)]) insideWords.add(id);
|
|
519
849
|
dropped++;
|
|
520
850
|
const where = { item_id: message.id, output_index: data.output_index, content_index: 0 };
|
|
521
851
|
const part = { type: "output_text", text: "", annotations: [] };
|
|
@@ -530,12 +860,43 @@ function snapshotStream(protocol, checkName) {
|
|
|
530
860
|
],
|
|
531
861
|
};
|
|
532
862
|
}
|
|
533
|
-
if (
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
863
|
+
if (typeof type === "string" && type.startsWith("response.function_call_arguments.")) {
|
|
864
|
+
// Whose frame this is, by what the frame itself says: the place it
|
|
865
|
+
// gives is a fact about the reply and is read first, whoever holds
|
|
866
|
+
// it; a frame that gives none belongs to the item the stream is
|
|
867
|
+
// inside; and one that arrives outside any item is read by its word.
|
|
868
|
+
// The answer is the same whichever order the calls arrived in,
|
|
869
|
+
// because no step of it subtracts one set of words from another.
|
|
870
|
+
const place = data.output_index;
|
|
871
|
+
const word = identity(data.item_id);
|
|
872
|
+
let holder = "kept";
|
|
873
|
+
if (place != null && left.has(place) && !keptPlaces.has(place)) holder = "dropped";
|
|
874
|
+
// Two items stated one index: the place cannot say whose the frame
|
|
875
|
+
// is, and the word it names is what is left.
|
|
876
|
+
else if (place != null && word !== undefined && keptWords.has(word)) holder = "kept";
|
|
877
|
+
else if (place != null && left.has(place)) holder = "dropped";
|
|
878
|
+
else if (place != null && keptPlaces.has(place)) holder = "kept";
|
|
879
|
+
else if (place == null && insideID !== undefined)
|
|
880
|
+
// A word the reply named on some other item is that item's frame,
|
|
881
|
+
// interleaved with this call's own: the call the stream is inside
|
|
882
|
+
// answers for the words it was named by, and for a word the reply
|
|
883
|
+
// names nowhere else (measured: the added frame carries no id, and
|
|
884
|
+
// the frames after it name one).
|
|
885
|
+
holder =
|
|
886
|
+
word !== undefined && seen.has(word) && !insideWords.has(word) ? "kept" : "dropped";
|
|
887
|
+
else if (place == null && insideKept) holder = "kept";
|
|
888
|
+
else if (word !== undefined && leftIDs.has(word)) holder = "dropped";
|
|
889
|
+
if (holder === "dropped") {
|
|
890
|
+
if (word !== undefined) {
|
|
891
|
+
leftIDs.add(word);
|
|
892
|
+
// The id names the call the stream is inside, so a copy of that
|
|
893
|
+
// call which later carries it answers the id the client was
|
|
894
|
+
// handed (placeOf).
|
|
895
|
+
const answers = (place != null ? left.get(place) : undefined) ?? insideID;
|
|
896
|
+
if (answers !== undefined) minted.set(word, answers);
|
|
897
|
+
}
|
|
898
|
+
return { changed: true, data: null };
|
|
899
|
+
}
|
|
539
900
|
}
|
|
540
901
|
} else {
|
|
541
902
|
if (type === "message_start") {
|
|
@@ -577,7 +938,7 @@ function snapshotStream(protocol, checkName) {
|
|
|
577
938
|
return { changed: true, data };
|
|
578
939
|
}
|
|
579
940
|
}
|
|
580
|
-
const checked = checkSnapshots(data, protocol, checkName);
|
|
941
|
+
const checked = checkSnapshots(data, protocol, checkName, placeOf);
|
|
581
942
|
kept += checked.kept;
|
|
582
943
|
dropped += checked.left;
|
|
583
944
|
return { changed: checked.changed, data };
|
|
@@ -593,7 +954,10 @@ async function readBounded(response) {
|
|
|
593
954
|
if (!reader) return "";
|
|
594
955
|
const parts = [];
|
|
595
956
|
let bytes = 0;
|
|
596
|
-
|
|
957
|
+
// Named for the read it ends, not `failure`: that word is this module's
|
|
958
|
+
// error constructor, and a local of the same name reads as if the guard's
|
|
959
|
+
// own failures were being built here.
|
|
960
|
+
let readFailure;
|
|
597
961
|
try {
|
|
598
962
|
while (true) {
|
|
599
963
|
const { done, value } = await reader.read();
|
|
@@ -604,7 +968,7 @@ async function readBounded(response) {
|
|
|
604
968
|
parts.push(value);
|
|
605
969
|
}
|
|
606
970
|
} catch (error) {
|
|
607
|
-
|
|
971
|
+
readFailure = error;
|
|
608
972
|
}
|
|
609
973
|
// Whatever ended the read — the limit, a malformed body, or the end of it —
|
|
610
974
|
// this body is never read again, so the stream is cancelled and the reader
|
|
@@ -619,12 +983,12 @@ async function readBounded(response) {
|
|
|
619
983
|
try {
|
|
620
984
|
await reader.cancel();
|
|
621
985
|
} catch (error) {
|
|
622
|
-
if (!
|
|
623
|
-
if (
|
|
986
|
+
if (!readFailure) throw error;
|
|
987
|
+
if (readFailure instanceof Error && !("cause" in readFailure)) readFailure.cause = error;
|
|
624
988
|
} finally {
|
|
625
989
|
reader.releaseLock();
|
|
626
990
|
}
|
|
627
|
-
if (
|
|
991
|
+
if (readFailure) throw readFailure;
|
|
628
992
|
const all = new Uint8Array(bytes);
|
|
629
993
|
let offset = 0;
|
|
630
994
|
for (const part of parts) {
|
|
@@ -960,6 +1324,18 @@ function guardedStream(response, protocol, checkName, endCutShort) {
|
|
|
960
1324
|
controller.close();
|
|
961
1325
|
return;
|
|
962
1326
|
}
|
|
1327
|
+
// An event after the terminal marker is not part of the reply: the
|
|
1328
|
+
// reply ended where the marker said. Reading it as more of the
|
|
1329
|
+
// same was this path's behaviour alone — the collapse path read
|
|
1330
|
+
// the same bytes and dropped it (stream.mjs's `if (completed)
|
|
1331
|
+
// return`) — so the same upstream answered two different replies.
|
|
1332
|
+
// It is ignored: not applied to the buffer, not forwarded. This
|
|
1333
|
+
// sits below the error branch above on purpose: an upstream that
|
|
1334
|
+
// reports an error after it ended the reply still failed the turn,
|
|
1335
|
+
// and that is a failure, not something to ignore. A frame with no
|
|
1336
|
+
// data at all is the SSE's own framing rather than an event, and
|
|
1337
|
+
// it keeps flowing from above this point.
|
|
1338
|
+
if (completed) continue;
|
|
963
1339
|
const checked = chat ? chat.apply(data) : snapshots.apply(data, type);
|
|
964
1340
|
const { changed, data: result, extra = [] } = checked;
|
|
965
1341
|
if (result) track?.see(result);
|
|
@@ -970,7 +1346,12 @@ function guardedStream(response, protocol, checkName, endCutShort) {
|
|
|
970
1346
|
)
|
|
971
1347
|
completed = true;
|
|
972
1348
|
if (result || extra.length) {
|
|
973
|
-
|
|
1349
|
+
// A frame the walk left as it came is forwarded as it came: only
|
|
1350
|
+
// its own bytes carry what a rewrite would drop — the event name
|
|
1351
|
+
// it was sent under, and any field of the frame the guard does
|
|
1352
|
+
// not read.
|
|
1353
|
+
let own = "";
|
|
1354
|
+
if (result) own = changed ? rewrite(frame, result) : frame.raw;
|
|
974
1355
|
const added = extra.map((value) => encodeEvent(value, value.type)).join("");
|
|
975
1356
|
controller.enqueue(encoder.encode(own + added));
|
|
976
1357
|
return;
|
|
@@ -1027,11 +1408,14 @@ function guardedStream(response, protocol, checkName, endCutShort) {
|
|
|
1027
1408
|
export async function guardToolResponse(response, protocol, policy, options = {}) {
|
|
1028
1409
|
// A protocol this guard has no rules for is the caller's mistake, and it
|
|
1029
1410
|
// is one whichever the upstream answered: checked before the status is
|
|
1030
|
-
// read, so an error response cannot return it unexamined.
|
|
1411
|
+
// read, so an error response cannot return it unexamined. The policy is
|
|
1412
|
+
// read here for the same reason: a policy this guard cannot read refuses
|
|
1413
|
+
// every call of every reply, and that must be said rather than done
|
|
1414
|
+
// quietly (declaredNames), whichever the upstream answered.
|
|
1031
1415
|
if (!["chat", "responses", "anthropic"].includes(protocol))
|
|
1032
1416
|
throw new TypeError(`Unsupported protocol: ${protocol}`);
|
|
1033
|
-
if (!response.ok) return response;
|
|
1034
1417
|
const checkName = nameGuard(policy);
|
|
1418
|
+
if (!response.ok) return response;
|
|
1035
1419
|
const headers = headersFor(response);
|
|
1036
1420
|
const contentType = response.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
|
|
1037
1421
|
if (contentType === "text/event-stream") {
|