@tanstack/ai 0.42.0 → 0.43.0
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/README.md +15 -1
- package/dist/esm/activities/chat/adapter.js +23 -16
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/agent-loop-strategies.d.ts +5 -36
- package/dist/esm/activities/chat/agent-loop-strategies.js +75 -21
- package/dist/esm/activities/chat/agent-loop-strategies.js.map +1 -1
- package/dist/esm/activities/chat/cancel.d.ts +40 -0
- package/dist/esm/activities/chat/cancel.js +54 -0
- package/dist/esm/activities/chat/cancel.js.map +1 -0
- package/dist/esm/activities/chat/index.d.ts +28 -21
- package/dist/esm/activities/chat/index.js +2100 -1813
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/mcp/manager.d.ts +2 -2
- package/dist/esm/activities/chat/mcp/manager.js +90 -77
- package/dist/esm/activities/chat/mcp/manager.js.map +1 -1
- package/dist/esm/activities/chat/mcp/types.d.ts +2 -2
- package/dist/esm/activities/chat/messages.js +397 -346
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/builder.js +17 -15
- package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
- package/dist/esm/activities/chat/middleware/capabilities.js +78 -43
- package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.d.ts +94 -1
- package/dist/esm/activities/chat/middleware/compose.js +623 -531
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/define.js +12 -5
- package/dist/esm/activities/chat/middleware/define.js.map +1 -1
- package/dist/esm/activities/chat/middleware/index.d.ts +5 -1
- package/dist/esm/activities/chat/middleware/locks.d.ts +50 -0
- package/dist/esm/activities/chat/middleware/locks.js +71 -0
- package/dist/esm/activities/chat/middleware/locks.js.map +1 -0
- package/dist/esm/activities/chat/middleware/pending-turn.d.ts +15 -0
- package/dist/esm/activities/chat/middleware/pending-turn.js +35 -0
- package/dist/esm/activities/chat/middleware/pending-turn.js.map +1 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.d.ts +23 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.js +42 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.js.map +1 -0
- package/dist/esm/activities/chat/middleware/run-store.d.ts +283 -0
- package/dist/esm/activities/chat/middleware/run-store.js +176 -0
- package/dist/esm/activities/chat/middleware/run-store.js.map +1 -0
- package/dist/esm/activities/chat/middleware/sandbox-runtime.js +14 -8
- package/dist/esm/activities/chat/middleware/sandbox-runtime.js.map +1 -1
- package/dist/esm/activities/chat/middleware/tool-cache-middleware.js +79 -70
- package/dist/esm/activities/chat/middleware/tool-cache-middleware.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +59 -2
- package/dist/esm/activities/chat/middleware/validate.js +23 -28
- package/dist/esm/activities/chat/middleware/validate.js.map +1 -1
- package/dist/esm/activities/chat/stream/json-parser.js +39 -25
- package/dist/esm/activities/chat/stream/json-parser.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.js +275 -234
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +24 -4
- package/dist/esm/activities/chat/stream/processor.js +1341 -1542
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/stream/strategies.js +69 -53
- package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
- package/dist/esm/activities/chat/tools/approval-schema.d.ts +19 -0
- package/dist/esm/activities/chat/tools/approval-schema.js +117 -0
- package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -0
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js +164 -191
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
- package/dist/esm/activities/chat/tools/lazy-tools.js +24 -12
- package/dist/esm/activities/chat/tools/lazy-tools.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.js +293 -146
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +18 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +522 -531
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +75 -16
- package/dist/esm/activities/chat/tools/tool-definition.js +95 -23
- package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
- package/dist/esm/activities/error-payload.js +85 -47
- package/dist/esm/activities/error-payload.js.map +1 -1
- package/dist/esm/activities/generateAudio/adapter.js +22 -15
- package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
- package/dist/esm/activities/generateAudio/index.d.ts +4 -0
- package/dist/esm/activities/generateAudio/index.js +141 -105
- package/dist/esm/activities/generateAudio/index.js.map +1 -1
- package/dist/esm/activities/generateImage/adapter.js +22 -15
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateImage/index.d.ts +4 -0
- package/dist/esm/activities/generateImage/index.js +155 -111
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateSpeech/adapter.js +22 -15
- package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.d.ts +4 -0
- package/dist/esm/activities/generateSpeech/index.js +159 -110
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateTranscription/adapter.js +22 -15
- package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
- package/dist/esm/activities/generateTranscription/index.d.ts +4 -0
- package/dist/esm/activities/generateTranscription/index.js +159 -100
- package/dist/esm/activities/generateTranscription/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.js +36 -29
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +143 -19
- package/dist/esm/activities/generateVideo/index.js +456 -279
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/snap.js +60 -48
- package/dist/esm/activities/generateVideo/snap.js.map +1 -1
- package/dist/esm/activities/index.js +8 -34
- package/dist/esm/activities/middleware/index.d.ts +1 -1
- package/dist/esm/activities/middleware/run.d.ts +10 -0
- package/dist/esm/activities/middleware/run.js +53 -29
- package/dist/esm/activities/middleware/run.js.map +1 -1
- package/dist/esm/activities/middleware/types.d.ts +44 -6
- package/dist/esm/activities/stream-generation-result.d.ts +4 -1
- package/dist/esm/activities/stream-generation-result.js +79 -44
- package/dist/esm/activities/stream-generation-result.js.map +1 -1
- package/dist/esm/activities/summarize/adapter.js +22 -15
- package/dist/esm/activities/summarize/adapter.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js +252 -202
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/activities/summarize/index.d.ts +27 -0
- package/dist/esm/activities/summarize/index.js +268 -102
- package/dist/esm/activities/summarize/index.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +2 -1
- package/dist/esm/adapter-internals.js +4 -11
- package/dist/esm/client.d.ts +25 -3
- package/dist/esm/client.js +131 -64
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/custom-events.d.ts +76 -0
- package/dist/esm/custom-events.js +37 -0
- package/dist/esm/custom-events.js.map +1 -0
- package/dist/esm/delivery-detach.d.ts +50 -0
- package/dist/esm/delivery-detach.js +71 -0
- package/dist/esm/delivery-detach.js.map +1 -0
- package/dist/esm/delivery-disconnect.d.ts +62 -0
- package/dist/esm/delivery-disconnect.js +81 -0
- package/dist/esm/delivery-disconnect.js.map +1 -0
- package/dist/esm/extend-adapter.js +19 -17
- package/dist/esm/extend-adapter.js.map +1 -1
- package/dist/esm/index.d.ts +24 -6
- package/dist/esm/index.js +30 -98
- package/dist/esm/interrupt-resume.d.ts +71 -0
- package/dist/esm/interrupt-resume.js +438 -0
- package/dist/esm/interrupt-resume.js.map +1 -0
- package/dist/esm/interrupt-serialization.d.ts +12 -0
- package/dist/esm/interrupt-serialization.js +178 -0
- package/dist/esm/interrupt-serialization.js.map +1 -0
- package/dist/esm/interrupts.d.ts +84 -0
- package/dist/esm/interrupts.js +31 -0
- package/dist/esm/interrupts.js.map +1 -0
- package/dist/esm/locks.d.ts +10 -0
- package/dist/esm/locks.js +2 -0
- package/dist/esm/logger/console-logger.js +101 -78
- package/dist/esm/logger/console-logger.js.map +1 -1
- package/dist/esm/logger/internal-logger.js +104 -89
- package/dist/esm/logger/internal-logger.js.map +1 -1
- package/dist/esm/logger/resolve.js +54 -49
- package/dist/esm/logger/resolve.js.map +1 -1
- package/dist/esm/logger/types.d.ts +1 -1
- package/dist/esm/middlewares/content-guard.js +142 -148
- package/dist/esm/middlewares/content-guard.js.map +1 -1
- package/dist/esm/middlewares/index.js +2 -6
- package/dist/esm/middlewares/otel.js +598 -732
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/middlewares/usage-attributes.js +47 -40
- package/dist/esm/middlewares/usage-attributes.js.map +1 -1
- package/dist/esm/realtime/event-emitter.js +24 -25
- package/dist/esm/realtime/event-emitter.js.map +1 -1
- package/dist/esm/realtime/index.d.ts +5 -9
- package/dist/esm/realtime/index.js +29 -6
- package/dist/esm/realtime/index.js.map +1 -1
- package/dist/esm/scope.d.ts +47 -0
- package/dist/esm/stream-durability.d.ts +171 -0
- package/dist/esm/stream-durability.js +295 -0
- package/dist/esm/stream-durability.js.map +1 -0
- package/dist/esm/stream-to-response.d.ts +178 -13
- package/dist/esm/stream-to-response.js +663 -115
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.js +30 -16
- package/dist/esm/strip-to-spec-middleware.js.map +1 -1
- package/dist/esm/system-prompts.js +27 -21
- package/dist/esm/system-prompts.js.map +1 -1
- package/dist/esm/tool-registry.js +72 -45
- package/dist/esm/tool-registry.js.map +1 -1
- package/dist/esm/tools/provider-tool.js +14 -5
- package/dist/esm/tools/provider-tool.js.map +1 -1
- package/dist/esm/types.d.ts +321 -42
- package/dist/esm/types.js +2 -0
- package/dist/esm/utilities/ag-ui-wire.js +79 -93
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/chat-params.d.ts +26 -4
- package/dist/esm/utilities/chat-params.js +218 -92
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/dist/esm/utilities/errors.js +28 -18
- package/dist/esm/utilities/errors.js.map +1 -1
- package/dist/esm/utilities/media-prompt.js +46 -41
- package/dist/esm/utilities/media-prompt.js.map +1 -1
- package/dist/esm/utilities/numbers.js +13 -10
- package/dist/esm/utilities/numbers.js.map +1 -1
- package/dist/esm/utilities/provider-executed.js +20 -11
- package/dist/esm/utilities/provider-executed.js.map +1 -1
- package/dist/esm/utilities/sampling-keys.js +31 -19
- package/dist/esm/utilities/sampling-keys.js.map +1 -1
- package/dist/esm/utilities/tool-result.js +42 -30
- package/dist/esm/utilities/tool-result.js.map +1 -1
- package/dist/esm/utilities/usage.js +27 -9
- package/dist/esm/utilities/usage.js.map +1 -1
- package/dist/esm/utils.js +26 -18
- package/dist/esm/utils.js.map +1 -1
- package/package.json +10 -6
- package/skills/ai-core/SKILL.md +69 -18
- package/skills/ai-core/adapter-configuration/SKILL.md +44 -21
- package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +1 -3
- package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +148 -0
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -6
- package/skills/ai-core/adapter-configuration/references/groq-adapter.md +2 -6
- package/skills/ai-core/adapter-configuration/references/openai-adapter.md +1 -3
- package/skills/ai-core/ag-ui-protocol/SKILL.md +1 -1
- package/skills/ai-core/chat-experience/SKILL.md +98 -11
- package/skills/ai-core/client-persistence/SKILL.md +277 -0
- package/skills/ai-core/custom-backend-integration/SKILL.md +1 -1
- package/skills/ai-core/debug-logging/SKILL.md +1 -1
- package/skills/ai-core/locks/SKILL.md +143 -0
- package/skills/ai-core/media-generation/SKILL.md +144 -12
- package/skills/ai-core/middleware/SKILL.md +258 -33
- package/skills/ai-core/structured-outputs/SKILL.md +1 -1
- package/skills/ai-core/tool-calling/SKILL.md +54 -61
- package/src/activities/chat/agent-loop-strategies.ts +5 -39
- package/src/activities/chat/cancel.ts +81 -0
- package/src/activities/chat/index.ts +1091 -200
- package/src/activities/chat/mcp/manager.ts +4 -4
- package/src/activities/chat/mcp/types.ts +2 -2
- package/src/activities/chat/messages.ts +5 -3
- package/src/activities/chat/middleware/builder.ts +1 -1
- package/src/activities/chat/middleware/compose.ts +186 -9
- package/src/activities/chat/middleware/index.ts +26 -0
- package/src/activities/chat/middleware/locks.ts +102 -0
- package/src/activities/chat/middleware/pending-turn.ts +47 -0
- package/src/activities/chat/middleware/run-disconnect.ts +62 -0
- package/src/activities/chat/middleware/run-store.ts +412 -0
- package/src/activities/chat/middleware/types.ts +62 -1
- package/src/activities/chat/stream/processor.ts +189 -5
- package/src/activities/chat/tools/approval-schema.ts +205 -0
- package/src/activities/chat/tools/tool-calls.ts +106 -13
- package/src/activities/chat/tools/tool-definition.ts +210 -39
- package/src/activities/generateAudio/index.ts +20 -3
- package/src/activities/generateImage/index.ts +20 -3
- package/src/activities/generateSpeech/index.ts +25 -3
- package/src/activities/generateTranscription/index.ts +26 -3
- package/src/activities/generateVideo/index.ts +345 -82
- package/src/activities/middleware/index.ts +2 -0
- package/src/activities/middleware/run.ts +31 -0
- package/src/activities/middleware/types.ts +49 -5
- package/src/activities/stream-generation-result.ts +30 -2
- package/src/activities/summarize/chat-stream-summarize.ts +5 -0
- package/src/activities/summarize/index.ts +200 -10
- package/src/adapter-internals.ts +10 -1
- package/src/client.ts +244 -0
- package/src/custom-events.ts +107 -0
- package/src/delivery-detach.ts +72 -0
- package/src/delivery-disconnect.ts +84 -0
- package/src/index.ts +138 -1
- package/src/interrupt-resume.ts +824 -0
- package/src/interrupt-serialization.ts +183 -0
- package/src/interrupts.ts +146 -0
- package/src/locks.ts +17 -0
- package/src/logger/types.ts +1 -1
- package/src/middlewares/otel.ts +1 -0
- package/src/realtime/index.ts +5 -9
- package/src/scope.ts +47 -0
- package/src/stream-durability.ts +598 -0
- package/src/stream-to-response.ts +1051 -95
- package/src/strip-to-spec-middleware.ts +3 -3
- package/src/types.ts +405 -45
- package/src/utilities/chat-params.ts +245 -55
- package/dist/esm/activities/index.js.map +0 -1
- package/dist/esm/adapter-internals.js.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/middlewares/index.js.map +0 -1
|
@@ -1,121 +1,669 @@
|
|
|
1
1
|
import { toRunErrorPayload } from "./activities/error-payload.js";
|
|
2
|
+
import { isCancelRequestedReason } from "./activities/chat/cancel.js";
|
|
3
|
+
import { isRunStatus, isTerminalRunStatus } from "./activities/chat/middleware/run-store.js";
|
|
4
|
+
import { wasRunDetached } from "./delivery-detach.js";
|
|
5
|
+
import { notifyRunDisconnected } from "./delivery-disconnect.js";
|
|
6
|
+
import { resolveResumeRunId } from "./stream-durability.js";
|
|
7
|
+
import { EventType } from "./types.js";
|
|
8
|
+
import { resolveDebugOption } from "./logger/resolve.js";
|
|
9
|
+
//#region src/stream-to-response.ts
|
|
10
|
+
/**
|
|
11
|
+
* Collect all text content from a StreamChunk async iterable and return as a string.
|
|
12
|
+
*
|
|
13
|
+
* This function consumes the entire stream, accumulating content from TEXT_MESSAGE_CONTENT events,
|
|
14
|
+
* and returns the final concatenated text.
|
|
15
|
+
*
|
|
16
|
+
* @param stream - AsyncIterable of StreamChunks from chat()
|
|
17
|
+
* @returns Promise<string> - The accumulated text content
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const stream = chat({
|
|
22
|
+
* adapter: openaiText('gpt-5.5'),
|
|
23
|
+
* messages: [{ role: 'user', content: 'Hello!' }]
|
|
24
|
+
* });
|
|
25
|
+
* const text = await streamToText(stream);
|
|
26
|
+
* console.log(text); // "Hello! How can I help you today?"
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
2
29
|
async function streamToText(stream) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
30
|
+
let accumulatedContent = "";
|
|
31
|
+
for await (const chunk of stream) if (chunk.type === "TEXT_MESSAGE_CONTENT" && chunk.delta) accumulatedContent += chunk.delta;
|
|
32
|
+
return accumulatedContent;
|
|
33
|
+
}
|
|
34
|
+
function errorMessage(error) {
|
|
35
|
+
return toRunErrorPayload(error).message;
|
|
36
|
+
}
|
|
37
|
+
function combineFailures(primary, secondary, phase) {
|
|
38
|
+
if (primary === secondary) return primary;
|
|
39
|
+
const errors = primary instanceof AggregateError ? [...primary.errors, secondary] : [primary, secondary];
|
|
40
|
+
return new AggregateError(errors, `${errorMessage(primary)}; ${phase}: ${errorMessage(secondary)}`);
|
|
41
|
+
}
|
|
42
|
+
function runErrorChunk(error) {
|
|
43
|
+
const payload = toRunErrorPayload(error);
|
|
44
|
+
return {
|
|
45
|
+
type: EventType.RUN_ERROR,
|
|
46
|
+
timestamp: Date.now(),
|
|
47
|
+
message: payload.message,
|
|
48
|
+
...payload.code === void 0 ? {} : { code: payload.code },
|
|
49
|
+
error: payload
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function isAborted(signal) {
|
|
53
|
+
return signal.aborted;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Whether this abort is an EXPLICIT in-process cancel — the caller aborted with
|
|
57
|
+
* {@link RUN_CANCEL_REASON} rather than the socket going away.
|
|
58
|
+
*
|
|
59
|
+
* Core's own guard, independent of any middleware verdict: a user pressing Stop
|
|
60
|
+
* must always get a closed, terminal log, so the sink refuses to treat that abort
|
|
61
|
+
* as a detach even if the run's middleware published one. A reason-less abort
|
|
62
|
+
* carries a `DOMException`, never a string, so a non-string reason is "no
|
|
63
|
+
* explicit intent" — exactly how `resolveAbortReason` reads it in `chat()`.
|
|
64
|
+
*/
|
|
65
|
+
function isExplicitCancel(signal) {
|
|
66
|
+
const reason = signal.reason;
|
|
67
|
+
return typeof reason === "string" && isCancelRequestedReason(reason);
|
|
68
|
+
}
|
|
69
|
+
function needsTerminalPersistence(terminalPersisted, cancelled, failed) {
|
|
70
|
+
return !terminalPersisted && (cancelled || failed);
|
|
71
|
+
}
|
|
72
|
+
function toEncodedStream(stream, abortController, encodeChunk, encodeError, detachOnCancel = false, onDetachedCancel) {
|
|
73
|
+
const cancellation = abortController ?? new AbortController();
|
|
74
|
+
let iterator;
|
|
75
|
+
let iteratorCleanup;
|
|
76
|
+
let pumpPromise = Promise.resolve();
|
|
77
|
+
let pumpFailure;
|
|
78
|
+
let cancelled = false;
|
|
79
|
+
const recordPumpFailure = (error, phase) => {
|
|
80
|
+
pumpFailure = { error: pumpFailure === void 0 ? error : combineFailures(pumpFailure.error, error, phase) };
|
|
81
|
+
};
|
|
82
|
+
const closeIterator = () => {
|
|
83
|
+
iteratorCleanup ??= (async () => {
|
|
84
|
+
if (iterator?.return) await iterator.return();
|
|
85
|
+
})();
|
|
86
|
+
return iteratorCleanup;
|
|
87
|
+
};
|
|
88
|
+
return new ReadableStream({
|
|
89
|
+
start(controller) {
|
|
90
|
+
iterator = stream[Symbol.asyncIterator]();
|
|
91
|
+
pumpPromise = (async () => {
|
|
92
|
+
let index = 0;
|
|
93
|
+
let iteratorDone = false;
|
|
94
|
+
try {
|
|
95
|
+
while (!isAborted(cancellation.signal)) {
|
|
96
|
+
const result = await iterator.next();
|
|
97
|
+
if (result.done) {
|
|
98
|
+
iteratorDone = true;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
if (isAborted(cancellation.signal)) break;
|
|
102
|
+
if (!cancelled) controller.enqueue(encodeChunk(result.value, index));
|
|
103
|
+
index += 1;
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
recordPumpFailure(error, "stream iteration failed");
|
|
107
|
+
} finally {
|
|
108
|
+
if (!iteratorDone) try {
|
|
109
|
+
await closeIterator();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
recordPumpFailure(error, "iterator cleanup failed");
|
|
112
|
+
}
|
|
113
|
+
if (!cancelled && !isAborted(cancellation.signal) && pumpFailure !== void 0) controller.enqueue(encodeError(pumpFailure.error));
|
|
114
|
+
if (!cancelled) controller.close();
|
|
115
|
+
}
|
|
116
|
+
})().catch((error) => {
|
|
117
|
+
recordPumpFailure(error, "stream pump failed");
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
async cancel(reason) {
|
|
121
|
+
cancelled = true;
|
|
122
|
+
if (detachOnCancel) {
|
|
123
|
+
onDetachedCancel?.();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!isAborted(cancellation.signal)) cancellation.abort(reason);
|
|
127
|
+
let cancellationFailure;
|
|
128
|
+
try {
|
|
129
|
+
await closeIterator();
|
|
130
|
+
} catch (error) {
|
|
131
|
+
cancellationFailure = { error };
|
|
132
|
+
}
|
|
133
|
+
await pumpPromise;
|
|
134
|
+
if (pumpFailure !== void 0 && cancellationFailure !== void 0) throw combineFailures(pumpFailure.error, cancellationFailure.error, "iterator cancellation failed");
|
|
135
|
+
if (pumpFailure !== void 0) throw pumpFailure.error;
|
|
136
|
+
if (cancellationFailure !== void 0) throw cancellationFailure.error;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Convert a StreamChunk async iterable to a ReadableStream in Server-Sent Events format
|
|
142
|
+
*
|
|
143
|
+
* This creates a ReadableStream that emits chunks in SSE format:
|
|
144
|
+
* - Each chunk is prefixed with "data: "
|
|
145
|
+
* - Each chunk is followed by "\n\n"
|
|
146
|
+
* - Stream ends when the underlying iterable is exhausted (RUN_FINISHED is the terminal event)
|
|
147
|
+
*
|
|
148
|
+
* @param stream - AsyncIterable of StreamChunks from chat()
|
|
149
|
+
* @param abortController - Optional AbortController to abort when stream is cancelled
|
|
150
|
+
* @param getId - Optional per-chunk durability offset; when present, each event gets an `id:` line
|
|
151
|
+
* @returns ReadableStream in Server-Sent Events format
|
|
152
|
+
*/
|
|
153
|
+
function toServerSentEventsStream(stream, abortController, getId) {
|
|
154
|
+
const { encodeChunk, encodeError } = sseEncoders(getId);
|
|
155
|
+
return toEncodedStream(stream, abortController, encodeChunk, encodeError);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* SSE chunk/error encoders. Shared by the public {@link toServerSentEventsStream}
|
|
159
|
+
* and the internal durability branch (which additionally needs `toEncodedStream`'s
|
|
160
|
+
* private `detachOnCancel`), so the wire format stays identical for both.
|
|
161
|
+
*/
|
|
162
|
+
function sseEncoders(getId) {
|
|
163
|
+
const encoder = new TextEncoder();
|
|
164
|
+
return {
|
|
165
|
+
encodeChunk: (chunk, index) => {
|
|
166
|
+
const id = getId?.(chunk, index);
|
|
167
|
+
const idLine = id === void 0 ? "" : `id: ${id}\n`;
|
|
168
|
+
return encoder.encode(`${idLine}data: ${JSON.stringify(chunk)}\n\n`);
|
|
169
|
+
},
|
|
170
|
+
encodeError: (error) => encoder.encode(`data: ${JSON.stringify(runErrorChunk(error))}\n\n`)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** Default number of chunks buffered before a durability `append`. */
|
|
174
|
+
var DEFAULT_DURABILITY_BATCH = 32;
|
|
175
|
+
/**
|
|
176
|
+
* Resolve and validate the durability batch size. A non-positive-integer (0,
|
|
177
|
+
* negative, fractional, or `NaN`) is rejected rather than clamped: silently
|
|
178
|
+
* `Math.max(1, …)`-ing a `NaN` used to disable size-based flushing entirely
|
|
179
|
+
* (`length >= NaN` is always false), which is a subtle footgun.
|
|
180
|
+
*/
|
|
181
|
+
function resolveBatchSize(batch) {
|
|
182
|
+
if (batch === void 0) return DEFAULT_DURABILITY_BATCH;
|
|
183
|
+
if (!Number.isInteger(batch) || batch <= 0) throw new Error(`Invalid durability batch size: ${batch}. Must be a positive integer.`);
|
|
184
|
+
return batch;
|
|
52
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Boundaries at which the batching producer flushes early, regardless of the
|
|
188
|
+
* batch size — the run-start marker, terminal events, and tool-call ends.
|
|
189
|
+
* Flushing here keeps the durability log promptly consistent at semantically
|
|
190
|
+
* meaningful points.
|
|
191
|
+
*
|
|
192
|
+
* `RUN_STARTED` matters especially for one-shot activities (image, speech,
|
|
193
|
+
* transcription, summarize): they emit `RUN_STARTED`, then await the provider
|
|
194
|
+
* for seconds, then a terminal. Without flushing `RUN_STARTED` the log stays
|
|
195
|
+
* empty for the whole run, so a mount-time `joinRun` finds nothing and its
|
|
196
|
+
* empty-log deadline fast-fails as "run gone" — even though the run is alive.
|
|
197
|
+
* Flushing it immediately makes the run resumable from the instant it starts.
|
|
198
|
+
*/
|
|
199
|
+
function isDurabilityFlushBoundary(chunk) {
|
|
200
|
+
return chunk.type === "RUN_STARTED" || chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR" || chunk.type === "TOOL_CALL_END";
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Name of the synthetic `CUSTOM` chunk a fresh durable producer appends to its
|
|
204
|
+
* log before pulling the first real chunk.
|
|
205
|
+
*
|
|
206
|
+
* Flushing `RUN_STARTED` (above) makes a run joinable from the instant the
|
|
207
|
+
* stream EMITS something — but a `chat()` whose middleware boots a sandbox
|
|
208
|
+
* (create a container, install a CLI) legitimately emits nothing for minutes,
|
|
209
|
+
* and during that window the log is empty. Every joiner's empty-log fail-fast
|
|
210
|
+
* (`memoryStream`'s first-chunk deadline, the client's rejoin connect deadline)
|
|
211
|
+
* then reads the run as gone — and the client clears its resume pointer, so a
|
|
212
|
+
* reload during the boot window permanently orphans a run that is still going.
|
|
213
|
+
*
|
|
214
|
+
* This marker closes the window: it is appended (and flushed) before the
|
|
215
|
+
* producer stream is first pulled, so a join always finds a first chunk within
|
|
216
|
+
* milliseconds of the run being accepted. Takeover alignment is unaffected — a
|
|
217
|
+
* journal replay cannot reproduce the marker, and alignment already skips
|
|
218
|
+
* stored `CUSTOM` chunks as out-of-band for exactly that reason (see
|
|
219
|
+
* `isBridgeCustomChunk` in `@tanstack/ai-sandbox`).
|
|
220
|
+
*/
|
|
221
|
+
var RUN_ACCEPTED_EVENT = "run.accepted";
|
|
222
|
+
/**
|
|
223
|
+
* Build the delivery-durable source iterable for a transport helper.
|
|
224
|
+
*
|
|
225
|
+
* - **Resume** (`resumeFrom()` non-null): replay strictly after the offset,
|
|
226
|
+
* reading only from the durability log. The input `stream` is NEVER iterated,
|
|
227
|
+
* so `chat()`'s lazy iterator never fires the provider — the untouched
|
|
228
|
+
* generator is simply GC'd. This is what makes resume free of re-invocation.
|
|
229
|
+
* - **Fresh** (`resumeFrom()` null): iterate `stream`, buffering up to `batch`
|
|
230
|
+
* chunks (flushing early at terminal / tool-call boundaries), `append` each
|
|
231
|
+
* batch to the log, then forward. Appending BEFORE forwarding guarantees a
|
|
232
|
+
* reconnecting client can always replay exactly what it already saw.
|
|
233
|
+
*
|
|
234
|
+
* The returned `getId` maps each forwarded chunk to the exact opaque offset
|
|
235
|
+
* returned by the durability adapter for the SSE `id:` line.
|
|
236
|
+
*/
|
|
237
|
+
function durableStreamSource(stream, durability, options) {
|
|
238
|
+
const resumeOffset = durability.resumeFrom();
|
|
239
|
+
const batchSize = resolveBatchSize(options.batch);
|
|
240
|
+
const abortController = options.abortController;
|
|
241
|
+
const logger = options.logger;
|
|
242
|
+
const idByChunk = /* @__PURE__ */ new WeakMap();
|
|
243
|
+
const seenOffsets = /* @__PURE__ */ new Set();
|
|
244
|
+
const getId = (chunk) => idByChunk.get(chunk);
|
|
245
|
+
const validateOffset = (offset) => {
|
|
246
|
+
if (offset.length === 0 || offset.includes("\0") || offset.includes("\r") || offset.includes("\n") || offset !== offset.trim()) throw new Error(`Invalid durability offset for SSE id: ${JSON.stringify(offset)}`);
|
|
247
|
+
if (seenOffsets.has(offset)) throw new Error(`Durability adapter must return a unique offset per chunk: ${JSON.stringify(offset)}`);
|
|
248
|
+
seenOffsets.add(offset);
|
|
249
|
+
};
|
|
250
|
+
async function* produce() {
|
|
251
|
+
let batch = [];
|
|
252
|
+
let terminalPersisted = false;
|
|
253
|
+
let terminalForwarded = false;
|
|
254
|
+
let failure;
|
|
255
|
+
let terminalCause;
|
|
256
|
+
let hasTerminalCause = false;
|
|
257
|
+
const recordFailure = (error, phase) => {
|
|
258
|
+
failure = { error: failure === void 0 ? error : combineFailures(failure.error, error, phase) };
|
|
259
|
+
};
|
|
260
|
+
async function* flush() {
|
|
261
|
+
if (batch.length === 0) return;
|
|
262
|
+
const toForward = batch;
|
|
263
|
+
batch = [];
|
|
264
|
+
const offsets = await durability.append(toForward);
|
|
265
|
+
if (offsets.length !== toForward.length) throw new Error(`Durability append returned ${offsets.length} offsets for ${toForward.length} chunks`);
|
|
266
|
+
toForward.forEach((chunk, i) => {
|
|
267
|
+
const offset = offsets[i];
|
|
268
|
+
if (offset === void 0) throw new Error(`Durability append omitted offset at index ${i}`);
|
|
269
|
+
validateOffset(offset);
|
|
270
|
+
idByChunk.set(chunk, offset);
|
|
271
|
+
});
|
|
272
|
+
if (toForward.some((chunk) => chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR")) terminalPersisted = true;
|
|
273
|
+
for (const chunk of toForward) {
|
|
274
|
+
if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") terminalForwarded = true;
|
|
275
|
+
yield chunk;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
if (isAborted(abortController.signal)) return;
|
|
280
|
+
batch.push({
|
|
281
|
+
type: "CUSTOM",
|
|
282
|
+
name: RUN_ACCEPTED_EVENT,
|
|
283
|
+
value: {},
|
|
284
|
+
timestamp: Date.now()
|
|
285
|
+
});
|
|
286
|
+
yield* flush();
|
|
287
|
+
for await (const chunk of stream) {
|
|
288
|
+
if (isAborted(abortController.signal)) break;
|
|
289
|
+
batch.push(chunk);
|
|
290
|
+
if (batch.length >= batchSize || isDurabilityFlushBoundary(chunk)) yield* flush();
|
|
291
|
+
}
|
|
292
|
+
if (!isAborted(abortController.signal)) yield* flush();
|
|
293
|
+
} catch (error) {
|
|
294
|
+
terminalCause = error;
|
|
295
|
+
hasTerminalCause = true;
|
|
296
|
+
recordFailure(error, "producer failed");
|
|
297
|
+
if (!isAborted(abortController.signal)) try {
|
|
298
|
+
yield* flush();
|
|
299
|
+
} catch (flushError) {
|
|
300
|
+
recordFailure(flushError, "flushing buffered chunks failed");
|
|
301
|
+
}
|
|
302
|
+
} finally {
|
|
303
|
+
const cancelled = isAborted(abortController.signal);
|
|
304
|
+
if (batch.length > 0) try {
|
|
305
|
+
for await (const _chunk of flush());
|
|
306
|
+
} catch (flushError) {
|
|
307
|
+
recordFailure(flushError, "flushing buffered chunks on exit failed");
|
|
308
|
+
}
|
|
309
|
+
const detached = cancelled && !isExplicitCancel(abortController.signal) && !hasTerminalCause && wasRunDetached(stream);
|
|
310
|
+
if (!detached && needsTerminalPersistence(terminalPersisted, cancelled, hasTerminalCause)) {
|
|
311
|
+
const cause = hasTerminalCause ? terminalCause : { name: "AbortError" };
|
|
312
|
+
try {
|
|
313
|
+
await durability.append([runErrorChunk(cause)]);
|
|
314
|
+
terminalPersisted = true;
|
|
315
|
+
} catch (terminalError) {
|
|
316
|
+
logger?.errors("persisting terminal RUN_ERROR failed", { error: terminalError });
|
|
317
|
+
recordFailure(terminalError, "persisting terminal RUN_ERROR failed");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (!detached) try {
|
|
321
|
+
await durability.close();
|
|
322
|
+
} catch (closeError) {
|
|
323
|
+
logger?.errors("closing durability stream failed", { error: closeError });
|
|
324
|
+
recordFailure(closeError, "closing durability stream failed");
|
|
325
|
+
}
|
|
326
|
+
if (failure !== void 0) {
|
|
327
|
+
if (!terminalForwarded) throw failure.error;
|
|
328
|
+
logger?.errors("durability failure after a terminal event was forwarded", { error: failure.error });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function* replay(offset) {
|
|
333
|
+
for await (const { offset: eventOffset, chunk } of durability.read(offset, abortController.signal)) {
|
|
334
|
+
if (isAborted(abortController.signal)) break;
|
|
335
|
+
validateOffset(eventOffset);
|
|
336
|
+
idByChunk.set(chunk, eventOffset);
|
|
337
|
+
yield chunk;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
source: resumeOffset !== null ? replay(resumeOffset) : produce(),
|
|
342
|
+
getId
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Convert a StreamChunk async iterable to a Response in Server-Sent Events format
|
|
347
|
+
*
|
|
348
|
+
* This creates a Response that emits chunks in SSE format:
|
|
349
|
+
* - Each chunk is prefixed with "data: "
|
|
350
|
+
* - Each chunk is followed by "\n\n"
|
|
351
|
+
* - Stream ends when the underlying iterable is exhausted (RUN_FINISHED is the terminal event)
|
|
352
|
+
*
|
|
353
|
+
* Pass a `durability` sink (`memoryStream(request)` / `durableStream(request)`)
|
|
354
|
+
* to make the stream resumable: fresh runs are appended to the log and each SSE
|
|
355
|
+
* event is tagged with an `id:` offset; a reconnect (native `Last-Event-ID`) or
|
|
356
|
+
* a `?offset` join replays from the log without re-running the producer. `batch`
|
|
357
|
+
* controls how many chunks are buffered per `append` (default 32).
|
|
358
|
+
*
|
|
359
|
+
* @param stream - AsyncIterable of StreamChunks from chat()
|
|
360
|
+
* @param init - Optional Response initialization options (including `abortController`, `durability` with its optional `batch`, and `debug`)
|
|
361
|
+
* @returns Response in Server-Sent Events format
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* ```typescript
|
|
365
|
+
* export async function POST(request: Request) {
|
|
366
|
+
* const stream = chat({ adapter: openaiText('gpt-5.5'), messages: [...] });
|
|
367
|
+
* return toServerSentEventsResponse(stream, { durability: { adapter: memoryStream(request) } });
|
|
368
|
+
* }
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
53
371
|
function toServerSentEventsResponse(stream, init) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
372
|
+
const { headers, abortController, durability, debug, ...responseInit } = init ?? {};
|
|
373
|
+
const mergedHeaders = new Headers({
|
|
374
|
+
"Content-Type": "text/event-stream",
|
|
375
|
+
"Cache-Control": "no-cache",
|
|
376
|
+
Connection: "keep-alive"
|
|
377
|
+
});
|
|
378
|
+
if (headers) new Headers(headers).forEach((value, key) => {
|
|
379
|
+
mergedHeaders.set(key, value);
|
|
380
|
+
});
|
|
381
|
+
let body;
|
|
382
|
+
if (durability) {
|
|
383
|
+
const isFresh = durability.adapter.resumeFrom() === null;
|
|
384
|
+
const producerAbortController = abortController ?? new AbortController();
|
|
385
|
+
const deliveryAbortController = isFresh ? new AbortController() : producerAbortController;
|
|
386
|
+
const { source, getId } = durableStreamSource(stream, durability.adapter, {
|
|
387
|
+
abortController: producerAbortController,
|
|
388
|
+
batch: durability.batch,
|
|
389
|
+
logger: resolveDebugOption(debug)
|
|
390
|
+
});
|
|
391
|
+
const { encodeChunk, encodeError } = sseEncoders(getId);
|
|
392
|
+
body = toEncodedStream(source, deliveryAbortController, encodeChunk, encodeError, isFresh, isFresh ? () => notifyRunDisconnected(stream) : void 0);
|
|
393
|
+
} else body = toServerSentEventsStream(stream, abortController);
|
|
394
|
+
return new Response(body, {
|
|
395
|
+
...responseInit,
|
|
396
|
+
headers: mergedHeaders
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* A resume is served entirely from the durability log, so there is no producer
|
|
401
|
+
* to iterate. This empty source satisfies the response helpers' signature; on a
|
|
402
|
+
* resume `durableStreamSource` replays from the log and never touches it.
|
|
403
|
+
*/
|
|
404
|
+
function emptyDurableSource() {
|
|
405
|
+
return (async function* () {})();
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Take over an in-flight run as a side effect of serving its log.
|
|
409
|
+
*
|
|
410
|
+
* The response itself is unchanged: it still replays from the durability log via
|
|
411
|
+
* `emptyDurableSource()`. The drive runs BESIDE it, appending to the run's own
|
|
412
|
+
* producer-side log through the injected `pipe`, and the response tails what
|
|
413
|
+
* lands. That separation is what lets a taken-over run keep `chat()`'s normal
|
|
414
|
+
* middleware path — `withPersistence.onFinish` is what saves the transcript, so a
|
|
415
|
+
* parallel translation path would lose the history of any run that completed
|
|
416
|
+
* while detached.
|
|
417
|
+
*
|
|
418
|
+
* TOTAL BY CONSTRUCTION. Every failure is logged and swallowed:
|
|
419
|
+
*
|
|
420
|
+
* - No run id, no record, or a terminal record → serve the log, drive nothing.
|
|
421
|
+
* A second tab attaching to a finished run must still see the transcript.
|
|
422
|
+
* - The claim is refused (another host is already driving) → serve the log,
|
|
423
|
+
* drive nothing. That is the documented "two hosts attach at once: one wins
|
|
424
|
+
* the lease and drives, the other tails the log" behavior.
|
|
425
|
+
* - The drive throws → logged. It cannot be reported to this response, which is
|
|
426
|
+
* already streaming the log; the run's own `RUN_ERROR` event is the channel.
|
|
427
|
+
*
|
|
428
|
+
* A rejection escaping here would be an unhandled rejection with nobody to
|
|
429
|
+
* report it to — process-fatal on modern Node and instance-fatal inside a
|
|
430
|
+
* Durable Object.
|
|
431
|
+
*/
|
|
432
|
+
function startRunDriver(driver) {
|
|
433
|
+
const logger = driver.logger;
|
|
434
|
+
const promise = (async () => {
|
|
435
|
+
const runId = resolveResumeRunId(driver.request);
|
|
436
|
+
if (runId === null) return;
|
|
437
|
+
let record = null;
|
|
438
|
+
try {
|
|
439
|
+
record = await driver.runs.get(runId);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
logger?.errors("resume driver: reading the run record failed", {
|
|
442
|
+
runId,
|
|
443
|
+
error
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (record !== null && !isRunStatus(record.status)) {
|
|
448
|
+
logger?.errors("resume driver: the run record has an unrecognized status", {
|
|
449
|
+
runId,
|
|
450
|
+
status: record.status
|
|
451
|
+
});
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (record === null || isTerminalRunStatus(record.status)) return;
|
|
455
|
+
if (record.cancelRequested === true) return;
|
|
456
|
+
const active = record;
|
|
457
|
+
try {
|
|
458
|
+
await driver.claim({
|
|
459
|
+
runs: driver.runs,
|
|
460
|
+
locks: driver.locks,
|
|
461
|
+
runId
|
|
462
|
+
}, async (claim) => {
|
|
463
|
+
try {
|
|
464
|
+
await driver.runs.update(runId, { detachedSince: void 0 });
|
|
465
|
+
} catch (error) {
|
|
466
|
+
logger?.errors("resume driver: clearing detachedSince failed", {
|
|
467
|
+
runId,
|
|
468
|
+
error
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
await driver.pipe(driver.drive({
|
|
472
|
+
runId,
|
|
473
|
+
threadId: active.threadId,
|
|
474
|
+
signal: claim.signal
|
|
475
|
+
}), {
|
|
476
|
+
runId,
|
|
477
|
+
threadId: active.threadId,
|
|
478
|
+
signal: claim.signal
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
} catch (error) {
|
|
482
|
+
logger?.provider("resume driver: not driving this run", {
|
|
483
|
+
runId,
|
|
484
|
+
error
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
})();
|
|
488
|
+
if (driver.waitUntil) driver.waitUntil(promise);
|
|
489
|
+
else promise.catch(() => {});
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* The single wiring point both resume helpers call, so the SSE and NDJSON
|
|
493
|
+
* halves cannot drift: a fix here applies to both. Called AFTER each helper's
|
|
494
|
+
* `resumeFrom() === null` 400 check — an attach with no offset has nothing to
|
|
495
|
+
* replay, and driving a run whose response will 400 would start an agent
|
|
496
|
+
* nobody is watching.
|
|
497
|
+
*/
|
|
498
|
+
function maybeStartRunDriver(driver) {
|
|
499
|
+
if (driver) startRunDriver(driver);
|
|
500
|
+
}
|
|
501
|
+
var NO_RESUME_OFFSET = "No resume offset provided (expected a Last-Event-ID header or an ?offset query parameter).";
|
|
502
|
+
/**
|
|
503
|
+
* Serve a resumable run from its durability log over Server-Sent Events, without
|
|
504
|
+
* re-running the model. Use this in a `GET` handler so a reload or a second tab
|
|
505
|
+
* can re-attach to an in-flight or finished run.
|
|
506
|
+
*
|
|
507
|
+
* The adapter (`memoryStream(request)` / `durableStream(request)`) captures the
|
|
508
|
+
* resume offset from the request. If there is none (no `Last-Event-ID` header
|
|
509
|
+
* and no `?offset`), there is nothing to replay and this returns a 400.
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* ```typescript
|
|
513
|
+
* export async function GET(request: Request) {
|
|
514
|
+
* return resumeServerSentEventsResponse({ adapter: memoryStream(request) });
|
|
515
|
+
* }
|
|
516
|
+
* ```
|
|
517
|
+
*/
|
|
518
|
+
function resumeServerSentEventsResponse(options) {
|
|
519
|
+
const { adapter, batch, debug, driver, ...responseInit } = options;
|
|
520
|
+
if (adapter.resumeFrom() === null) return new Response(NO_RESUME_OFFSET, { status: 400 });
|
|
521
|
+
maybeStartRunDriver(driver);
|
|
522
|
+
return toServerSentEventsResponse(emptyDurableSource(), {
|
|
523
|
+
...responseInit,
|
|
524
|
+
durability: {
|
|
525
|
+
adapter,
|
|
526
|
+
batch
|
|
527
|
+
},
|
|
528
|
+
debug
|
|
529
|
+
});
|
|
108
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Convert a StreamChunk async iterable to a ReadableStream in HTTP stream format (newline-delimited JSON)
|
|
533
|
+
*
|
|
534
|
+
* This creates a ReadableStream that emits chunks as newline-delimited JSON:
|
|
535
|
+
* - Each chunk is JSON.stringify'd and followed by "\n"
|
|
536
|
+
* - No SSE formatting (no "data: " prefix)
|
|
537
|
+
*
|
|
538
|
+
* This format is compatible with `fetchHttpStream` connection adapter.
|
|
539
|
+
*
|
|
540
|
+
* When `getId` is supplied (delivery durability), each chunk is emitted as an
|
|
541
|
+
* envelope `{"id":"<offset>","chunk":{…}}` instead of a bare chunk. NDJSON has
|
|
542
|
+
* no native event-id field like SSE's `id:` line, so the resumable offset rides
|
|
543
|
+
* inside the payload. Untagged chunks (no id) stay bare, so a non-durable
|
|
544
|
+
* stream is byte-identical to before and the client auto-detects either form.
|
|
545
|
+
*
|
|
546
|
+
* @param stream - AsyncIterable of StreamChunks from chat()
|
|
547
|
+
* @param abortController - Optional AbortController to abort when stream is cancelled
|
|
548
|
+
* @param getId - Optional per-chunk durability offset; when present, chunks are envelope-encoded
|
|
549
|
+
* @returns ReadableStream in HTTP stream format (newline-delimited JSON)
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* ```typescript
|
|
553
|
+
* const stream = chat({ adapter: openaiText('gpt-5.5'), messages: [...] });
|
|
554
|
+
* const readableStream = toHttpStream(stream);
|
|
555
|
+
* // Use with Response for HTTP streaming (not SSE)
|
|
556
|
+
* return new Response(readableStream, {
|
|
557
|
+
* headers: { 'Content-Type': 'application/x-ndjson' }
|
|
558
|
+
* });
|
|
559
|
+
* ```
|
|
560
|
+
*/
|
|
561
|
+
function toHttpStream(stream, abortController, getId) {
|
|
562
|
+
const { encodeChunk, encodeError } = ndjsonEncoders(getId);
|
|
563
|
+
return toEncodedStream(stream, abortController, encodeChunk, encodeError);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* NDJSON chunk/error encoders. Shared by {@link toHttpStream} and the internal
|
|
567
|
+
* durability branch (see {@link sseEncoders}).
|
|
568
|
+
*/
|
|
569
|
+
function ndjsonEncoders(getId) {
|
|
570
|
+
const encoder = new TextEncoder();
|
|
571
|
+
return {
|
|
572
|
+
encodeChunk: (chunk, index) => {
|
|
573
|
+
const id = getId?.(chunk, index);
|
|
574
|
+
const line = id === void 0 ? JSON.stringify(chunk) : JSON.stringify({
|
|
575
|
+
id,
|
|
576
|
+
chunk
|
|
577
|
+
});
|
|
578
|
+
return encoder.encode(`${line}\n`);
|
|
579
|
+
},
|
|
580
|
+
encodeError: (error) => encoder.encode(`${JSON.stringify(runErrorChunk(error))}\n`)
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Convert a StreamChunk async iterable to a Response in HTTP stream format (newline-delimited JSON)
|
|
585
|
+
*
|
|
586
|
+
* This creates a Response that emits chunks in HTTP stream format:
|
|
587
|
+
* - Each chunk is JSON.stringify'd and followed by "\n"
|
|
588
|
+
* - No SSE formatting (no "data: " prefix)
|
|
589
|
+
*
|
|
590
|
+
* This format is compatible with `fetchHttpStream` connection adapter.
|
|
591
|
+
*
|
|
592
|
+
* Pass a `durability` sink (`memoryStream(request)` / `durableStream(request)`)
|
|
593
|
+
* to make the stream resumable: fresh runs are appended to the log and each
|
|
594
|
+
* NDJSON line is emitted as an `{ id, chunk }` envelope carrying an opaque
|
|
595
|
+
* offset; a reconnect (native `Last-Event-ID` header) or a `?offset` join
|
|
596
|
+
* replays from the log without re-running the producer. `batch` controls how
|
|
597
|
+
* many chunks are buffered per `append` (default 32). This shares the exact
|
|
598
|
+
* `durableStreamSource` used by `toServerSentEventsResponse` — only the wire
|
|
599
|
+
* encoding differs.
|
|
600
|
+
*
|
|
601
|
+
* @param stream - AsyncIterable of StreamChunks from chat()
|
|
602
|
+
* @param init - Optional Response initialization options (including `abortController`, `durability` with its optional `batch`, and `debug`)
|
|
603
|
+
* @returns Response in HTTP stream format (newline-delimited JSON)
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```typescript
|
|
607
|
+
* export async function POST(request: Request) {
|
|
608
|
+
* const stream = chat({ adapter: openaiText('gpt-5.5'), messages: [...] });
|
|
609
|
+
* return toHttpResponse(stream, { durability: { adapter: memoryStream(request) } });
|
|
610
|
+
* }
|
|
611
|
+
* ```
|
|
612
|
+
*/
|
|
109
613
|
function toHttpResponse(stream, init) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
614
|
+
const { abortController, durability, debug, headers, ...responseInit } = init ?? {};
|
|
615
|
+
const mergedHeaders = new Headers({
|
|
616
|
+
"Content-Type": "application/x-ndjson",
|
|
617
|
+
"Cache-Control": "no-cache"
|
|
618
|
+
});
|
|
619
|
+
if (headers) new Headers(headers).forEach((value, key) => {
|
|
620
|
+
mergedHeaders.set(key, value);
|
|
621
|
+
});
|
|
622
|
+
let body;
|
|
623
|
+
if (durability) {
|
|
624
|
+
const isFresh = durability.adapter.resumeFrom() === null;
|
|
625
|
+
const producerAbortController = abortController ?? new AbortController();
|
|
626
|
+
const deliveryAbortController = isFresh ? new AbortController() : producerAbortController;
|
|
627
|
+
const { source, getId } = durableStreamSource(stream, durability.adapter, {
|
|
628
|
+
abortController: producerAbortController,
|
|
629
|
+
batch: durability.batch,
|
|
630
|
+
logger: resolveDebugOption(debug)
|
|
631
|
+
});
|
|
632
|
+
const { encodeChunk, encodeError } = ndjsonEncoders(getId);
|
|
633
|
+
body = toEncodedStream(source, deliveryAbortController, encodeChunk, encodeError, isFresh, isFresh ? () => notifyRunDisconnected(stream) : void 0);
|
|
634
|
+
} else body = toHttpStream(stream, abortController);
|
|
635
|
+
return new Response(body, {
|
|
636
|
+
...responseInit,
|
|
637
|
+
headers: mergedHeaders
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Serve a resumable run from its durability log over NDJSON, without re-running
|
|
642
|
+
* the model. The NDJSON counterpart of {@link resumeServerSentEventsResponse};
|
|
643
|
+
* pair it with a `toHttpResponse` producer. Returns a 400 when the request
|
|
644
|
+
* carries no resume offset (no `Last-Event-ID` header and no `?offset`).
|
|
645
|
+
*
|
|
646
|
+
* @example
|
|
647
|
+
* ```typescript
|
|
648
|
+
* export async function GET(request: Request) {
|
|
649
|
+
* return resumeHttpResponse({ adapter: memoryStream(request) });
|
|
650
|
+
* }
|
|
651
|
+
* ```
|
|
652
|
+
*/
|
|
653
|
+
function resumeHttpResponse(options) {
|
|
654
|
+
const { adapter, batch, debug, driver, ...responseInit } = options;
|
|
655
|
+
if (adapter.resumeFrom() === null) return new Response(NO_RESUME_OFFSET, { status: 400 });
|
|
656
|
+
maybeStartRunDriver(driver);
|
|
657
|
+
return toHttpResponse(emptyDurableSource(), {
|
|
658
|
+
...responseInit,
|
|
659
|
+
durability: {
|
|
660
|
+
adapter,
|
|
661
|
+
batch
|
|
662
|
+
},
|
|
663
|
+
debug
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
//#endregion
|
|
667
|
+
export { RUN_ACCEPTED_EVENT, resolveResumeRunId, resumeHttpResponse, resumeServerSentEventsResponse, streamToText, toHttpResponse, toHttpStream, toServerSentEventsResponse, toServerSentEventsStream };
|
|
668
|
+
|
|
669
|
+
//# sourceMappingURL=stream-to-response.js.map
|