opencode-zen-freex 0.2.16 → 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/stream.mjs CHANGED
@@ -94,6 +94,13 @@ function carries(snapshot, accumulated, fields) {
94
94
  return weight(snapshot, fields) > 0 || weight(accumulated, fields) === 0;
95
95
  }
96
96
 
97
+ // Whether a value can be read as a snapshot of something the stream
98
+ // accumulated. A snapshot is the upstream's own view of an item, a block or a
99
+ // part; a null, a string or a list is not one, and stands in for nothing.
100
+ function isSnapshotObject(value) {
101
+ return typeof value === "object" && value !== null && !Array.isArray(value);
102
+ }
103
+
97
104
  function chatAccumulator() {
98
105
  const body = { object: "chat.completion", choices: [] };
99
106
  const choices = new Map();
@@ -184,67 +191,178 @@ function chatAccumulator() {
184
191
  function responsesAccumulator() {
185
192
  let body = { object: "response", output: [] };
186
193
  let final;
187
- const output = new Map();
194
+ // The items a reply holds, in the order the stream opened them, keyed by the
195
+ // order itself. The key is not the item's output_index: an index the
196
+ // upstream states and an index this file has to infer are two different
197
+ // things, and while they shared one key space a frame that stated
198
+ // `output_index: 0` landed on the item an earlier frame had inferred into
199
+ // slot 0 — `keep` read the second item as a newer view of the first and the
200
+ // reply answered 200 with one item of the two, wearing the first one's id.
201
+ // One key space per fact: `output` follows the arrival order, `indexMap`
202
+ // holds only the indices the upstream itself stated, and a frame that states
203
+ // an index no item holds opens a new item rather than displacing one.
204
+ const output = new Map(); // arrival order -> { value, id, index }
205
+ let next = 0;
206
+ const indexMap = new Map(); // stated output_index -> arrival order
207
+ // The id a streamed item was named by, once a frame names it: an item opened
208
+ // by a frame that carries no id of its own is named by the frames after it.
209
+ const idMap = new Map(); // item id -> arrival order
188
210
  // The fields a streamed item fills in: a message its content, a call its
189
211
  // arguments, a reasoning item its summary.
190
212
  const fields = ["content", "arguments", "summary", "text"];
191
- // The slot a frame belongs to: the output_index it carries, or the slot
192
- // of the item it names when the upstream left output_index out. A frame
193
- // naming an item the stream never opened has no slot at all — slot 0 is
194
- // only for a frame that names nothing, which a stream that never indexed
195
- // its items means as the first one. Merging an unknown item into slot 0
196
- // was measured handing a function_call there the text of a message. An
197
- // `added` frame opens an item, so the id it names is new there by
198
- // definition and takes the next free slot; a frame that has to find an
199
- // item that is already open still throws — unless the reply holds exactly
200
- // one item of the frame's own kind with no id of its own, which is the item
201
- // the frame is naming: measured on this route, a call's output_item.added
202
- // can arrive carrying no id at all and every frame after it names the item
203
- // by an id of its own, so the id is adopted onto the item the stream opened
204
- // for that call, and one call stays one item. Two candidates, or none of
205
- // the frame's kind, is still not something to guess at, and still throws.
206
- function slot(data, id, fresh = false, kind) {
207
- if (data.output_index !== undefined) return index(data.output_index);
208
- if (id === undefined || id === null || id === "") return index(undefined);
209
- const position = [...output].find(([, value]) => value.id === id)?.[0];
210
- if (position !== undefined) return index(position);
211
- if (!fresh) {
212
- const candidates = [...output].filter(([, value]) => !value.id && value.type === kind);
213
- if (candidates.length === 1) {
214
- const [position, value] = candidates[0];
215
- value.id = id;
216
- return index(position);
213
+ // The newest item this stream opened without an id, by kind: a reply whose
214
+ // frames name nothing at all is one the upstream never indexed, and every
215
+ // frame after its item — the ones that say `added` and the ones that only
216
+ // fill a field in — belongs to that item. Remembering where it is keeps a
217
+ // frame with no id from opening a second item of its own kind (`item()`
218
+ // defaults to "message", so a text delta of such a reply would), while a
219
+ // frame of another kind is still not allowed to continue the wrong item —
220
+ // measured: a response's text landing on its function call. An item that
221
+ // adopts an id later still has the frames after the id that name nothing at
222
+ // all, so the slot stays here after it is named.
223
+ const openNone = new Map();
224
+ // What a streamed item is named by, when it is named at all: a non-empty
225
+ // string. `""` names nothing a client can key by, and neither does a number
226
+ // or a boolean — an item whose id is `0` or `false` has no identity here,
227
+ // however loudly the upstream says it. The same rule, in the same words, as
228
+ // `identity()` in tool-response.mjs: one upstream value must not be a name to
229
+ // one file and no name to the other, or two items of one reply share an item
230
+ // (measured: two calls with `"id": 0` and their own output_indexes — 0 and 1 —
231
+ // resolved to the first one's item, and the second call's arguments were
232
+ // written over the first's).
233
+ function identity(value) {
234
+ return typeof value === "string" && value !== "" ? value : undefined;
235
+ }
236
+ const at = (id) => idMap.get(id);
237
+ // Whether a record is still nameless: it was opened by a frame that said
238
+ // nothing about what it is, which is the one shape an id-less frame or a
239
+ // stated index may still claim as its own.
240
+ const nameless = (record) => record.id === undefined && record.index === undefined;
241
+ // A record's item and the item's identity are one fact told twice: the
242
+ // frames that follow a name an item took must answer by it, and the finished
243
+ // item must carry it. Written once, so the two cannot drift.
244
+ function named(slot, id) {
245
+ const record = output.get(slot);
246
+ record.id = id;
247
+ if (record.value !== undefined) record.value.id = id;
248
+ // Which item a frame that names the word alone reaches, when the reply has
249
+ // spent the word on two items: the one that took it last — the item the
250
+ // stream is working on, which is the reading this file already gives a
251
+ // frame that names nothing at all (openNone, above). The item it pointed
252
+ // at before keeps the id it answered with; only the routing moves. Measured
253
+ // on two calls sharing one id, each with its own output_index and one of
254
+ // them naming the id alone in its arguments frames: the older item held the
255
+ // word and every argument of the newer landed on it, so the reply came back
256
+ // with the newer call's arguments on both items.
257
+ idMap.set(id, slot);
258
+ }
259
+ // The record a frame belongs to. What it states comes first: the
260
+ // output_index it carries, then the item id it names — an item is the one
261
+ // its own frames say it is. What it leaves to this file comes second: a
262
+ // frame naming nothing continues the item the stream opened namelessly,
263
+ // which is what a reply that never indexed its items means. Anything else
264
+ // is a frame over an item this stream never opened, which is still not
265
+ // something to guess at.
266
+ function locate(data, id, opening = false, kind) {
267
+ const name = identity(id);
268
+ if (data.output_index !== undefined) {
269
+ const held = indexMap.get(index(data.output_index));
270
+ if (held !== undefined) return held;
271
+ } else if (name !== undefined) {
272
+ const held = at(name);
273
+ if (held !== undefined) return held;
274
+ const candidates = [...output].filter(
275
+ ([, record]) => record.id === undefined && record.value?.type === kind,
276
+ );
277
+ const open = openNone.get(kind);
278
+ if (open !== undefined && output.get(open).id === undefined) {
279
+ // The item the stream opened namelessly is the one an id-less reply
280
+ // names from the frames after it, so the id is adopted and one call
281
+ // stays one item (measured on this route: a call's output_item.added
282
+ // arrives with no id at all). An item that already has an id of its
283
+ // own is not it, and one named item is not a candidate at all.
284
+ named(open, name);
285
+ return open;
286
+ }
287
+ if (!opening) {
288
+ // Exactly one nameless item of this kind is the item the frame names,
289
+ // so the id is adopted; two of them, or none, is a guess.
290
+ if (candidates.length === 1) {
291
+ const [held] = candidates[0];
292
+ named(held, name);
293
+ return held;
294
+ }
295
+ throw new Error("Upstream frame names an unknown item without an output index");
217
296
  }
297
+ } else if (!opening) {
298
+ const open = openNone.get(kind);
299
+ if (open !== undefined) return open;
300
+ const candidates = [...output].filter(
301
+ ([, record]) => nameless(record) && record.value?.type === kind,
302
+ );
303
+ if (candidates.length === 1) return candidates[0][0];
218
304
  throw new Error("Upstream frame names an unknown item without an output index");
219
305
  }
220
- let next = 0;
221
- for (const held of output.keys()) next = Math.max(next, held + 1);
222
- return index(next);
306
+ // A stated index this stream has not seen opens the item it names; the
307
+ // item a stream opens namelessly takes its place in the arrival order,
308
+ // which no stated index can collide with.
309
+ const slot = next++;
310
+ const record = { value: undefined, id: undefined, index: undefined };
311
+ output.set(slot, record);
312
+ if (data.output_index !== undefined) {
313
+ record.index = index(data.output_index);
314
+ indexMap.set(record.index, slot);
315
+ }
316
+ if (name !== undefined) {
317
+ // An `added` frame opens an item, and the id it carries is that item's
318
+ // own even when the reply has spent the word on an item already. A
319
+ // stated index says this frame belongs to another item, and a stated
320
+ // index is a fact about the reply; an id another item happens to hold is
321
+ // not a reason to refuse the frame. Refusing it lost the whole reply —
322
+ // two calls, one name, and a client that gets neither — where the
323
+ // upstream's own contradiction costs the reply only its second name.
324
+ named(slot, name);
325
+ }
326
+ if (nameless(record) && name === undefined) openNone.set(kind, slot);
327
+ return slot;
223
328
  }
224
329
  // A snapshot replaces the item in its slot only when it is a whole view of
225
330
  // it; a partial one leaves what the deltas delivered standing. A snapshot
226
331
  // that carries no id does not take away the one the stream named the item by
227
- // (slot, above): the item's own snapshot arrives without it in the shape
332
+ // (locate, above): the item's own snapshot arrives without it in the shape
228
333
  // that names an item from the frames after it, and taking the snapshot as it
229
334
  // stands lost the id again. One that carries an id still wins, as it did.
230
- function keep(position, value) {
231
- if (carries(value, output.get(position), fields)) {
232
- const held = output.get(position);
233
- if (held?.id && !value.id) value.id = held.id;
234
- output.set(position, value);
235
- }
335
+ function keep(slot, value) {
336
+ const record = output.get(slot);
337
+ if (!carries(value, record.value, fields)) return;
338
+ // The id is inherited by the same item's partial view, never by another
339
+ // item. The slot is the item's own — locate resolves a stated index or id
340
+ // to the item that stated it, and opens a new item for one no item holds —
341
+ // so the snapshot in it names the same item, and a snapshot that brings an
342
+ // id of its own is that item named at last.
343
+ const stated = identity(value.id);
344
+ if (stated === undefined && record.id !== undefined) value.id = record.id;
345
+ record.value = value;
346
+ if (stated !== undefined) named(slot, stated);
347
+ if (record.index !== undefined) indexMap.set(record.index, slot);
236
348
  }
237
349
  function item(data, type = "message") {
238
- const position = slot(data, data.item_id, false, type);
239
- if (!output.has(position)) {
350
+ // The name is read the one way this file reads one (identity, above), so a
351
+ // frame carrying `0` or `false` opens the same item a frame carrying no id
352
+ // at all does, rather than one that is half-named.
353
+ const slot = locate(data, identity(data.item_id), false, type);
354
+ const record = output.get(slot);
355
+ if (record.value === undefined) {
240
356
  const value = { type };
241
- if (data.item_id) value.id = data.item_id;
357
+ // locate has already recorded the name on the record when the frame
358
+ // carried one, so the item takes the name it was opened with.
359
+ if (record.id !== undefined) value.id = record.id;
242
360
  if (type === "message") Object.assign(value, { role: "assistant", content: [] });
243
361
  if (type === "function_call") value.arguments = "";
244
362
  if (type === "reasoning") value.summary = [];
245
- output.set(position, value);
363
+ record.value = value;
246
364
  }
247
- return output.get(position);
365
+ return record.value;
248
366
  }
249
367
  return {
250
368
  apply(data, type) {
@@ -254,13 +372,18 @@ function responsesAccumulator() {
254
372
  // A snapshot that carries an item where the reply already has one
255
373
  // adds only what it actually carries: the accumulated items stand
256
374
  // against an output list it does not carry, or carries empty.
257
- for (const [position, value] of (data.response.output ?? []).entries())
258
- keep(position, value);
375
+ for (const value of data.response.output ?? [])
376
+ keep(locate({}, identity(value?.id), true, value?.type), value);
259
377
  }
260
378
  } else if (type === "response.output_item.added" || type === "response.output_item.done") {
261
379
  if (data.item)
262
380
  keep(
263
- slot(data, data.item.id, type === "response.output_item.added", data.item?.type),
381
+ locate(
382
+ data,
383
+ identity(data.item.id),
384
+ type === "response.output_item.added",
385
+ data.item?.type,
386
+ ),
264
387
  data.item,
265
388
  );
266
389
  } else if (type === "response.content_part.added" || type === "response.content_part.done") {
@@ -332,11 +455,40 @@ function responsesAccumulator() {
332
455
  if (data.usage) body.usage = { ...body.usage, ...data.usage };
333
456
  },
334
457
  finish() {
458
+ // The reply holds the items that reached a value — a frame described one
459
+ // and a snapshot may not have arrived yet. Their order is the order the
460
+ // upstream placed them in: the output_index it stated for an item is
461
+ // where that item sits in the reply, and an item it stated none for takes
462
+ // the first place no stated index claims, in the order the stream opened
463
+ // it. That is the ordering this file's slots used to give by construction
464
+ // — an indexed item sat at its index, an unplaced one at the next free
465
+ // number — and the client that reads no stream has only the array's order
466
+ // to read the reply by: answering the items in arrival order instead
467
+ // answered them 2 then 1 for a reply that said 1 then 2. A stated index
468
+ // is a fact and an inferred place is a guess, so where the two meet the
469
+ // stated index keeps its place and the unplaced item takes the next one.
470
+ const items = () => {
471
+ const stated = new Set();
472
+ for (const record of output.values())
473
+ if (record.index !== undefined) stated.add(record.index);
474
+ const placed = [];
475
+ let free = 0;
476
+ for (const record of output.values()) {
477
+ if (record.value === undefined) continue;
478
+ if (record.index !== undefined) {
479
+ placed.push([record.index, record.value]);
480
+ continue;
481
+ }
482
+ while (stated.has(free)) free++;
483
+ placed.push([free++, record.value]);
484
+ }
485
+ return placed.sort(([a], [b]) => a - b).map(([, value]) => value);
486
+ };
335
487
  if (!final) {
336
- body.output = sorted(output);
488
+ body.output = items();
337
489
  return body;
338
490
  }
339
- const kept = sorted(output);
491
+ const kept = items();
340
492
  // A terminal object that carries no output of its own — absent, or
341
493
  // empty while the stream accumulated some — is not a whole view of the
342
494
  // reply: returning it verbatim discards every delta already delivered,
@@ -363,6 +515,19 @@ function responsesAccumulator() {
363
515
  };
364
516
  }
365
517
 
518
+ // The block field each Anthropic text delta fills in. A Map rather than an
519
+ // object literal, and one of them for the whole module rather than one per
520
+ // delta: read off an object, a `delta.type` of "__proto__", "toString" or
521
+ // "constructor" reached the member the prototype chain holds there and the
522
+ // delta wrote that key onto its block — a key the delta never named, holding a
523
+ // function that JSON.stringify then dropped. A Map holds no chain: those three
524
+ // names are the misses they are.
525
+ const ANTHROPIC_TEXT_FIELDS = new Map([
526
+ ["text_delta", "text"],
527
+ ["thinking_delta", "thinking"],
528
+ ["signature_delta", "signature"],
529
+ ]);
530
+
366
531
  function anthropicAccumulator() {
367
532
  let body = {
368
533
  type: "message",
@@ -380,12 +545,21 @@ function anthropicAccumulator() {
380
545
  apply(data, type) {
381
546
  if (type === "message_start") {
382
547
  body = { ...body, ...data.message };
548
+ // `content` is the one key of a message_start that `apply` fills in
549
+ // itself, and a value that is not a list of blocks — a string, a
550
+ // number, an object — is not a view of the reply to take it from: the
551
+ // spread above would set it on the body and answer with it, ahead of
552
+ // the blocks this stream accumulates. A null or a missing one is the
553
+ // shape every frame of this accumulator leaves alone already.
554
+ const stated = Array.isArray(data.message?.content) ? data.message.content : [];
555
+ body.content = stated;
383
556
  // A message_start that repeats beside the deltas it opened, or one
384
557
  // whose block has not been filled in yet, is a partial view of the
385
558
  // reply: a block that carries nothing where the accumulated one has
386
559
  // content leaves it standing.
387
- for (const [position, block] of (data.message?.content ?? []).entries())
388
- if (carries(block, blocks.get(position), fields)) blocks.set(position, block);
560
+ for (const [position, block] of stated.entries())
561
+ if (isSnapshotObject(block) && carries(block, blocks.get(position), fields))
562
+ blocks.set(position, block);
389
563
  } else if (type === "content_block_start") {
390
564
  // The same test message_start's blocks get, for the same reason: a
391
565
  // gateway re-emitting a block's opening frame carries a block that
@@ -393,19 +567,24 @@ function anthropicAccumulator() {
393
567
  // it would discard the text, thinking or signature the deltas
394
568
  // already delivered. A start with nothing accumulated applies, as
395
569
  // does one that carries content of its own.
570
+ //
571
+ // A frame that carries no block at all, or a null or a flat value
572
+ // where one belongs, is the same partial view: `carries` weighs it as
573
+ // nothing, so it was stored as it stood and the reply answered 200
574
+ // with a null among its content, or threw on the first delta that
575
+ // read a field of it. Nothing to take hold of is not a reason to fail
576
+ // a stream whose other frames are sound — it opens nothing, and the
577
+ // deltas that follow it are refused exactly like a delta with no
578
+ // block above them, which is the answer this file gives that shape.
396
579
  const position = index(data.index);
397
- if (carries(data.content_block, blocks.get(position), fields))
580
+ if (isSnapshotObject(data.content_block) && carries(data.content_block, blocks.get(position), fields))
398
581
  blocks.set(position, data.content_block);
399
582
  } else if (type === "content_block_delta") {
400
583
  const position = index(data.index);
401
584
  const block = blocks.get(position);
402
585
  if (!block) throw new Error("Content delta without a content block");
403
586
  const delta = data.delta ?? {};
404
- const field = {
405
- text_delta: "text",
406
- thinking_delta: "thinking",
407
- signature_delta: "signature",
408
- }[delta.type];
587
+ const field = ANTHROPIC_TEXT_FIELDS.get(delta.type);
409
588
  if (field) applyText(block, field, delta[field], true);
410
589
  if (delta.type === "input_json_delta") {
411
590
  argumentsJSON.set(