@convex-dev/agent 0.6.3 → 0.7.0-alpha.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/MIGRATION.md +60 -116
- package/dist/client/messages.d.ts +1 -398
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +3 -47
- package/dist/client/messages.js.map +1 -1
- package/dist/client/types.d.ts +1 -455
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +805 -45
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/apiKeys.js +5 -5
- package/dist/component/apiKeys.js.map +1 -1
- package/dist/component/files.d.ts.map +1 -1
- package/dist/component/files.js +13 -11
- package/dist/component/files.js.map +1 -1
- package/dist/component/messages.d.ts +1252 -201
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +83 -47
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +2593 -332
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/streams.d.ts +7 -1
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +46 -47
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts.map +1 -1
- package/dist/component/threads.js +8 -9
- package/dist/component/threads.js.map +1 -1
- package/dist/component/users.js +2 -2
- package/dist/component/users.js.map +1 -1
- package/dist/component/vector/index.d.ts.map +1 -1
- package/dist/component/vector/index.js +14 -8
- package/dist/component/vector/index.js.map +1 -1
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -5
- package/dist/react/index.js.map +1 -1
- package/dist/shared.d.ts +19 -7
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +4 -4
- package/dist/shared.js.map +1 -1
- package/dist/streaming/materializePersistedUIMessageChunks.d.ts +16 -0
- package/dist/streaming/materializePersistedUIMessageChunks.d.ts.map +1 -0
- package/dist/streaming/materializePersistedUIMessageChunks.js +302 -0
- package/dist/streaming/materializePersistedUIMessageChunks.js.map +1 -0
- package/dist/streaming/persistedUIMessageChunks.d.ts +106 -0
- package/dist/streaming/persistedUIMessageChunks.d.ts.map +1 -0
- package/dist/streaming/persistedUIMessageChunks.js +475 -0
- package/dist/streaming/persistedUIMessageChunks.js.map +1 -0
- package/dist/validators.d.ts +10026 -1055
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +71 -9
- package/dist/validators.js.map +1 -1
- package/dist/{UIMessages.d.ts → vercel/UIMessages.d.ts} +1 -1
- package/dist/vercel/UIMessages.d.ts.map +1 -0
- package/dist/{UIMessages.js → vercel/UIMessages.js} +63 -1
- package/dist/vercel/UIMessages.js.map +1 -0
- package/dist/{client → vercel/client}/createTool.d.ts +23 -16
- package/dist/vercel/client/createTool.d.ts.map +1 -0
- package/dist/{client → vercel/client}/createTool.js +2 -0
- package/dist/vercel/client/createTool.js.map +1 -0
- package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts +593 -134
- package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/{client → vercel/client}/definePlaygroundAPI.js +13 -9
- package/dist/vercel/client/definePlaygroundAPI.js.map +1 -0
- package/dist/{client → vercel/client}/files.d.ts +2 -2
- package/dist/vercel/client/files.d.ts.map +1 -0
- package/dist/vercel/client/files.js.map +1 -0
- package/dist/vercel/client/messages.d.ts +542 -0
- package/dist/vercel/client/messages.d.ts.map +1 -0
- package/dist/vercel/client/messages.js +66 -0
- package/dist/vercel/client/messages.js.map +1 -0
- package/dist/{client → vercel/client}/mockModel.d.ts +17 -17
- package/dist/vercel/client/mockModel.d.ts.map +1 -0
- package/dist/{client → vercel/client}/mockModel.js +12 -13
- package/dist/vercel/client/mockModel.js.map +1 -0
- package/dist/{client → vercel/client}/saveInputMessages.d.ts +2 -2
- package/dist/vercel/client/saveInputMessages.d.ts.map +1 -0
- package/dist/vercel/client/saveInputMessages.js.map +1 -0
- package/dist/{client → vercel/client}/search.d.ts +119 -22
- package/dist/vercel/client/search.d.ts.map +1 -0
- package/dist/{client → vercel/client}/search.js +2 -2
- package/dist/vercel/client/search.js.map +1 -0
- package/dist/{client → vercel/client}/start.d.ts +26 -11
- package/dist/vercel/client/start.d.ts.map +1 -0
- package/dist/{client → vercel/client}/start.js +64 -22
- package/dist/vercel/client/start.js.map +1 -0
- package/dist/{client → vercel/client}/streamText.d.ts +13 -12
- package/dist/vercel/client/streamText.d.ts.map +1 -0
- package/dist/{client → vercel/client}/streamText.js +43 -9
- package/dist/vercel/client/streamText.js.map +1 -0
- package/dist/{client → vercel/client}/streaming.d.ts +1410 -183
- package/dist/vercel/client/streaming.d.ts.map +1 -0
- package/dist/{client → vercel/client}/streaming.js +120 -52
- package/dist/vercel/client/streaming.js.map +1 -0
- package/dist/vercel/client/types.d.ts +456 -0
- package/dist/vercel/client/types.d.ts.map +1 -0
- package/dist/vercel/client/types.js +3 -0
- package/dist/vercel/client/types.js.map +1 -0
- package/dist/{client → vercel/client}/utils.d.ts +3 -2
- package/dist/vercel/client/utils.d.ts.map +1 -0
- package/dist/{client → vercel/client}/utils.js +2 -2
- package/dist/vercel/client/utils.js.map +1 -0
- package/dist/vercel/deltas.d.ts +31 -0
- package/dist/vercel/deltas.d.ts.map +1 -0
- package/dist/vercel/deltas.js +525 -0
- package/dist/vercel/deltas.js.map +1 -0
- package/dist/{client → vercel}/index.d.ts +484 -97
- package/dist/vercel/index.d.ts.map +1 -0
- package/dist/{client → vercel}/index.js +65 -38
- package/dist/vercel/index.js.map +1 -0
- package/dist/{mapping.d.ts → vercel/mapping.d.ts} +5 -13
- package/dist/vercel/mapping.d.ts.map +1 -0
- package/dist/{mapping.js → vercel/mapping.js} +411 -112
- package/dist/vercel/mapping.js.map +1 -0
- package/dist/{react → vercel/react}/optimisticallySendMessage.d.ts +2 -2
- package/dist/vercel/react/optimisticallySendMessage.d.ts.map +1 -0
- package/dist/vercel/react/optimisticallySendMessage.js.map +1 -0
- package/dist/{react → vercel/react}/types.d.ts +1 -1
- package/dist/vercel/react/types.d.ts.map +1 -0
- package/dist/{react → vercel/react}/types.js.map +1 -1
- package/dist/{react → vercel/react}/useDeltaStreams.d.ts +1 -1
- package/dist/vercel/react/useDeltaStreams.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useDeltaStreams.js +1 -1
- package/dist/vercel/react/useDeltaStreams.js.map +1 -0
- package/dist/{react → vercel/react}/useStreamingUIMessages.d.ts +1 -1
- package/dist/vercel/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/vercel/react/useStreamingUIMessages.js +112 -0
- package/dist/vercel/react/useStreamingUIMessages.js.map +1 -0
- package/dist/{react → vercel/react}/useThreadMessages.d.ts +1 -1
- package/dist/vercel/react/useThreadMessages.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useThreadMessages.js +1 -1
- package/dist/vercel/react/useThreadMessages.js.map +1 -0
- package/dist/{react → vercel/react}/useUIMessages.d.ts +2 -1
- package/dist/vercel/react/useUIMessages.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useUIMessages.js +8 -4
- package/dist/vercel/react/useUIMessages.js.map +1 -0
- package/package.json +30 -22
- package/src/client/messages.ts +5 -107
- package/src/client/types.ts +0 -621
- package/src/component/_generated/component.ts +1089 -48
- package/src/component/apiKeys.ts +5 -5
- package/src/component/files.test.ts +1 -1
- package/src/component/files.ts +14 -12
- package/src/component/messages.test.ts +273 -1
- package/src/component/messages.ts +99 -57
- package/src/component/streams.ts +80 -64
- package/src/component/threads.ts +12 -9
- package/src/component/users.ts +2 -2
- package/src/component/vector/index.ts +14 -7
- package/src/eslintConfig.test.ts +44 -0
- package/src/react/index.ts +8 -5
- package/src/shared.ts +26 -36
- package/src/streaming/materializePersistedUIMessageChunks.test.ts +513 -0
- package/src/streaming/materializePersistedUIMessageChunks.ts +398 -0
- package/src/streaming/persistedUIMessageChunks.test.ts +205 -0
- package/src/streaming/persistedUIMessageChunks.ts +658 -0
- package/src/validators.test.ts +4 -1
- package/src/validators.ts +89 -8
- package/src/{UIMessages.combineUIMessages.test.ts → vercel/UIMessages.combineUIMessages.test.ts} +4 -1
- package/src/{UIMessages.ts → vercel/UIMessages.ts} +96 -5
- package/src/{client → vercel/client}/approval.test.ts +48 -19
- package/src/{client → vercel/client}/createTool.ts +63 -23
- package/src/{client → vercel/client}/definePlaygroundAPI.ts +44 -29
- package/src/{client → vercel/client}/files.ts +2 -2
- package/src/{client → vercel/client}/index.test.ts +162 -3
- package/src/vercel/client/messages.ts +182 -0
- package/src/{client → vercel/client}/mockModel.ts +38 -39
- package/src/{client → vercel/client}/saveInputMessages.test.ts +1 -1
- package/src/{client → vercel/client}/saveInputMessages.ts +7 -4
- package/src/{client → vercel/client}/search.test.ts +3 -3
- package/src/{client → vercel/client}/search.ts +4 -4
- package/src/{client → vercel/client}/setup.test.ts +6 -3
- package/src/vercel/client/start.test.ts +14 -0
- package/src/{client → vercel/client}/start.ts +147 -36
- package/src/vercel/client/streamText.test.ts +223 -0
- package/src/{client → vercel/client}/streamText.ts +102 -31
- package/src/{client → vercel/client}/streaming.integration.test.ts +40 -115
- package/src/{client → vercel/client}/streaming.test.ts +178 -4
- package/src/{client → vercel/client}/streaming.ts +114 -52
- package/src/vercel/client/types.test.ts +13 -0
- package/src/vercel/client/types.ts +679 -0
- package/src/{client → vercel/client}/utils.ts +13 -7
- package/src/vercel/deltas.test.ts +898 -0
- package/src/vercel/deltas.ts +633 -0
- package/src/{fromUIMessages.test.ts → vercel/fromUIMessages.test.ts} +17 -1
- package/src/{client → vercel}/index.ts +169 -62
- package/src/vercel/mapping.test.ts +965 -0
- package/src/{mapping.ts → vercel/mapping.ts} +501 -131
- package/src/{react → vercel/react}/optimisticallySendMessage.ts +2 -2
- package/src/{react → vercel/react}/types.ts +1 -1
- package/src/{react → vercel/react}/useDeltaStreams.ts +6 -2
- package/src/{react → vercel/react}/useStreamingUIMessages.ts +67 -35
- package/src/{react → vercel/react}/useThreadMessages.ts +2 -2
- package/src/{react → vercel/react}/useUIMessages.test.ts +80 -1
- package/src/{react → vercel/react}/useUIMessages.ts +13 -5
- package/src/{toUIMessages.test.ts → vercel/toUIMessages.test.ts} +49 -2
- package/dist/UIMessages.d.ts.map +0 -1
- package/dist/UIMessages.js.map +0 -1
- package/dist/client/createTool.d.ts.map +0 -1
- package/dist/client/createTool.js.map +0 -1
- package/dist/client/definePlaygroundAPI.js.map +0 -1
- package/dist/client/files.d.ts.map +0 -1
- package/dist/client/files.js.map +0 -1
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/index.js.map +0 -1
- package/dist/client/mockModel.d.ts.map +0 -1
- package/dist/client/mockModel.js.map +0 -1
- package/dist/client/saveInputMessages.d.ts.map +0 -1
- package/dist/client/saveInputMessages.js.map +0 -1
- package/dist/client/search.d.ts.map +0 -1
- package/dist/client/search.js.map +0 -1
- package/dist/client/start.d.ts.map +0 -1
- package/dist/client/start.js.map +0 -1
- package/dist/client/streamText.d.ts.map +0 -1
- package/dist/client/streamText.js.map +0 -1
- package/dist/client/streaming.d.ts.map +0 -1
- package/dist/client/streaming.js.map +0 -1
- package/dist/client/utils.d.ts.map +0 -1
- package/dist/client/utils.js.map +0 -1
- package/dist/deltas.d.ts +0 -43
- package/dist/deltas.d.ts.map +0 -1
- package/dist/deltas.js +0 -446
- package/dist/deltas.js.map +0 -1
- package/dist/mapping.d.ts.map +0 -1
- package/dist/mapping.js.map +0 -1
- package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
- package/dist/react/optimisticallySendMessage.js.map +0 -1
- package/dist/react/types.d.ts.map +0 -1
- package/dist/react/useDeltaStreams.d.ts.map +0 -1
- package/dist/react/useDeltaStreams.js.map +0 -1
- package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
- package/dist/react/useStreamingUIMessages.js +0 -92
- package/dist/react/useStreamingUIMessages.js.map +0 -1
- package/dist/react/useThreadMessages.d.ts.map +0 -1
- package/dist/react/useThreadMessages.js.map +0 -1
- package/dist/react/useUIMessages.d.ts.map +0 -1
- package/dist/react/useUIMessages.js.map +0 -1
- package/src/client/streamText.test.ts +0 -102
- package/src/deltas.test.ts +0 -626
- package/src/deltas.ts +0 -569
- package/src/mapping.test.ts +0 -402
- /package/dist/{client → vercel/client}/files.js +0 -0
- /package/dist/{client → vercel/client}/saveInputMessages.js +0 -0
- /package/dist/{react → vercel/react}/optimisticallySendMessage.js +0 -0
- /package/dist/{react → vercel/react}/types.js +0 -0
- /package/src/{UIMessages.test.ts → vercel/UIMessages.test.ts} +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ModelMessage,
|
|
2
3
|
StepResult,
|
|
3
4
|
StreamTextResult,
|
|
4
5
|
ToolSet,
|
|
5
6
|
UIMessage as AIUIMessage,
|
|
6
7
|
} from "ai";
|
|
8
|
+
import type { Context } from "@ai-sdk/provider-utils";
|
|
7
9
|
import { streamText as streamTextAi } from "ai";
|
|
8
10
|
import {
|
|
9
11
|
compressUIMessageChunks,
|
|
@@ -14,16 +16,31 @@ import {
|
|
|
14
16
|
import type {
|
|
15
17
|
ActionCtx,
|
|
16
18
|
AgentComponent,
|
|
17
|
-
AgentPrompt,
|
|
18
19
|
GenerationOutputMetadata,
|
|
19
20
|
Options,
|
|
20
|
-
|
|
21
|
+
StreamingTextArgs,
|
|
21
22
|
} from "./types.js";
|
|
23
|
+
import type { Output as AISDKOutput } from "ai";
|
|
22
24
|
import { startGeneration } from "./start.js";
|
|
23
|
-
import type { Agent } from "
|
|
24
|
-
import { getModelName, getProviderName } from "
|
|
25
|
+
import type { Agent } from "../index.js";
|
|
26
|
+
import { getModelName, getProviderName } from "../../shared.js";
|
|
25
27
|
import { errorToString, willContinue } from "./utils.js";
|
|
26
28
|
|
|
29
|
+
/** Finish every abort cleanup path before surfacing an internal failure. */
|
|
30
|
+
export async function runAbortCleanup(cleanup: {
|
|
31
|
+
failCall: () => Promise<void>;
|
|
32
|
+
failStreamer: () => Promise<void>;
|
|
33
|
+
onAbort?: () => PromiseLike<void> | void;
|
|
34
|
+
}): Promise<void> {
|
|
35
|
+
const results = await Promise.allSettled([
|
|
36
|
+
cleanup.failCall(),
|
|
37
|
+
cleanup.failStreamer(),
|
|
38
|
+
]);
|
|
39
|
+
await cleanup.onAbort?.();
|
|
40
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
41
|
+
if (failure) throw failure.reason;
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
/**
|
|
28
45
|
* This behaves like {@link streamText} from the "ai" package except that
|
|
29
46
|
* it add context based on the userId and threadId and saves the input and
|
|
@@ -32,8 +49,14 @@ import { errorToString, willContinue } from "./utils.js";
|
|
|
32
49
|
* to a thread (and optionally userId).
|
|
33
50
|
*/
|
|
34
51
|
export async function streamText<
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
AgentTools extends ToolSet,
|
|
53
|
+
TOOLS extends ToolSet | undefined = undefined,
|
|
54
|
+
OUTPUT extends AISDKOutput.Output<any, any, any> = AISDKOutput.Output<
|
|
55
|
+
string,
|
|
56
|
+
string,
|
|
57
|
+
never
|
|
58
|
+
>,
|
|
59
|
+
RUNTIME_CONTEXT extends Context = Context,
|
|
37
60
|
>(
|
|
38
61
|
ctx: ActionCtx,
|
|
39
62
|
component: AgentComponent,
|
|
@@ -41,17 +64,7 @@ export async function streamText<
|
|
|
41
64
|
* The arguments to the streamText function, similar to the ai sdk's
|
|
42
65
|
* {@link streamText} function, along with Agent prompt options.
|
|
43
66
|
*/
|
|
44
|
-
streamTextArgs:
|
|
45
|
-
Omit<
|
|
46
|
-
Parameters<typeof streamTextAi<TOOLS, OUTPUT>>[0],
|
|
47
|
-
"model" | "prompt" | "messages"
|
|
48
|
-
> & {
|
|
49
|
-
/**
|
|
50
|
-
* The tools to use for the tool calls. This will override tools specified
|
|
51
|
-
* in the Agent constructor or createThread / continueThread.
|
|
52
|
-
*/
|
|
53
|
-
tools?: TOOLS;
|
|
54
|
-
},
|
|
67
|
+
streamTextArgs: StreamingTextArgs<AgentTools, TOOLS, OUTPUT, RUNTIME_CONTEXT>,
|
|
55
68
|
/**
|
|
56
69
|
* The {@link ContextOptions} and {@link StorageOptions}
|
|
57
70
|
* options to use for fetching contextual messages and saving input/output messages.
|
|
@@ -73,17 +86,49 @@ export async function streamText<
|
|
|
73
86
|
saveStreamDeltas?: boolean | StreamingOptions;
|
|
74
87
|
agentForToolCtx?: Agent;
|
|
75
88
|
},
|
|
76
|
-
): Promise<
|
|
89
|
+
): Promise<
|
|
90
|
+
StreamTextResult<
|
|
91
|
+
TOOLS extends undefined ? AgentTools : TOOLS,
|
|
92
|
+
RUNTIME_CONTEXT,
|
|
93
|
+
OUTPUT
|
|
94
|
+
> &
|
|
95
|
+
GenerationOutputMetadata
|
|
96
|
+
> {
|
|
97
|
+
type Tools = TOOLS extends undefined ? AgentTools : TOOLS;
|
|
77
98
|
const { threadId } = options ?? {};
|
|
78
99
|
const { args, userId, order, stepOrder, promptMessageId, ...call } =
|
|
79
|
-
await startGeneration
|
|
100
|
+
await startGeneration<
|
|
101
|
+
StreamingTextArgs<AgentTools, TOOLS, OUTPUT, RUNTIME_CONTEXT>,
|
|
102
|
+
Tools,
|
|
103
|
+
object,
|
|
104
|
+
RUNTIME_CONTEXT
|
|
105
|
+
>(
|
|
106
|
+
ctx,
|
|
107
|
+
component,
|
|
108
|
+
streamTextArgs,
|
|
109
|
+
options,
|
|
110
|
+
"streamText",
|
|
111
|
+
);
|
|
80
112
|
|
|
81
|
-
const steps: StepResult<
|
|
113
|
+
const steps: StepResult<Tools, RUNTIME_CONTEXT>[] = [];
|
|
114
|
+
let initialResponseMessages: ModelMessage[] = [];
|
|
115
|
+
let initialResponseMessagesSaved = false;
|
|
116
|
+
const responseMessagesForStep = (
|
|
117
|
+
step: StepResult<Tools, RUNTIME_CONTEXT>,
|
|
118
|
+
) => [
|
|
119
|
+
...(initialResponseMessagesSaved ? [] : initialResponseMessages),
|
|
120
|
+
...step.response.messages,
|
|
121
|
+
];
|
|
82
122
|
|
|
83
123
|
// Track the final step for atomic save with stream finish (issue #181).
|
|
84
124
|
// Only used when streamText awaits stream consumption itself; the
|
|
85
125
|
// `returnImmediately` path saves inline instead (see onStepFinish below).
|
|
86
|
-
let pendingFinalStep:
|
|
126
|
+
let pendingFinalStep:
|
|
127
|
+
| {
|
|
128
|
+
step: StepResult<Tools, RUNTIME_CONTEXT>;
|
|
129
|
+
responseMessages: ModelMessage[];
|
|
130
|
+
}
|
|
131
|
+
| undefined;
|
|
87
132
|
|
|
88
133
|
// Whether streamText will await stream consumption before returning.
|
|
89
134
|
// When false (saveStreamDeltas.returnImmediately === true), we cannot
|
|
@@ -123,7 +168,7 @@ export async function streamText<
|
|
|
123
168
|
)
|
|
124
169
|
: undefined;
|
|
125
170
|
|
|
126
|
-
const result = streamTextAi({
|
|
171
|
+
const result = streamTextAi<Tools, RUNTIME_CONTEXT, OUTPUT>({
|
|
127
172
|
...args,
|
|
128
173
|
abortSignal: streamer?.abortController.signal ?? args.abortSignal,
|
|
129
174
|
experimental_transform: mergeTransforms(
|
|
@@ -136,7 +181,19 @@ export async function streamText<
|
|
|
136
181
|
await streamer?.fail(errorToString(error.error));
|
|
137
182
|
return streamTextArgs.onError?.(error);
|
|
138
183
|
},
|
|
184
|
+
onAbort: async (event) => {
|
|
185
|
+
const reason = args.abortSignal?.reason
|
|
186
|
+
? errorToString(args.abortSignal.reason)
|
|
187
|
+
: "streamText aborted";
|
|
188
|
+
await runAbortCleanup({
|
|
189
|
+
failCall: () => call.fail(reason),
|
|
190
|
+
failStreamer: async () => streamer?.fail(reason),
|
|
191
|
+
onAbort: () => streamTextArgs.onAbort?.(event),
|
|
192
|
+
});
|
|
193
|
+
},
|
|
139
194
|
prepareStep: async (options) => {
|
|
195
|
+
if (options.stepNumber === 0)
|
|
196
|
+
initialResponseMessages = [...options.responseMessages];
|
|
140
197
|
const result = await streamTextArgs.prepareStep?.(options);
|
|
141
198
|
if (result) {
|
|
142
199
|
const model = result.model ?? options.model;
|
|
@@ -150,7 +207,7 @@ export async function streamText<
|
|
|
150
207
|
}
|
|
151
208
|
return undefined;
|
|
152
209
|
},
|
|
153
|
-
|
|
210
|
+
onStepEnd: async (step) => {
|
|
154
211
|
steps.push(step);
|
|
155
212
|
const createPendingMessage = await willContinue(steps, args.stopWhen);
|
|
156
213
|
if (!createPendingMessage && streamer) {
|
|
@@ -159,22 +216,36 @@ export async function streamText<
|
|
|
159
216
|
if (willAwaitStream) {
|
|
160
217
|
// We're about to `await stream` below — defer the save so it
|
|
161
218
|
// happens atomically with stream finish (issue #181).
|
|
162
|
-
pendingFinalStep =
|
|
219
|
+
pendingFinalStep = {
|
|
220
|
+
step,
|
|
221
|
+
responseMessages: responseMessagesForStep(step),
|
|
222
|
+
};
|
|
163
223
|
} else {
|
|
164
224
|
// returnImmediately path: streamText is about to return without
|
|
165
225
|
// awaiting consumption, so the deferred-save block below won't
|
|
166
226
|
// see this step. Save inline now (issue #265).
|
|
167
227
|
const finishStreamId = await streamer.getOrCreateStreamId();
|
|
168
|
-
await call.save(
|
|
228
|
+
await call.save(
|
|
229
|
+
{ step, responseMessages: responseMessagesForStep(step) },
|
|
230
|
+
false,
|
|
231
|
+
finishStreamId,
|
|
232
|
+
);
|
|
233
|
+
initialResponseMessagesSaved = true;
|
|
169
234
|
}
|
|
170
235
|
} else {
|
|
171
|
-
await call.save(
|
|
236
|
+
await call.save(
|
|
237
|
+
{ step, responseMessages: responseMessagesForStep(step) },
|
|
238
|
+
createPendingMessage,
|
|
239
|
+
);
|
|
240
|
+
initialResponseMessagesSaved = true;
|
|
172
241
|
}
|
|
173
|
-
return
|
|
242
|
+
return (streamTextArgs.onStepEnd ?? streamTextArgs.onStepFinish)?.(step);
|
|
174
243
|
},
|
|
175
|
-
}
|
|
244
|
+
} as Parameters<
|
|
245
|
+
typeof streamTextAi<Tools, RUNTIME_CONTEXT, OUTPUT>
|
|
246
|
+
>[0]) as StreamTextResult<Tools, RUNTIME_CONTEXT, OUTPUT>;
|
|
176
247
|
const stream = streamer?.consumeStream(
|
|
177
|
-
result.toUIMessageStream<AIUIMessage<
|
|
248
|
+
result.toUIMessageStream<AIUIMessage<Tools>>(),
|
|
178
249
|
);
|
|
179
250
|
if (willAwaitStream) {
|
|
180
251
|
try {
|
|
@@ -188,7 +259,7 @@ export async function streamText<
|
|
|
188
259
|
// Save the deferred final step if it was already generated but not yet persisted
|
|
189
260
|
if (pendingFinalStep) {
|
|
190
261
|
try {
|
|
191
|
-
await call.save(
|
|
262
|
+
await call.save(pendingFinalStep, false);
|
|
192
263
|
} catch (saveError) {
|
|
193
264
|
console.error("Failed to save deferred final step:", saveError);
|
|
194
265
|
}
|
|
@@ -201,7 +272,7 @@ export async function streamText<
|
|
|
201
272
|
// If we deferred the final step save, do it now with atomic stream finish.
|
|
202
273
|
if (pendingFinalStep && streamer) {
|
|
203
274
|
const finishStreamId = await streamer.getOrCreateStreamId();
|
|
204
|
-
await call.save(
|
|
275
|
+
await call.save(pendingFinalStep, false, finishStreamId);
|
|
205
276
|
}
|
|
206
277
|
const metadata: GenerationOutputMetadata = {
|
|
207
278
|
promptMessageId,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, test } from "vitest";
|
|
2
|
-
import { createThread } from "
|
|
2
|
+
import { createThread } from "../../client/threads.js";
|
|
3
3
|
import type { GenericSchema, SchemaDefinition } from "convex/server";
|
|
4
4
|
import { streamText } from "ai";
|
|
5
5
|
import { components, initConvexTest } from "./setup.test.js";
|
|
@@ -12,10 +12,9 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
getParts,
|
|
14
14
|
deriveUIMessagesFromDeltas,
|
|
15
|
-
deriveUIMessagesFromTextStreamParts,
|
|
16
15
|
} from "../deltas.js";
|
|
17
16
|
import type { TestConvex } from "convex-test";
|
|
18
|
-
import type { StreamDelta, StreamMessage } from "
|
|
17
|
+
import type { StreamDelta, StreamMessage } from "../../validators.js";
|
|
19
18
|
import { dedupeMessages } from "../react/useUIMessages.js";
|
|
20
19
|
|
|
21
20
|
const defaultTestOptions = {
|
|
@@ -152,9 +151,7 @@ describe("HTTP Streaming Initiation", () => {
|
|
|
152
151
|
|
|
153
152
|
// Verify we can reconstruct the text from deltas
|
|
154
153
|
const { parts } = getParts(deltas);
|
|
155
|
-
const textParts = parts.filter(
|
|
156
|
-
(p: any) => p.type === "text-delta",
|
|
157
|
-
);
|
|
154
|
+
const textParts = parts.filter((p: any) => p.type === "text-delta");
|
|
158
155
|
expect(textParts.length).toBeGreaterThan(0);
|
|
159
156
|
});
|
|
160
157
|
});
|
|
@@ -265,10 +262,9 @@ describe("Stream Exclusion Logic", () => {
|
|
|
265
262
|
await streamer2.addParts([{ type: "start" }]);
|
|
266
263
|
|
|
267
264
|
// Default list: only streaming
|
|
268
|
-
const defaultStreams = await ctx.runQuery(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
);
|
|
265
|
+
const defaultStreams = await ctx.runQuery(components.agent.streams.list, {
|
|
266
|
+
threadId,
|
|
267
|
+
});
|
|
272
268
|
expect(defaultStreams).toHaveLength(1);
|
|
273
269
|
expect(defaultStreams[0].status).toBe("streaming");
|
|
274
270
|
expect(defaultStreams[0].order).toBe(1);
|
|
@@ -325,10 +321,10 @@ describe("Stream Exclusion Logic", () => {
|
|
|
325
321
|
expect(finishedStreams[0].status).toBe("finished");
|
|
326
322
|
|
|
327
323
|
// Query for only aborted
|
|
328
|
-
const abortedStreams = await ctx.runQuery(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
);
|
|
324
|
+
const abortedStreams = await ctx.runQuery(components.agent.streams.list, {
|
|
325
|
+
threadId,
|
|
326
|
+
statuses: ["aborted"],
|
|
327
|
+
});
|
|
332
328
|
expect(abortedStreams).toHaveLength(1);
|
|
333
329
|
expect(abortedStreams[0].status).toBe("aborted");
|
|
334
330
|
|
|
@@ -487,10 +483,10 @@ describe("Delta Stream Consumption", () => {
|
|
|
487
483
|
expect(laterParts.length).toBeLessThanOrEqual(allParts.length);
|
|
488
484
|
|
|
489
485
|
// Fetching from the end cursor should yield nothing
|
|
490
|
-
const noDeltas = await ctx.runQuery(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
);
|
|
486
|
+
const noDeltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
487
|
+
threadId,
|
|
488
|
+
cursors: [{ cursor: endCursor, streamId }],
|
|
489
|
+
});
|
|
494
490
|
expect(noDeltas).toHaveLength(0);
|
|
495
491
|
});
|
|
496
492
|
});
|
|
@@ -526,16 +522,13 @@ describe("Delta Stream Consumption", () => {
|
|
|
526
522
|
const id2 = streamer2.streamId!;
|
|
527
523
|
|
|
528
524
|
// Fetch deltas for both streams simultaneously
|
|
529
|
-
const deltas = await ctx.runQuery(
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
],
|
|
537
|
-
},
|
|
538
|
-
);
|
|
525
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
526
|
+
threadId,
|
|
527
|
+
cursors: [
|
|
528
|
+
{ cursor: 0, streamId: id1 },
|
|
529
|
+
{ cursor: 0, streamId: id2 },
|
|
530
|
+
],
|
|
531
|
+
});
|
|
539
532
|
|
|
540
533
|
// Should have deltas for both streams
|
|
541
534
|
const s1Deltas = deltas.filter((d) => d.streamId === id1);
|
|
@@ -568,10 +561,10 @@ describe("Delta Stream Consumption", () => {
|
|
|
568
561
|
threadId,
|
|
569
562
|
statuses: ["finished"],
|
|
570
563
|
});
|
|
571
|
-
const deltas = await ctx.runQuery(
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
);
|
|
564
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
565
|
+
threadId,
|
|
566
|
+
cursors: [{ cursor: 0, streamId }],
|
|
567
|
+
});
|
|
575
568
|
|
|
576
569
|
// Derive UI messages
|
|
577
570
|
const uiMessages = await deriveUIMessagesFromDeltas(
|
|
@@ -615,10 +608,10 @@ describe("Delta Stream Consumption", () => {
|
|
|
615
608
|
await streamer.consumeStream(result.toUIMessageStream());
|
|
616
609
|
const streamId = streamer.streamId!;
|
|
617
610
|
|
|
618
|
-
const deltas = await ctx.runQuery(
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
);
|
|
611
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
612
|
+
threadId,
|
|
613
|
+
cursors: [{ cursor: 0, streamId }],
|
|
614
|
+
});
|
|
622
615
|
const { parts } = getParts(deltas);
|
|
623
616
|
|
|
624
617
|
// Compressed: all text-deltas for one text section should be merged
|
|
@@ -677,77 +670,6 @@ describe("Delta Stream Consumption", () => {
|
|
|
677
670
|
expect((parts[0] as { type: string }).type).toBe("new");
|
|
678
671
|
expect(cursor).toBe(6);
|
|
679
672
|
});
|
|
680
|
-
|
|
681
|
-
test("TextStreamPart format delta reconstruction with tool calls", () => {
|
|
682
|
-
const streamId = "s1";
|
|
683
|
-
const streamMessage: StreamMessage = {
|
|
684
|
-
streamId,
|
|
685
|
-
order: 1,
|
|
686
|
-
stepOrder: 0,
|
|
687
|
-
status: "streaming",
|
|
688
|
-
};
|
|
689
|
-
const deltas: StreamDelta[] = [
|
|
690
|
-
{
|
|
691
|
-
streamId,
|
|
692
|
-
start: 0,
|
|
693
|
-
end: 1,
|
|
694
|
-
parts: [{ type: "text-delta", id: "txt-0", text: "Let me call a tool. " }],
|
|
695
|
-
},
|
|
696
|
-
{
|
|
697
|
-
streamId,
|
|
698
|
-
start: 1,
|
|
699
|
-
end: 2,
|
|
700
|
-
parts: [
|
|
701
|
-
{
|
|
702
|
-
type: "tool-call",
|
|
703
|
-
toolCallId: "tc1",
|
|
704
|
-
toolName: "search",
|
|
705
|
-
input: { query: "hello" },
|
|
706
|
-
},
|
|
707
|
-
],
|
|
708
|
-
},
|
|
709
|
-
{
|
|
710
|
-
streamId,
|
|
711
|
-
start: 2,
|
|
712
|
-
end: 3,
|
|
713
|
-
parts: [
|
|
714
|
-
{
|
|
715
|
-
type: "tool-result",
|
|
716
|
-
toolCallId: "tc1",
|
|
717
|
-
toolName: "search",
|
|
718
|
-
output: "Found 3 results",
|
|
719
|
-
},
|
|
720
|
-
],
|
|
721
|
-
},
|
|
722
|
-
{
|
|
723
|
-
streamId,
|
|
724
|
-
start: 3,
|
|
725
|
-
end: 4,
|
|
726
|
-
parts: [
|
|
727
|
-
{ type: "text-delta", id: "txt-1", text: "Here are the results." },
|
|
728
|
-
],
|
|
729
|
-
},
|
|
730
|
-
];
|
|
731
|
-
|
|
732
|
-
const [messages, , changed] = deriveUIMessagesFromTextStreamParts(
|
|
733
|
-
"thread1",
|
|
734
|
-
[streamMessage],
|
|
735
|
-
[],
|
|
736
|
-
deltas,
|
|
737
|
-
);
|
|
738
|
-
|
|
739
|
-
expect(messages).toHaveLength(1);
|
|
740
|
-
expect(changed).toBe(true);
|
|
741
|
-
|
|
742
|
-
const msg = messages[0];
|
|
743
|
-
expect(msg.text).toContain("Let me call a tool.");
|
|
744
|
-
expect(msg.text).toContain("Here are the results.");
|
|
745
|
-
|
|
746
|
-
const toolParts = msg.parts.filter((p: any) =>
|
|
747
|
-
p.type.startsWith("tool-"),
|
|
748
|
-
);
|
|
749
|
-
expect(toolParts.length).toBeGreaterThan(0);
|
|
750
|
-
});
|
|
751
673
|
});
|
|
752
674
|
|
|
753
675
|
// ============================================================================
|
|
@@ -879,18 +801,21 @@ describe("Fallback Behavior", () => {
|
|
|
879
801
|
order: 0,
|
|
880
802
|
stepOrder: 0,
|
|
881
803
|
status: "streaming",
|
|
804
|
+
format: "UIMessageChunk",
|
|
882
805
|
};
|
|
883
806
|
const finishedMsg: StreamMessage = {
|
|
884
807
|
streamId: "s2",
|
|
885
808
|
order: 1,
|
|
886
809
|
stepOrder: 0,
|
|
887
810
|
status: "finished",
|
|
811
|
+
format: "UIMessageChunk",
|
|
888
812
|
};
|
|
889
813
|
const abortedMsg: StreamMessage = {
|
|
890
814
|
streamId: "s3",
|
|
891
815
|
order: 2,
|
|
892
816
|
stepOrder: 0,
|
|
893
817
|
status: "aborted",
|
|
818
|
+
format: "UIMessageChunk",
|
|
894
819
|
};
|
|
895
820
|
|
|
896
821
|
const msgs = await deriveUIMessagesFromDeltas(
|
|
@@ -1001,10 +926,10 @@ describe("Stream Lifecycle Integration", () => {
|
|
|
1001
926
|
expect(finished).toHaveLength(1);
|
|
1002
927
|
|
|
1003
928
|
// 4. Derive UI messages from stored deltas
|
|
1004
|
-
const deltas = await ctx.runQuery(
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
);
|
|
929
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
930
|
+
threadId,
|
|
931
|
+
cursors: [{ cursor: 0, streamId }],
|
|
932
|
+
});
|
|
1008
933
|
const uiMessages = await deriveUIMessagesFromDeltas(
|
|
1009
934
|
threadId,
|
|
1010
935
|
finished,
|
|
@@ -1056,10 +981,10 @@ describe("Stream Lifecycle Integration", () => {
|
|
|
1056
981
|
expect(aborted[0].status).toBe("aborted");
|
|
1057
982
|
|
|
1058
983
|
// Even aborted streams have their deltas stored
|
|
1059
|
-
const deltas = await ctx.runQuery(
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
);
|
|
984
|
+
const deltas = await ctx.runQuery(components.agent.streams.listDeltas, {
|
|
985
|
+
threadId,
|
|
986
|
+
cursors: [{ cursor: 0, streamId }],
|
|
987
|
+
});
|
|
1063
988
|
expect(deltas.length).toBeGreaterThan(0);
|
|
1064
989
|
});
|
|
1065
990
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, test } from "vitest";
|
|
2
|
-
import { createThread } from "
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { createThread } from "../../client/threads.js";
|
|
3
|
+
import type { MutationCtx } from "../../client/types.js";
|
|
3
4
|
import type { GenericSchema, SchemaDefinition } from "convex/server";
|
|
4
5
|
import { streamText } from "ai";
|
|
5
6
|
import { components, initConvexTest } from "./setup.test.js";
|
|
6
7
|
import { mockModel } from "./mockModel.js";
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
compressUIMessageChunks,
|
|
10
|
+
DeltaStreamer,
|
|
11
|
+
} from "./streaming.js";
|
|
8
12
|
import { getParts } from "../deltas.js";
|
|
9
13
|
import type { TestConvex } from "convex-test";
|
|
10
14
|
|
|
@@ -181,6 +185,176 @@ describe("DeltaStreamer", () => {
|
|
|
181
185
|
}
|
|
182
186
|
});
|
|
183
187
|
});
|
|
184
|
-
|
|
188
|
+
test("honors a signal that was aborted before construction", async () => {
|
|
189
|
+
const abortController = new AbortController();
|
|
190
|
+
abortController.abort();
|
|
191
|
+
|
|
192
|
+
await t.run(async (ctx) => {
|
|
193
|
+
const streamer = new DeltaStreamer<string>(
|
|
194
|
+
components.agent,
|
|
195
|
+
ctx,
|
|
196
|
+
{ ...defaultTestOptions, abortSignal: abortController.signal },
|
|
197
|
+
{ ...testMetadata, threadId },
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(streamer.abortController.signal.aborted).toBe(true);
|
|
201
|
+
await streamer.addParts(["ignored"]);
|
|
202
|
+
expect(streamer.streamId).toBeUndefined();
|
|
203
|
+
await expect(streamer.getOrCreateStreamId()).rejects.toThrow(
|
|
204
|
+
"Cannot create a stream after it has been aborted",
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("shares signal and fail cleanup while stream creation is in flight", async () => {
|
|
210
|
+
let resolveCreate!: (streamId: string) => void;
|
|
211
|
+
const creatingStream = new Promise<string>((resolve) => {
|
|
212
|
+
resolveCreate = resolve;
|
|
213
|
+
});
|
|
214
|
+
let resolveAbort!: () => void;
|
|
215
|
+
const abortingStream = new Promise<void>((resolve) => {
|
|
216
|
+
resolveAbort = resolve;
|
|
217
|
+
});
|
|
218
|
+
const runMutation = vi
|
|
219
|
+
.fn()
|
|
220
|
+
.mockImplementationOnce(() => creatingStream)
|
|
221
|
+
.mockImplementationOnce(() => abortingStream);
|
|
222
|
+
const abortController = new AbortController();
|
|
223
|
+
const streamer = new DeltaStreamer<string>(
|
|
224
|
+
components.agent,
|
|
225
|
+
{ runMutation } as unknown as MutationCtx,
|
|
226
|
+
{ ...defaultTestOptions, abortSignal: abortController.signal },
|
|
227
|
+
{ ...testMetadata, threadId },
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const streamId = streamer.getStreamId();
|
|
231
|
+
abortController.abort();
|
|
232
|
+
const failing = streamer.fail("creation failed");
|
|
233
|
+
let failSettled = false;
|
|
234
|
+
void failing.then(() => {
|
|
235
|
+
failSettled = true;
|
|
236
|
+
});
|
|
237
|
+
resolveCreate("stream-1");
|
|
238
|
+
await expect(streamId).resolves.toBe("stream-1");
|
|
239
|
+
await vi.waitFor(() => expect(runMutation).toHaveBeenCalledTimes(2));
|
|
240
|
+
|
|
241
|
+
expect(failSettled).toBe(false);
|
|
242
|
+
resolveAbort();
|
|
243
|
+
await failing;
|
|
244
|
+
|
|
245
|
+
expect(runMutation).toHaveBeenNthCalledWith(
|
|
246
|
+
2,
|
|
247
|
+
components.agent.streams.abort,
|
|
248
|
+
{ streamId: "stream-1", reason: "abortSignal" },
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("aborts the component stream when a delta write fails", async () => {
|
|
253
|
+
const runMutation = vi
|
|
254
|
+
.fn()
|
|
255
|
+
.mockResolvedValueOnce("stream-1")
|
|
256
|
+
.mockRejectedValueOnce(new Error("delta failed"))
|
|
257
|
+
.mockResolvedValueOnce(undefined);
|
|
258
|
+
let abortReason: string | undefined;
|
|
259
|
+
const streamer = new DeltaStreamer<string>(
|
|
260
|
+
components.agent,
|
|
261
|
+
{ runMutation } as unknown as MutationCtx,
|
|
262
|
+
{
|
|
263
|
+
...defaultTestOptions,
|
|
264
|
+
onAsyncAbort: async (reason) => {
|
|
265
|
+
abortReason = reason;
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
{ ...testMetadata, threadId },
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
await streamer.addParts(["A"]);
|
|
272
|
+
await streamer.finish();
|
|
273
|
+
|
|
274
|
+
expect(abortReason).toBe("delta failed");
|
|
275
|
+
expect(runMutation).toHaveBeenNthCalledWith(
|
|
276
|
+
3,
|
|
277
|
+
components.agent.streams.abort,
|
|
278
|
+
{ streamId: "stream-1", reason: "delta failed" },
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("surfaces pending-message cleanup failure after aborting the stream", async () => {
|
|
283
|
+
const pendingMessageFailure = new Error("pending message cleanup failed");
|
|
284
|
+
let resolveComponentAbort!: () => void;
|
|
285
|
+
const componentAborted = new Promise<void>((resolve) => {
|
|
286
|
+
resolveComponentAbort = resolve;
|
|
287
|
+
});
|
|
288
|
+
const runMutation = vi
|
|
289
|
+
.fn()
|
|
290
|
+
.mockResolvedValueOnce("stream-1")
|
|
291
|
+
.mockRejectedValueOnce(new Error("delta failed"))
|
|
292
|
+
.mockImplementationOnce(() => {
|
|
293
|
+
resolveComponentAbort();
|
|
294
|
+
return Promise.resolve();
|
|
295
|
+
});
|
|
296
|
+
const streamer = new DeltaStreamer<string>(
|
|
297
|
+
components.agent,
|
|
298
|
+
{ runMutation } as unknown as MutationCtx,
|
|
299
|
+
{
|
|
300
|
+
...defaultTestOptions,
|
|
301
|
+
onAsyncAbort: async () => {
|
|
302
|
+
throw pendingMessageFailure;
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{ ...testMetadata, threadId },
|
|
306
|
+
);
|
|
307
|
+
const stream = {
|
|
308
|
+
async *[Symbol.asyncIterator]() {
|
|
309
|
+
yield "A";
|
|
310
|
+
await componentAborted;
|
|
311
|
+
},
|
|
312
|
+
} as unknown as Parameters<typeof streamer.consumeStream>[0];
|
|
313
|
+
|
|
314
|
+
await expect(streamer.consumeStream(stream)).rejects.toBe(
|
|
315
|
+
pendingMessageFailure,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(runMutation).toHaveBeenNthCalledWith(
|
|
319
|
+
3,
|
|
320
|
+
components.agent.streams.abort,
|
|
321
|
+
{ streamId: "stream-1", reason: "delta failed" },
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("finishes external abort cleanup when the active delta write fails", async () => {
|
|
326
|
+
const pendingMessageFailure = new Error("pending message cleanup failed");
|
|
327
|
+
let rejectDelta!: (error: Error) => void;
|
|
328
|
+
const deltaWrite = new Promise<never>((_, reject) => {
|
|
329
|
+
rejectDelta = reject;
|
|
330
|
+
});
|
|
331
|
+
const runMutation = vi
|
|
332
|
+
.fn()
|
|
333
|
+
.mockResolvedValueOnce("stream-1")
|
|
334
|
+
.mockImplementationOnce(() => deltaWrite)
|
|
335
|
+
.mockResolvedValueOnce(undefined);
|
|
336
|
+
const streamer = new DeltaStreamer<string>(
|
|
337
|
+
components.agent,
|
|
338
|
+
{ runMutation } as unknown as MutationCtx,
|
|
339
|
+
{
|
|
340
|
+
...defaultTestOptions,
|
|
341
|
+
onAsyncAbort: async () => {
|
|
342
|
+
throw pendingMessageFailure;
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
{ ...testMetadata, threadId },
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
await streamer.addParts(["A"]);
|
|
349
|
+
const failing = streamer.fail("external abort");
|
|
350
|
+
rejectDelta(new Error("delta failed"));
|
|
351
|
+
|
|
352
|
+
await expect(failing).rejects.toBe(pendingMessageFailure);
|
|
353
|
+
expect(runMutation).toHaveBeenNthCalledWith(
|
|
354
|
+
3,
|
|
355
|
+
components.agent.streams.abort,
|
|
356
|
+
{ streamId: "stream-1", reason: "external abort" },
|
|
357
|
+
);
|
|
358
|
+
});
|
|
185
359
|
// TODO: test fetching partial stream data - syncStreams w/ cursors
|
|
186
360
|
});
|