@shogo-ai/core 1.9.13 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-H625AGFN.js +466 -0
- package/dist/chunk-MDRCI2CN.js +197 -0
- package/dist/chunk-NTAY4E43.js +72 -0
- package/dist/{chunk-24AL3V6R.js → chunk-VW6ZGCBI.js} +35 -1
- package/dist/{chunk-IDGVRGAL.js → chunk-WRHTXHTH.js} +81 -11
- package/dist/file-types.cjs +79 -5
- package/dist/file-types.d.cts +23 -4
- package/dist/file-types.d.ts +23 -4
- package/dist/file-types.js +7 -1
- package/dist/index.cjs +464 -17
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +35 -7
- package/dist/instrumentation.cjs +95 -10
- package/dist/instrumentation.js +2 -1
- package/dist/logger.cjs +39 -2
- package/dist/logger.d.cts +71 -1
- package/dist/logger.d.ts +71 -1
- package/dist/logger.js +9 -3
- package/dist/stream-buffer.cjs +242 -5
- package/dist/stream-buffer.d.cts +97 -1
- package/dist/stream-buffer.d.ts +97 -1
- package/dist/stream-buffer.js +13 -3
- package/dist/tech-stack-registry.cjs +36 -0
- package/dist/tech-stack-registry.d.cts +48 -1
- package/dist/tech-stack-registry.d.ts +48 -1
- package/dist/tech-stack-registry.js +7 -1
- package/package.json +16 -1
- package/dist/chunk-ELMG6OXT.js +0 -236
- package/dist/chunk-HKX3MEJN.js +0 -126
- package/dist/chunk-KTDDFWXM.js +0 -39
package/dist/stream-buffer.cjs
CHANGED
|
@@ -21,15 +21,135 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var stream_buffer_exports = {};
|
|
22
22
|
__export(stream_buffer_exports, {
|
|
23
23
|
StreamBufferStore: () => StreamBufferStore,
|
|
24
|
-
|
|
24
|
+
TURN_COMPLETE_EVENT_TYPE: () => TURN_COMPLETE_EVENT_TYPE,
|
|
25
|
+
TURN_SEQ_EVENT_TYPE: () => TURN_SEQ_EVENT_TYPE,
|
|
26
|
+
compactSseFrames: () => compactSseFrames,
|
|
27
|
+
createBufferingTransform: () => createBufferingTransform,
|
|
28
|
+
encodeTurnCompleteFrame: () => encodeTurnCompleteFrame,
|
|
29
|
+
encodeTurnSeqFrame: () => encodeTurnSeqFrame
|
|
25
30
|
});
|
|
26
31
|
module.exports = __toCommonJS(stream_buffer_exports);
|
|
32
|
+
|
|
33
|
+
// src/stream-compaction.ts
|
|
34
|
+
var TURN_SEQ_EVENT_TYPE = "data-turn-seq";
|
|
35
|
+
var DELTA_PART = {
|
|
36
|
+
"text-delta": "text",
|
|
37
|
+
"reasoning-delta": "reasoning"
|
|
38
|
+
};
|
|
39
|
+
var PART_BOUNDARY = {
|
|
40
|
+
"text-end": "text",
|
|
41
|
+
"reasoning-end": "reasoning",
|
|
42
|
+
"text-start": "text",
|
|
43
|
+
"reasoning-start": "reasoning"
|
|
44
|
+
};
|
|
45
|
+
function compactSseFrames(frames) {
|
|
46
|
+
if (frames.length === 0) return new Uint8Array(0);
|
|
47
|
+
const decoder = new TextDecoder();
|
|
48
|
+
let text = "";
|
|
49
|
+
for (const frame of frames) text += decoder.decode(frame, { stream: true });
|
|
50
|
+
text += decoder.decode();
|
|
51
|
+
const events = text.split("\n\n");
|
|
52
|
+
const tail = events.pop() ?? "";
|
|
53
|
+
const out = [];
|
|
54
|
+
const openParts = /* @__PURE__ */ new Map();
|
|
55
|
+
const openToolInputs = /* @__PURE__ */ new Map();
|
|
56
|
+
const preliminaryOutputs = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const event of events) {
|
|
58
|
+
const chunk = parseEvent(event);
|
|
59
|
+
if (!chunk) {
|
|
60
|
+
out.push({ raw: event });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const type = chunk.type;
|
|
64
|
+
if (type === TURN_SEQ_EVENT_TYPE) continue;
|
|
65
|
+
if (type in PART_BOUNDARY) {
|
|
66
|
+
openParts.delete(`${PART_BOUNDARY[type]}:${chunk.id}`);
|
|
67
|
+
out.push({ chunk });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (type in DELTA_PART && typeof chunk.delta === "string") {
|
|
71
|
+
const key = `${DELTA_PART[type]}:${chunk.id}`;
|
|
72
|
+
const merged = openParts.get(key);
|
|
73
|
+
if (merged) {
|
|
74
|
+
merged.delta += chunk.delta;
|
|
75
|
+
if (chunk.providerMetadata !== void 0) merged.providerMetadata = chunk.providerMetadata;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const copy = { ...chunk };
|
|
79
|
+
openParts.set(key, copy);
|
|
80
|
+
out.push({ chunk: copy });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (type === "tool-input-delta" && typeof chunk.inputTextDelta === "string") {
|
|
84
|
+
const index = openToolInputs.get(chunk.toolCallId);
|
|
85
|
+
const existing = index !== void 0 ? out[index] : null;
|
|
86
|
+
if (existing && "chunk" in existing) {
|
|
87
|
+
existing.chunk.inputTextDelta += chunk.inputTextDelta;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
openToolInputs.set(chunk.toolCallId, out.length);
|
|
91
|
+
out.push({ chunk: { ...chunk } });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (type === "tool-input-available" || type === "tool-input-error") {
|
|
95
|
+
const index = openToolInputs.get(chunk.toolCallId);
|
|
96
|
+
if (index !== void 0) out[index] = null;
|
|
97
|
+
openToolInputs.delete(chunk.toolCallId);
|
|
98
|
+
} else if (type === "tool-input-start") {
|
|
99
|
+
openToolInputs.delete(chunk.toolCallId);
|
|
100
|
+
} else if (type === "tool-output-available" || type === "tool-output-error") {
|
|
101
|
+
const superseded = preliminaryOutputs.get(chunk.toolCallId);
|
|
102
|
+
if (superseded !== void 0) {
|
|
103
|
+
out[superseded] = null;
|
|
104
|
+
preliminaryOutputs.delete(chunk.toolCallId);
|
|
105
|
+
}
|
|
106
|
+
if (type === "tool-output-available" && chunk.preliminary === true) {
|
|
107
|
+
preliminaryOutputs.set(chunk.toolCallId, out.length);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
out.push({ chunk });
|
|
111
|
+
}
|
|
112
|
+
let result = "";
|
|
113
|
+
for (const entry of out) {
|
|
114
|
+
if (!entry) continue;
|
|
115
|
+
result += "chunk" in entry ? encodeEvent(entry.chunk) : `${entry.raw}
|
|
116
|
+
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
result += tail;
|
|
120
|
+
return new TextEncoder().encode(result);
|
|
121
|
+
}
|
|
122
|
+
function encodeTurnSeqFrame(turnId, seq) {
|
|
123
|
+
return new TextEncoder().encode(
|
|
124
|
+
encodeEvent({ type: TURN_SEQ_EVENT_TYPE, data: { turnId, seq }, transient: true })
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
function encodeEvent(chunk) {
|
|
128
|
+
return `data: ${JSON.stringify(chunk)}
|
|
129
|
+
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
function parseEvent(event) {
|
|
133
|
+
if (!event.startsWith("data: ") || event.includes("\n")) return null;
|
|
134
|
+
const payload = event.slice(6);
|
|
135
|
+
if (!payload.startsWith("{")) return null;
|
|
136
|
+
try {
|
|
137
|
+
const chunk = JSON.parse(payload);
|
|
138
|
+
return chunk && typeof chunk.type === "string" ? chunk : null;
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/stream-buffer.ts
|
|
27
145
|
var CLEANUP_INTERVAL_MS = 6e4;
|
|
28
146
|
var MAX_BUFFER_AGE_MS = 30 * 6e4;
|
|
29
147
|
var COMPLETED_GRACE_MS = 3e4;
|
|
148
|
+
var TURN_REPLY_TTL_MS = 60 * 6e4;
|
|
30
149
|
var StreamBufferStore = class {
|
|
31
150
|
constructor() {
|
|
32
151
|
this.buffers = /* @__PURE__ */ new Map();
|
|
152
|
+
this.replies = /* @__PURE__ */ new Map();
|
|
33
153
|
this.cleanupTimer = null;
|
|
34
154
|
this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
|
|
35
155
|
const handle = this.cleanupTimer;
|
|
@@ -143,6 +263,44 @@ var StreamBufferStore = class {
|
|
|
143
263
|
lastEventAt: buf.lastEventAt
|
|
144
264
|
};
|
|
145
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Keep the reply a turn finished with, so it can still be read after the
|
|
268
|
+
* buffer is evicted (30 s grace) — e.g. by the API's call follower, or by a
|
|
269
|
+
* reconciliation pass long after the stream is gone. Keyed by the same
|
|
270
|
+
* session key as the buffer.
|
|
271
|
+
*/
|
|
272
|
+
recordTurnReply(key, reply) {
|
|
273
|
+
const trimmed = reply?.trim();
|
|
274
|
+
if (!trimmed) return;
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
this.pruneReplies(now);
|
|
277
|
+
this.replies.set(key, {
|
|
278
|
+
turnId: this.buffers.get(key)?.turnId ?? "",
|
|
279
|
+
reply,
|
|
280
|
+
completedAt: now
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The terminal reply for a session, or null when none was recorded or it
|
|
285
|
+
* expired. A reply belonging to an older turn of a session that has since
|
|
286
|
+
* started a new one is never returned.
|
|
287
|
+
*/
|
|
288
|
+
turnReply(key) {
|
|
289
|
+
const entry = this.replies.get(key);
|
|
290
|
+
if (!entry) return null;
|
|
291
|
+
if (Date.now() - entry.completedAt > TURN_REPLY_TTL_MS) {
|
|
292
|
+
this.replies.delete(key);
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const current = this.buffers.get(key);
|
|
296
|
+
if (current && entry.turnId && current.turnId !== entry.turnId) return null;
|
|
297
|
+
return entry;
|
|
298
|
+
}
|
|
299
|
+
pruneReplies(now) {
|
|
300
|
+
for (const [key, entry] of this.replies) {
|
|
301
|
+
if (now - entry.completedAt > TURN_REPLY_TTL_MS) this.replies.delete(key);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
146
304
|
/**
|
|
147
305
|
* Create a ReadableStream that first replays all buffered chunks past
|
|
148
306
|
* `opts.fromSeq` (default: 0 → replay everything), then subscribes for
|
|
@@ -155,15 +313,29 @@ var StreamBufferStore = class {
|
|
|
155
313
|
if (!buf) return null;
|
|
156
314
|
const fromSeq = Math.max(0, opts.fromSeq ?? 0);
|
|
157
315
|
let subscribedController = null;
|
|
316
|
+
if (opts.buildReplay) return this.createBuiltReplayStream(buf, fromSeq, opts.buildReplay);
|
|
158
317
|
return new ReadableStream({
|
|
159
318
|
start(controller) {
|
|
160
|
-
|
|
161
|
-
|
|
319
|
+
if (opts.compact) {
|
|
320
|
+
const pending = buf.frames.filter((frame) => frame.seq > fromSeq);
|
|
321
|
+
const compacted = compactSseFrames(pending.map((frame) => frame.chunk));
|
|
162
322
|
try {
|
|
163
|
-
controller.enqueue(
|
|
323
|
+
if (compacted.byteLength > 0) controller.enqueue(compacted);
|
|
324
|
+
if (pending.length > 0 && buf.status === "active") {
|
|
325
|
+
controller.enqueue(encodeTurnSeqFrame(buf.turnId, buf.nextSeq - 1));
|
|
326
|
+
}
|
|
164
327
|
} catch {
|
|
165
328
|
return;
|
|
166
329
|
}
|
|
330
|
+
} else {
|
|
331
|
+
for (const frame of buf.frames) {
|
|
332
|
+
if (frame.seq <= fromSeq) continue;
|
|
333
|
+
try {
|
|
334
|
+
controller.enqueue(frame.chunk);
|
|
335
|
+
} catch {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
167
339
|
}
|
|
168
340
|
if (buf.status !== "active") {
|
|
169
341
|
try {
|
|
@@ -183,8 +355,59 @@ var StreamBufferStore = class {
|
|
|
183
355
|
}
|
|
184
356
|
});
|
|
185
357
|
}
|
|
358
|
+
createBuiltReplayStream(buf, fromSeq, buildReplay) {
|
|
359
|
+
const pending = buf.frames.filter((frame) => frame.seq > fromSeq).map((frame) => frame.chunk);
|
|
360
|
+
const ctx = { turnId: buf.turnId, lastSeq: buf.nextSeq - 1 };
|
|
361
|
+
const held = [];
|
|
362
|
+
let ended = false;
|
|
363
|
+
let target = null;
|
|
364
|
+
const proxy = {
|
|
365
|
+
enqueue(chunk) {
|
|
366
|
+
if (target) target.enqueue(chunk);
|
|
367
|
+
else held.push(chunk);
|
|
368
|
+
},
|
|
369
|
+
close() {
|
|
370
|
+
if (target) target.close();
|
|
371
|
+
else ended = true;
|
|
372
|
+
},
|
|
373
|
+
error(reason) {
|
|
374
|
+
if (target) target.error(reason);
|
|
375
|
+
else ended = true;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const active = buf.status === "active";
|
|
379
|
+
if (active) buf.subscribers.add(proxy);
|
|
380
|
+
return new ReadableStream({
|
|
381
|
+
async start(controller) {
|
|
382
|
+
let replay;
|
|
383
|
+
try {
|
|
384
|
+
replay = await buildReplay(pending, ctx);
|
|
385
|
+
} catch {
|
|
386
|
+
replay = compactSseFrames(pending);
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
if (replay.byteLength > 0) controller.enqueue(replay);
|
|
390
|
+
if (pending.length > 0 && active) controller.enqueue(encodeTurnSeqFrame(ctx.turnId, ctx.lastSeq));
|
|
391
|
+
for (const chunk of held) controller.enqueue(chunk);
|
|
392
|
+
held.length = 0;
|
|
393
|
+
if (!active || ended || buf.status !== "active") {
|
|
394
|
+
controller.close();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
target = controller;
|
|
398
|
+
} catch {
|
|
399
|
+
buf.subscribers.delete(proxy);
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
cancel() {
|
|
403
|
+
buf.subscribers.delete(proxy);
|
|
404
|
+
target = null;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
}
|
|
186
408
|
cleanup() {
|
|
187
409
|
const now = Date.now();
|
|
410
|
+
this.pruneReplies(now);
|
|
188
411
|
for (const [key, buf] of this.buffers) {
|
|
189
412
|
if (buf.status !== "active" && buf.completedAt && now - buf.completedAt > COMPLETED_GRACE_MS) {
|
|
190
413
|
this.buffers.delete(key);
|
|
@@ -203,6 +426,7 @@ var StreamBufferStore = class {
|
|
|
203
426
|
this.completeBuffer(buf, "disposed");
|
|
204
427
|
}
|
|
205
428
|
this.buffers.clear();
|
|
429
|
+
this.replies.clear();
|
|
206
430
|
}
|
|
207
431
|
completeBuffer(buf, reason) {
|
|
208
432
|
if (buf.status !== "active") return;
|
|
@@ -233,6 +457,14 @@ var StreamBufferStore = class {
|
|
|
233
457
|
buf.subscribers.clear();
|
|
234
458
|
}
|
|
235
459
|
};
|
|
460
|
+
var TURN_COMPLETE_EVENT_TYPE = "data-turn-complete";
|
|
461
|
+
function encodeTurnCompleteFrame(data) {
|
|
462
|
+
return new TextEncoder().encode(
|
|
463
|
+
`data: ${JSON.stringify({ type: TURN_COMPLETE_EVENT_TYPE, data })}
|
|
464
|
+
|
|
465
|
+
`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
236
468
|
function generateTurnId() {
|
|
237
469
|
try {
|
|
238
470
|
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
@@ -256,6 +488,11 @@ function createBufferingTransform(store, key) {
|
|
|
256
488
|
// Annotate the CommonJS export names for ESM import in node:
|
|
257
489
|
0 && (module.exports = {
|
|
258
490
|
StreamBufferStore,
|
|
259
|
-
|
|
491
|
+
TURN_COMPLETE_EVENT_TYPE,
|
|
492
|
+
TURN_SEQ_EVENT_TYPE,
|
|
493
|
+
compactSseFrames,
|
|
494
|
+
createBufferingTransform,
|
|
495
|
+
encodeTurnCompleteFrame,
|
|
496
|
+
encodeTurnSeqFrame
|
|
260
497
|
});
|
|
261
498
|
//# sourceMappingURL=stream-buffer.cjs.map
|
package/dist/stream-buffer.d.cts
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction of a buffered AI SDK UI-message stream for replay.
|
|
3
|
+
*
|
|
4
|
+
* A long turn buffers tens of thousands of tiny frames (one per text, reasoning
|
|
5
|
+
* or tool-input delta). Replaying them one by one makes a reconnecting client
|
|
6
|
+
* rebuild the message chunk by chunk, which takes seconds on a phone. The
|
|
7
|
+
* compacted replay produces the same message with a handful of chunks:
|
|
8
|
+
*
|
|
9
|
+
* - all deltas of one text / reasoning part are merged into its first delta
|
|
10
|
+
* (a part runs from its `*-start` to its `*-end`; providers reuse ids
|
|
11
|
+
* across steps, so merging never crosses a start);
|
|
12
|
+
* - tool-input deltas are merged per tool call, and dropped entirely once
|
|
13
|
+
* `tool-input-available` / `tool-input-error` carries the full input;
|
|
14
|
+
* - of a tool call's `preliminary: true` outputs only the latest is kept (each
|
|
15
|
+
* one carries the whole accumulated state, so it replaces the earlier ones),
|
|
16
|
+
* and all of them are dropped once the call's final output or error is in
|
|
17
|
+
* the buffer — a sub-agent streams one of these per update;
|
|
18
|
+
* - `data-turn-seq` heartbeats are dropped; the caller appends one with the
|
|
19
|
+
* exact current seq so `?fromSeq=N` reconnects stay correct.
|
|
20
|
+
*
|
|
21
|
+
* Anything that isn't a parseable `data: {json}` event is kept verbatim, in
|
|
22
|
+
* place, as is a trailing partial event.
|
|
23
|
+
*/
|
|
24
|
+
declare const TURN_SEQ_EVENT_TYPE: "data-turn-seq";
|
|
25
|
+
declare function compactSseFrames(frames: Uint8Array[]): Uint8Array;
|
|
26
|
+
declare function encodeTurnSeqFrame(turnId: string, seq: number): Uint8Array;
|
|
27
|
+
|
|
1
28
|
/**
|
|
2
29
|
* In-memory stream buffer + turn ledger for SSE reconnect support.
|
|
3
30
|
*
|
|
@@ -16,6 +43,7 @@
|
|
|
16
43
|
*
|
|
17
44
|
* Lifted into the SDK from `@shogo/shared-runtime` (was AGPL) under MIT.
|
|
18
45
|
*/
|
|
46
|
+
|
|
19
47
|
type TurnStatus = 'active' | 'completed' | 'aborted' | 'failed';
|
|
20
48
|
interface TurnTerminal {
|
|
21
49
|
reason?: string;
|
|
@@ -41,6 +69,27 @@ interface ReplayOptions {
|
|
|
41
69
|
* Skip frames with seq <= fromSeq. Use 0 (or omit) to replay from the start.
|
|
42
70
|
*/
|
|
43
71
|
fromSeq?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Replay the buffered frames as one compacted chunk (see
|
|
74
|
+
* `compactSseFrames`) followed by a `data-turn-seq` frame with the exact
|
|
75
|
+
* last seq, then the live frames. The rebuilt message is the same; the
|
|
76
|
+
* client processes a handful of chunks instead of one per delta.
|
|
77
|
+
*/
|
|
78
|
+
compact?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Replace the buffered-frame replay with caller-built bytes. Receives the
|
|
81
|
+
* frames after `fromSeq` and returns what to send before the live frames
|
|
82
|
+
* (a `data-turn-seq` frame with the exact last seq is appended). Takes
|
|
83
|
+
* precedence over `compact`; if it throws, the replay falls back to
|
|
84
|
+
* `compactSseFrames`.
|
|
85
|
+
*
|
|
86
|
+
* The builder may be async: live frames written while it runs are held
|
|
87
|
+
* back and delivered after its output, in order.
|
|
88
|
+
*/
|
|
89
|
+
buildReplay?: (frames: Uint8Array[], ctx: {
|
|
90
|
+
turnId: string;
|
|
91
|
+
lastSeq: number;
|
|
92
|
+
}) => Uint8Array | Promise<Uint8Array>;
|
|
44
93
|
}
|
|
45
94
|
interface TurnSnapshot {
|
|
46
95
|
turnId: string;
|
|
@@ -51,8 +100,15 @@ interface TurnSnapshot {
|
|
|
51
100
|
completedAt: number | null;
|
|
52
101
|
lastEventAt: number;
|
|
53
102
|
}
|
|
103
|
+
/** The reply a pipeline turn finished with, kept past its buffer's lifetime. */
|
|
104
|
+
interface TurnReply {
|
|
105
|
+
turnId: string;
|
|
106
|
+
reply: string;
|
|
107
|
+
completedAt: number;
|
|
108
|
+
}
|
|
54
109
|
declare class StreamBufferStore {
|
|
55
110
|
private buffers;
|
|
111
|
+
private replies;
|
|
56
112
|
private cleanupTimer;
|
|
57
113
|
constructor();
|
|
58
114
|
/**
|
|
@@ -78,6 +134,20 @@ declare class StreamBufferStore {
|
|
|
78
134
|
has(key: string): boolean;
|
|
79
135
|
/** Read-only metadata about a buffered turn. Useful for resume responses. */
|
|
80
136
|
snapshot(key: string): TurnSnapshot | null;
|
|
137
|
+
/**
|
|
138
|
+
* Keep the reply a turn finished with, so it can still be read after the
|
|
139
|
+
* buffer is evicted (30 s grace) — e.g. by the API's call follower, or by a
|
|
140
|
+
* reconciliation pass long after the stream is gone. Keyed by the same
|
|
141
|
+
* session key as the buffer.
|
|
142
|
+
*/
|
|
143
|
+
recordTurnReply(key: string, reply: string): void;
|
|
144
|
+
/**
|
|
145
|
+
* The terminal reply for a session, or null when none was recorded or it
|
|
146
|
+
* expired. A reply belonging to an older turn of a session that has since
|
|
147
|
+
* started a new one is never returned.
|
|
148
|
+
*/
|
|
149
|
+
turnReply(key: string): TurnReply | null;
|
|
150
|
+
private pruneReplies;
|
|
81
151
|
/**
|
|
82
152
|
* Create a ReadableStream that first replays all buffered chunks past
|
|
83
153
|
* `opts.fromSeq` (default: 0 → replay everything), then subscribes for
|
|
@@ -86,11 +156,37 @@ declare class StreamBufferStore {
|
|
|
86
156
|
* Returns null if no buffer exists for the key.
|
|
87
157
|
*/
|
|
88
158
|
createReplayStream(key: string, opts?: ReplayOptions): ReadableStream<Uint8Array> | null;
|
|
159
|
+
private createBuiltReplayStream;
|
|
89
160
|
cleanup(): void;
|
|
90
161
|
dispose(): void;
|
|
91
162
|
private completeBuffer;
|
|
92
163
|
private failBuffer;
|
|
93
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* The SSE data-part event type the client's auto-resuming fetch treats as a
|
|
167
|
+
* turn's terminal marker. The client keeps reconnecting to `/stream?fromSeq=N`
|
|
168
|
+
* until it parses one of these; a buffer that reaches a terminal `status`
|
|
169
|
+
* WITHOUT such a frame makes the client replay the tail forever and pins
|
|
170
|
+
* `useChat().status` at `streaming` (composer stuck on Stop/Queue).
|
|
171
|
+
*/
|
|
172
|
+
declare const TURN_COMPLETE_EVENT_TYPE: "data-turn-complete";
|
|
173
|
+
interface TurnCompleteFrameData {
|
|
174
|
+
turnId: string;
|
|
175
|
+
chatSessionId: string;
|
|
176
|
+
status: TurnStatus;
|
|
177
|
+
error?: string;
|
|
178
|
+
lastSeq: number;
|
|
179
|
+
completedAt?: number;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Encode a `data-turn-complete` SSE frame as raw bytes suitable for
|
|
183
|
+
* {@link StreamBufferWriter.append}. Matches the AI SDK UI-message-stream wire
|
|
184
|
+
* format (`data: <json>\n\n`) so the client's SSE parser recognizes it
|
|
185
|
+
* identically to frames written through the live `writer`. Use this to
|
|
186
|
+
* synthesize a terminal marker when a turn ends abnormally (transport error,
|
|
187
|
+
* abort race) before the normal terminal frame was emitted.
|
|
188
|
+
*/
|
|
189
|
+
declare function encodeTurnCompleteFrame(data: TurnCompleteFrameData): Uint8Array;
|
|
94
190
|
/**
|
|
95
191
|
* TransformStream that copies every chunk into a StreamBufferStore while
|
|
96
192
|
* passing it through unchanged. On flush/cancel, completes the buffer.
|
|
@@ -100,4 +196,4 @@ declare class StreamBufferStore {
|
|
|
100
196
|
*/
|
|
101
197
|
declare function createBufferingTransform(store: StreamBufferStore, key: string): TransformStream<Uint8Array, Uint8Array>;
|
|
102
198
|
|
|
103
|
-
export { type ReplayOptions, StreamBufferStore, type StreamBufferWriter, type TurnSnapshot, type TurnStatus, type TurnTerminal, createBufferingTransform };
|
|
199
|
+
export { type ReplayOptions, StreamBufferStore, type StreamBufferWriter, TURN_COMPLETE_EVENT_TYPE, TURN_SEQ_EVENT_TYPE, type TurnCompleteFrameData, type TurnReply, type TurnSnapshot, type TurnStatus, type TurnTerminal, compactSseFrames, createBufferingTransform, encodeTurnCompleteFrame, encodeTurnSeqFrame };
|
package/dist/stream-buffer.d.ts
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction of a buffered AI SDK UI-message stream for replay.
|
|
3
|
+
*
|
|
4
|
+
* A long turn buffers tens of thousands of tiny frames (one per text, reasoning
|
|
5
|
+
* or tool-input delta). Replaying them one by one makes a reconnecting client
|
|
6
|
+
* rebuild the message chunk by chunk, which takes seconds on a phone. The
|
|
7
|
+
* compacted replay produces the same message with a handful of chunks:
|
|
8
|
+
*
|
|
9
|
+
* - all deltas of one text / reasoning part are merged into its first delta
|
|
10
|
+
* (a part runs from its `*-start` to its `*-end`; providers reuse ids
|
|
11
|
+
* across steps, so merging never crosses a start);
|
|
12
|
+
* - tool-input deltas are merged per tool call, and dropped entirely once
|
|
13
|
+
* `tool-input-available` / `tool-input-error` carries the full input;
|
|
14
|
+
* - of a tool call's `preliminary: true` outputs only the latest is kept (each
|
|
15
|
+
* one carries the whole accumulated state, so it replaces the earlier ones),
|
|
16
|
+
* and all of them are dropped once the call's final output or error is in
|
|
17
|
+
* the buffer — a sub-agent streams one of these per update;
|
|
18
|
+
* - `data-turn-seq` heartbeats are dropped; the caller appends one with the
|
|
19
|
+
* exact current seq so `?fromSeq=N` reconnects stay correct.
|
|
20
|
+
*
|
|
21
|
+
* Anything that isn't a parseable `data: {json}` event is kept verbatim, in
|
|
22
|
+
* place, as is a trailing partial event.
|
|
23
|
+
*/
|
|
24
|
+
declare const TURN_SEQ_EVENT_TYPE: "data-turn-seq";
|
|
25
|
+
declare function compactSseFrames(frames: Uint8Array[]): Uint8Array;
|
|
26
|
+
declare function encodeTurnSeqFrame(turnId: string, seq: number): Uint8Array;
|
|
27
|
+
|
|
1
28
|
/**
|
|
2
29
|
* In-memory stream buffer + turn ledger for SSE reconnect support.
|
|
3
30
|
*
|
|
@@ -16,6 +43,7 @@
|
|
|
16
43
|
*
|
|
17
44
|
* Lifted into the SDK from `@shogo/shared-runtime` (was AGPL) under MIT.
|
|
18
45
|
*/
|
|
46
|
+
|
|
19
47
|
type TurnStatus = 'active' | 'completed' | 'aborted' | 'failed';
|
|
20
48
|
interface TurnTerminal {
|
|
21
49
|
reason?: string;
|
|
@@ -41,6 +69,27 @@ interface ReplayOptions {
|
|
|
41
69
|
* Skip frames with seq <= fromSeq. Use 0 (or omit) to replay from the start.
|
|
42
70
|
*/
|
|
43
71
|
fromSeq?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Replay the buffered frames as one compacted chunk (see
|
|
74
|
+
* `compactSseFrames`) followed by a `data-turn-seq` frame with the exact
|
|
75
|
+
* last seq, then the live frames. The rebuilt message is the same; the
|
|
76
|
+
* client processes a handful of chunks instead of one per delta.
|
|
77
|
+
*/
|
|
78
|
+
compact?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Replace the buffered-frame replay with caller-built bytes. Receives the
|
|
81
|
+
* frames after `fromSeq` and returns what to send before the live frames
|
|
82
|
+
* (a `data-turn-seq` frame with the exact last seq is appended). Takes
|
|
83
|
+
* precedence over `compact`; if it throws, the replay falls back to
|
|
84
|
+
* `compactSseFrames`.
|
|
85
|
+
*
|
|
86
|
+
* The builder may be async: live frames written while it runs are held
|
|
87
|
+
* back and delivered after its output, in order.
|
|
88
|
+
*/
|
|
89
|
+
buildReplay?: (frames: Uint8Array[], ctx: {
|
|
90
|
+
turnId: string;
|
|
91
|
+
lastSeq: number;
|
|
92
|
+
}) => Uint8Array | Promise<Uint8Array>;
|
|
44
93
|
}
|
|
45
94
|
interface TurnSnapshot {
|
|
46
95
|
turnId: string;
|
|
@@ -51,8 +100,15 @@ interface TurnSnapshot {
|
|
|
51
100
|
completedAt: number | null;
|
|
52
101
|
lastEventAt: number;
|
|
53
102
|
}
|
|
103
|
+
/** The reply a pipeline turn finished with, kept past its buffer's lifetime. */
|
|
104
|
+
interface TurnReply {
|
|
105
|
+
turnId: string;
|
|
106
|
+
reply: string;
|
|
107
|
+
completedAt: number;
|
|
108
|
+
}
|
|
54
109
|
declare class StreamBufferStore {
|
|
55
110
|
private buffers;
|
|
111
|
+
private replies;
|
|
56
112
|
private cleanupTimer;
|
|
57
113
|
constructor();
|
|
58
114
|
/**
|
|
@@ -78,6 +134,20 @@ declare class StreamBufferStore {
|
|
|
78
134
|
has(key: string): boolean;
|
|
79
135
|
/** Read-only metadata about a buffered turn. Useful for resume responses. */
|
|
80
136
|
snapshot(key: string): TurnSnapshot | null;
|
|
137
|
+
/**
|
|
138
|
+
* Keep the reply a turn finished with, so it can still be read after the
|
|
139
|
+
* buffer is evicted (30 s grace) — e.g. by the API's call follower, or by a
|
|
140
|
+
* reconciliation pass long after the stream is gone. Keyed by the same
|
|
141
|
+
* session key as the buffer.
|
|
142
|
+
*/
|
|
143
|
+
recordTurnReply(key: string, reply: string): void;
|
|
144
|
+
/**
|
|
145
|
+
* The terminal reply for a session, or null when none was recorded or it
|
|
146
|
+
* expired. A reply belonging to an older turn of a session that has since
|
|
147
|
+
* started a new one is never returned.
|
|
148
|
+
*/
|
|
149
|
+
turnReply(key: string): TurnReply | null;
|
|
150
|
+
private pruneReplies;
|
|
81
151
|
/**
|
|
82
152
|
* Create a ReadableStream that first replays all buffered chunks past
|
|
83
153
|
* `opts.fromSeq` (default: 0 → replay everything), then subscribes for
|
|
@@ -86,11 +156,37 @@ declare class StreamBufferStore {
|
|
|
86
156
|
* Returns null if no buffer exists for the key.
|
|
87
157
|
*/
|
|
88
158
|
createReplayStream(key: string, opts?: ReplayOptions): ReadableStream<Uint8Array> | null;
|
|
159
|
+
private createBuiltReplayStream;
|
|
89
160
|
cleanup(): void;
|
|
90
161
|
dispose(): void;
|
|
91
162
|
private completeBuffer;
|
|
92
163
|
private failBuffer;
|
|
93
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* The SSE data-part event type the client's auto-resuming fetch treats as a
|
|
167
|
+
* turn's terminal marker. The client keeps reconnecting to `/stream?fromSeq=N`
|
|
168
|
+
* until it parses one of these; a buffer that reaches a terminal `status`
|
|
169
|
+
* WITHOUT such a frame makes the client replay the tail forever and pins
|
|
170
|
+
* `useChat().status` at `streaming` (composer stuck on Stop/Queue).
|
|
171
|
+
*/
|
|
172
|
+
declare const TURN_COMPLETE_EVENT_TYPE: "data-turn-complete";
|
|
173
|
+
interface TurnCompleteFrameData {
|
|
174
|
+
turnId: string;
|
|
175
|
+
chatSessionId: string;
|
|
176
|
+
status: TurnStatus;
|
|
177
|
+
error?: string;
|
|
178
|
+
lastSeq: number;
|
|
179
|
+
completedAt?: number;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Encode a `data-turn-complete` SSE frame as raw bytes suitable for
|
|
183
|
+
* {@link StreamBufferWriter.append}. Matches the AI SDK UI-message-stream wire
|
|
184
|
+
* format (`data: <json>\n\n`) so the client's SSE parser recognizes it
|
|
185
|
+
* identically to frames written through the live `writer`. Use this to
|
|
186
|
+
* synthesize a terminal marker when a turn ends abnormally (transport error,
|
|
187
|
+
* abort race) before the normal terminal frame was emitted.
|
|
188
|
+
*/
|
|
189
|
+
declare function encodeTurnCompleteFrame(data: TurnCompleteFrameData): Uint8Array;
|
|
94
190
|
/**
|
|
95
191
|
* TransformStream that copies every chunk into a StreamBufferStore while
|
|
96
192
|
* passing it through unchanged. On flush/cancel, completes the buffer.
|
|
@@ -100,4 +196,4 @@ declare class StreamBufferStore {
|
|
|
100
196
|
*/
|
|
101
197
|
declare function createBufferingTransform(store: StreamBufferStore, key: string): TransformStream<Uint8Array, Uint8Array>;
|
|
102
198
|
|
|
103
|
-
export { type ReplayOptions, StreamBufferStore, type StreamBufferWriter, type TurnSnapshot, type TurnStatus, type TurnTerminal, createBufferingTransform };
|
|
199
|
+
export { type ReplayOptions, StreamBufferStore, type StreamBufferWriter, TURN_COMPLETE_EVENT_TYPE, TURN_SEQ_EVENT_TYPE, type TurnCompleteFrameData, type TurnReply, type TurnSnapshot, type TurnStatus, type TurnTerminal, compactSseFrames, createBufferingTransform, encodeTurnCompleteFrame, encodeTurnSeqFrame };
|
package/dist/stream-buffer.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
StreamBufferStore,
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
TURN_COMPLETE_EVENT_TYPE,
|
|
4
|
+
TURN_SEQ_EVENT_TYPE,
|
|
5
|
+
compactSseFrames,
|
|
6
|
+
createBufferingTransform,
|
|
7
|
+
encodeTurnCompleteFrame,
|
|
8
|
+
encodeTurnSeqFrame
|
|
9
|
+
} from "./chunk-H625AGFN.js";
|
|
5
10
|
export {
|
|
6
11
|
StreamBufferStore,
|
|
7
|
-
|
|
12
|
+
TURN_COMPLETE_EVENT_TYPE,
|
|
13
|
+
TURN_SEQ_EVENT_TYPE,
|
|
14
|
+
compactSseFrames,
|
|
15
|
+
createBufferingTransform,
|
|
16
|
+
encodeTurnCompleteFrame,
|
|
17
|
+
encodeTurnSeqFrame
|
|
8
18
|
};
|
|
9
19
|
//# sourceMappingURL=stream-buffer.js.map
|
|
@@ -21,7 +21,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var tech_stack_registry_exports = {};
|
|
22
22
|
__export(tech_stack_registry_exports, {
|
|
23
23
|
TECH_STACK_REGISTRY: () => TECH_STACK_REGISTRY,
|
|
24
|
+
getDeclaredPorts: () => getDeclaredPorts,
|
|
25
|
+
getMinimumInstanceSize: () => getMinimumInstanceSize,
|
|
24
26
|
getStackEntry: () => getStackEntry,
|
|
27
|
+
isDockerTechStack: () => isDockerTechStack,
|
|
25
28
|
isMobileTechStack: () => isMobileTechStack,
|
|
26
29
|
stackSeedsItself: () => stackSeedsItself,
|
|
27
30
|
usesMetroBundler: () => usesMetroBundler
|
|
@@ -32,14 +35,35 @@ var TECH_STACK_REGISTRY = {
|
|
|
32
35
|
"react-app": { id: "react-app", target: "web" },
|
|
33
36
|
"threejs-game": { id: "threejs-game", target: "web" },
|
|
34
37
|
"phaser-game": { id: "phaser-game", target: "web" },
|
|
38
|
+
// Video (Remotion). `target: 'web'` because the canvas preview is the
|
|
39
|
+
// Remotion Player page the runtime serves from `dist/`; the stack's own
|
|
40
|
+
// `stack.json` declares `runtime.devServer: 'static-build'`, so no new
|
|
41
|
+
// target value (and therefore no new sizing/publish rules) is needed.
|
|
42
|
+
"video-editor": { id: "video-editor", target: "web", seedsOwnTemplate: true },
|
|
35
43
|
// Mobile (Metro/Expo) — agent-runtime owns the seed.
|
|
36
44
|
"expo-app": { id: "expo-app", target: "mobile", metro: true, seedsOwnTemplate: true },
|
|
37
45
|
"expo-three": { id: "expo-three", target: "mobile", metro: true, seedsOwnTemplate: true },
|
|
38
46
|
"react-native": { id: "react-native", target: "mobile", metro: true, seedsOwnTemplate: true },
|
|
39
47
|
// Data / scripting
|
|
40
48
|
"python-data": { id: "python-data", target: "data", seedsOwnTemplate: true },
|
|
49
|
+
// Multi-service backend (dockerd + docker compose). Requires the
|
|
50
|
+
// Docker-capable metal VM class; gated separately by
|
|
51
|
+
// `runtime.docker_class_enabled` (see apps/api/src/lib/runtime-class-setting.ts).
|
|
52
|
+
"docker-compose": {
|
|
53
|
+
id: "docker-compose",
|
|
54
|
+
target: "data",
|
|
55
|
+
seedsOwnTemplate: true,
|
|
56
|
+
vmClass: "docker",
|
|
57
|
+
minimumInstanceSize: "large",
|
|
58
|
+
ports: [
|
|
59
|
+
{ port: 8e3, label: "app", protocol: "http", defaultVisibility: "preview" },
|
|
60
|
+
{ port: 5432, label: "postgres", protocol: "tcp", defaultVisibility: "tunnel" }
|
|
61
|
+
]
|
|
62
|
+
},
|
|
41
63
|
// Native (full game engines)
|
|
42
64
|
"unity-game": { id: "unity-game", target: "native", seedsOwnTemplate: true },
|
|
65
|
+
// Bring-your-own framework: the preview runs the project's static build.
|
|
66
|
+
custom: { id: "custom", target: "web", seedsOwnTemplate: true },
|
|
43
67
|
// Bare / no-stack
|
|
44
68
|
none: { id: "none", target: "none", seedsOwnTemplate: true }
|
|
45
69
|
};
|
|
@@ -56,10 +80,22 @@ function usesMetroBundler(techStackId) {
|
|
|
56
80
|
function stackSeedsItself(techStackId) {
|
|
57
81
|
return getStackEntry(techStackId)?.seedsOwnTemplate === true;
|
|
58
82
|
}
|
|
83
|
+
function isDockerTechStack(techStackId) {
|
|
84
|
+
return getStackEntry(techStackId)?.vmClass === "docker";
|
|
85
|
+
}
|
|
86
|
+
function getMinimumInstanceSize(techStackId) {
|
|
87
|
+
return getStackEntry(techStackId)?.minimumInstanceSize ?? null;
|
|
88
|
+
}
|
|
89
|
+
function getDeclaredPorts(techStackId) {
|
|
90
|
+
return getStackEntry(techStackId)?.ports ?? [];
|
|
91
|
+
}
|
|
59
92
|
// Annotate the CommonJS export names for ESM import in node:
|
|
60
93
|
0 && (module.exports = {
|
|
61
94
|
TECH_STACK_REGISTRY,
|
|
95
|
+
getDeclaredPorts,
|
|
96
|
+
getMinimumInstanceSize,
|
|
62
97
|
getStackEntry,
|
|
98
|
+
isDockerTechStack,
|
|
63
99
|
isMobileTechStack,
|
|
64
100
|
stackSeedsItself,
|
|
65
101
|
usesMetroBundler
|