assistant-stream 0.3.26 → 0.3.27
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/core/AssistantStream.d.ts.map +1 -1
- package/dist/core/AssistantStreamChunk.d.ts +3 -3
- package/dist/core/accumulators/AssistantMessageStream.d.ts.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.js +65 -28
- package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
- package/dist/core/converters/toGenericMessages.js.map +1 -1
- package/dist/core/gorp/GorpStreamAccumulator.d.ts +15 -0
- package/dist/core/gorp/GorpStreamAccumulator.d.ts.map +1 -0
- package/dist/core/{object/ObjectStreamAccumulator.js → gorp/GorpStreamAccumulator.js} +11 -9
- package/dist/core/gorp/GorpStreamAccumulator.js.map +1 -0
- package/dist/core/gorp/GorpStreamDeltaTracker.d.ts +17 -0
- package/dist/core/gorp/GorpStreamDeltaTracker.d.ts.map +1 -0
- package/dist/core/gorp/GorpStreamDeltaTracker.js +38 -0
- package/dist/core/gorp/GorpStreamDeltaTracker.js.map +1 -0
- package/dist/core/gorp/GorpStreamResponse.d.ts +16 -0
- package/dist/core/gorp/GorpStreamResponse.d.ts.map +1 -0
- package/dist/core/gorp/GorpStreamResponse.js +51 -0
- package/dist/core/gorp/GorpStreamResponse.js.map +1 -0
- package/dist/core/gorp/changeTree.d.ts +16 -0
- package/dist/core/gorp/changeTree.d.ts.map +1 -0
- package/dist/core/gorp/changeTree.js +77 -0
- package/dist/core/gorp/changeTree.js.map +1 -0
- package/dist/core/gorp/createGorpStream.d.ts +16 -0
- package/dist/core/gorp/createGorpStream.d.ts.map +1 -0
- package/dist/core/{object/createObjectStream.js → gorp/createGorpStream.js} +13 -8
- package/dist/core/gorp/createGorpStream.js.map +1 -0
- package/dist/core/{object → gorp}/types.d.ts +6 -5
- package/dist/core/gorp/types.d.ts.map +1 -0
- package/dist/core/modules/assistant-stream.d.ts +5 -4
- package/dist/core/modules/assistant-stream.d.ts.map +1 -1
- package/dist/core/modules/assistant-stream.js +10 -6
- package/dist/core/modules/assistant-stream.js.map +1 -1
- package/dist/core/modules/tool-call.js +2 -2
- package/dist/core/modules/tool-call.js.map +1 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts.map +1 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.js +91 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.js.map +1 -1
- package/dist/core/serialization/data-stream/chunk-types.d.ts +2 -2
- package/dist/core/serialization/data-stream/chunk-types.js.map +1 -1
- package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts.map +1 -1
- package/dist/core/serialization/ui-message-stream/UIMessageStream.js +11 -1
- package/dist/core/serialization/ui-message-stream/UIMessageStream.js.map +1 -1
- package/dist/core/tool/ToolCallReader.d.ts.map +1 -1
- package/dist/core/tool/ToolCallReader.js.map +1 -1
- package/dist/core/tool/ToolExecutionStream.d.ts.map +1 -1
- package/dist/core/tool/ToolExecutionStream.js +9 -1
- package/dist/core/tool/ToolExecutionStream.js.map +1 -1
- package/dist/core/tool/ToolResponse.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.js +7 -2
- package/dist/core/tool/toolResultStream.js.map +1 -1
- package/dist/core/utils/stream/AssistantTransformStream.d.ts.map +1 -1
- package/dist/core/utils/stream/PipeableTransformStream.d.ts.map +1 -1
- package/dist/core/utils/stream/SSEEventDecoder.d.ts.map +1 -1
- package/dist/core/utils/stream/merge.d.ts +2 -0
- package/dist/core/utils/stream/merge.d.ts.map +1 -1
- package/dist/core/utils/stream/merge.js +27 -8
- package/dist/core/utils/stream/merge.js.map +1 -1
- package/dist/core/utils/stream/path-utils.d.ts.map +1 -1
- package/dist/index.d.ts +25 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -0
- package/dist/resumable/ResumableStreamContext.d.ts.map +1 -1
- package/dist/resumable/ResumableStreamContext.js +12 -5
- package/dist/resumable/ResumableStreamContext.js.map +1 -1
- package/dist/resumable/errors.d.ts.map +1 -1
- package/dist/resumable/stores/redis-impl.d.ts.map +1 -1
- package/dist/utils/AsyncIterableStream.d.ts.map +1 -1
- package/dist/utils/AsyncIterableStream.js +18 -3
- package/dist/utils/AsyncIterableStream.js.map +1 -1
- package/dist/utils/promiseWithResolvers.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/core/AssistantStreamChunk.ts +3 -3
- package/src/core/accumulators/assistant-message-accumulator.test.ts +374 -1
- package/src/core/accumulators/assistant-message-accumulator.ts +75 -26
- package/src/core/{object/ObjectStream.test.ts → gorp/GorpStream.test.ts} +86 -25
- package/src/core/gorp/GorpStreamAccumulator.test.ts +28 -0
- package/src/core/{object/ObjectStreamAccumulator.ts → gorp/GorpStreamAccumulator.ts} +13 -15
- package/src/core/gorp/GorpStreamDeltaTracker.test.ts +182 -0
- package/src/core/gorp/GorpStreamDeltaTracker.ts +55 -0
- package/src/core/{object/ObjectStreamResponse.test.ts → gorp/GorpStreamResponse.test.ts} +6 -6
- package/src/core/gorp/GorpStreamResponse.ts +88 -0
- package/src/core/gorp/changeTree.test.ts +168 -0
- package/src/core/gorp/changeTree.ts +96 -0
- package/src/core/{object/createObjectStream.ts → gorp/createGorpStream.ts} +23 -17
- package/src/core/{object → gorp}/types.ts +5 -3
- package/src/core/modules/assistant-stream.test.ts +154 -2
- package/src/core/modules/assistant-stream.ts +16 -6
- package/src/core/modules/tool-call.test.ts +67 -0
- package/src/core/modules/tool-call.ts +3 -3
- package/src/core/serialization/assistant-transport/AssistantTransport.test.ts +174 -1
- package/src/core/serialization/assistant-transport/AssistantTransport.ts +77 -1
- package/src/core/serialization/data-stream/chunk-types.ts +2 -2
- package/src/core/serialization/ui-message-stream/UIMessageStream.test.ts +111 -0
- package/src/core/serialization/ui-message-stream/UIMessageStream.ts +20 -1
- package/src/core/tool/ToolExecutionStream.ts +10 -0
- package/src/core/tool/toolResultStream.test.ts +50 -0
- package/src/core/tool/toolResultStream.ts +7 -2
- package/src/core/utils/stream/merge.ts +31 -9
- package/src/gorp.test.ts +42 -0
- package/src/index.ts +21 -6
- package/src/resumable/ResumableStreamContext.test.ts +116 -1
- package/src/resumable/ResumableStreamContext.ts +32 -5
- package/src/resumable/createResumableAssistantStreamResponse.test.ts +1 -25
- package/src/utils/AsyncIterableStream.test.ts +61 -0
- package/src/utils/AsyncIterableStream.ts +18 -3
- package/dist/core/object/ObjectStreamAccumulator.d.ts +0 -15
- package/dist/core/object/ObjectStreamAccumulator.d.ts.map +0 -1
- package/dist/core/object/ObjectStreamAccumulator.js.map +0 -1
- package/dist/core/object/ObjectStreamResponse.d.ts +0 -16
- package/dist/core/object/ObjectStreamResponse.d.ts.map +0 -1
- package/dist/core/object/ObjectStreamResponse.js +0 -57
- package/dist/core/object/ObjectStreamResponse.js.map +0 -1
- package/dist/core/object/createObjectStream.d.ts +0 -16
- package/dist/core/object/createObjectStream.d.ts.map +0 -1
- package/dist/core/object/createObjectStream.js.map +0 -1
- package/dist/core/object/types.d.ts.map +0 -1
- package/src/core/object/ObjectStreamResponse.ts +0 -114
- /package/dist/core/{object → gorp}/types.js +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;cA4Ea,uBAAkB,SAAA;;;;;;MAAmB,eAAA;;cAErC,6BAAoB;;KAErB,uBAAuB;;cAEtB,2BAAwB,UAAA,aAAA,eAAA;;KAEzB,oBAAoB"}
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,18 @@ import { ToolResponse } from "./core/tool/ToolResponse.js";
|
|
|
10
10
|
import { ToolExecutionStream } from "./core/tool/ToolExecutionStream.js";
|
|
11
11
|
import { toolResultStream, unstable_runPendingTools } from "./core/tool/toolResultStream.js";
|
|
12
12
|
import { toJSONSchema, toPartialJSONSchema, toToolsJSONSchema } from "./core/tool/schema-utils.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { createGorpStream } from "./core/gorp/createGorpStream.js";
|
|
14
|
+
import { GorpStreamResponse, fromGorpStreamResponse } from "./core/gorp/GorpStreamResponse.js";
|
|
15
|
+
import { GorpStreamDeltaTracker } from "./core/gorp/GorpStreamDeltaTracker.js";
|
|
15
16
|
import { toGenericMessages } from "./core/converters/toGenericMessages.js";
|
|
16
|
-
|
|
17
|
+
//#region src/index.ts
|
|
18
|
+
/** @deprecated Use the assistant-transport surface instead. */
|
|
19
|
+
const createObjectStream = createGorpStream;
|
|
20
|
+
/** @deprecated Use the assistant-transport surface instead. */
|
|
21
|
+
const ObjectStreamResponse = GorpStreamResponse;
|
|
22
|
+
/** @deprecated Use the assistant-transport surface instead. */
|
|
23
|
+
const fromObjectStreamResponse = fromGorpStreamResponse;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { AssistantMessageAccumulator, AssistantMessageStream, AssistantStream, AssistantTransportDecoder, GorpStreamDeltaTracker as AssistantTransportDeltaTracker, AssistantTransportEncoder, DataStreamDecoder, DataStreamEncoder, ObjectStreamResponse, PlainTextDecoder, PlainTextEncoder, ToolExecutionStream, ToolResponse, UIMessageStreamDecoder, createAssistantStream, createAssistantStreamController, createAssistantStreamResponse, createObjectStream, fromObjectStreamResponse, toGenericMessages, toJSONSchema, toPartialJSONSchema, toToolsJSONSchema, createInitialMessage as unstable_createInitialMessage, unstable_runPendingTools, toolResultStream as unstable_toolResultStream };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["export {\n createAssistantStream,\n createAssistantStreamResponse,\n createAssistantStreamController,\n} from \"./core/modules/assistant-stream\";\nexport {\n AssistantMessageAccumulator,\n createInitialMessage as unstable_createInitialMessage,\n} from \"./core/accumulators/assistant-message-accumulator\";\nexport { AssistantStream } from \"./core/AssistantStream\";\nexport type { AssistantStreamController } from \"./core/modules/assistant-stream\";\nexport type { AssistantStreamChunk } from \"./core/AssistantStreamChunk\";\nexport {\n DataStreamDecoder,\n DataStreamEncoder,\n} from \"./core/serialization/data-stream/DataStream\";\nexport {\n PlainTextDecoder,\n PlainTextEncoder,\n} from \"./core/serialization/PlainText\";\nexport {\n AssistantTransportDecoder,\n AssistantTransportEncoder,\n} from \"./core/serialization/assistant-transport/AssistantTransport\";\nexport {\n UIMessageStreamDecoder,\n type UIMessageStreamChunk,\n type UIMessageStreamDataChunk,\n type UIMessageStreamDecoderOptions,\n} from \"./core/serialization/ui-message-stream/UIMessageStream\";\nexport { AssistantMessageStream } from \"./core/accumulators/AssistantMessageStream\";\nexport type {\n AssistantMessage,\n AssistantMessageTiming,\n DataPart,\n ToolCallTiming,\n} from \"./core/utils/types\";\n\nexport type {\n Tool,\n ToolDeclaration,\n McpServerConfig,\n ToolModelContentPart,\n ToolModelOutputFunction,\n} from \"./core/tool/tool-types\";\nexport { ToolResponse, type ToolResponseLike } from \"./core/tool/ToolResponse\";\nexport { ToolExecutionStream } from \"./core/tool/ToolExecutionStream\";\nexport type { ProviderOptions, ToolCallReader } from \"./core/tool/tool-types\";\nexport {\n toolResultStream as unstable_toolResultStream,\n unstable_runPendingTools,\n type ToolResultStreamOptions,\n} from \"./core/tool/toolResultStream\";\nexport {\n toJSONSchema,\n toPartialJSONSchema,\n toToolsJSONSchema,\n type ToolJSONSchema,\n type ToToolsJSONSchemaOptions,\n} from \"./core/tool/schema-utils\";\n\nexport type { TextStreamController } from \"./core/modules/text\";\nexport type { ToolCallStreamController } from \"./core/modules/tool-call\";\n\nimport { createGorpStream } from \"./core/gorp/createGorpStream\";\nimport {\n GorpStreamResponse,\n fromGorpStreamResponse,\n} from \"./core/gorp/GorpStreamResponse\";\nimport type { GorpStreamChunk } from \"./core/gorp/types\";\nimport { GorpStreamDeltaTracker } from \"./core/gorp/GorpStreamDeltaTracker\";\n\nexport { GorpStreamDeltaTracker as AssistantTransportDeltaTracker };\nexport type { AssistantTransportStateOperation } from \"./core/gorp/types\";\n\n/** @deprecated Use the assistant-transport surface instead. */\nexport const createObjectStream = createGorpStream;\n/** @deprecated Use the assistant-transport surface instead. */\nexport const ObjectStreamResponse = GorpStreamResponse;\n/** @deprecated Use the assistant-transport surface instead. */\nexport type ObjectStreamResponse = GorpStreamResponse;\n/** @deprecated Use the assistant-transport surface instead. */\nexport const fromObjectStreamResponse = fromGorpStreamResponse;\n/** @deprecated Use the assistant-transport surface instead. */\nexport type ObjectStreamChunk = GorpStreamChunk;\n\nexport {\n toGenericMessages,\n type GenericMessage,\n type GenericSystemMessage,\n type GenericUserMessage,\n type GenericAssistantMessage,\n type GenericToolMessage,\n type GenericTextPart,\n type GenericFilePart,\n type GenericToolCallPart,\n type GenericToolResultPart,\n} from \"./core/converters/toGenericMessages\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AA4EA,MAAa,qBAAqB;;AAElC,MAAa,uBAAuB;;AAIpC,MAAa,2BAA2B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResumableStreamContext.d.ts","names":[],"sources":["../../src/resumable/ResumableStreamContext.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"ResumableStreamContext.d.ts","names":[],"sources":["../../src/resumable/ResumableStreamContext.ts"],"mappings":";;KAQY;WACD,OAAO;WACP;;;;;;WAMA,aAAa,SAAS;WACtB,aAAa,kBAAkB,MAAM;WACrC,YAAY,kBAAkB;WAC9B,cACP,kBACA,0BACA;WAEO,WAAW,kBAAkB;;UAGvB;;EAEf,IACE,kBACA,kBAAkB,eAAe,cAChC,QAAQ,eAAe;;EAG1B,OAAO,mBAAmB,QAAQ,eAAe;;EAGjD,cAAc,mBAAmB,QAAQ,eAAe;EAExD,OAAO,mBAAmB,QAAQ;EAElC,OAAO,mBAAmB;;iBAkBZ,6BACd,SAAS,gCACR"}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
+
import { withPromiseOrValue } from "../core/utils/withPromiseOrValue.js";
|
|
1
2
|
import { ResumableStreamError } from "./errors.js";
|
|
2
3
|
//#region src/resumable/ResumableStreamContext.ts
|
|
4
|
+
const invokeObservabilityHook = (name, hook, ...args) => {
|
|
5
|
+
if (!hook) return;
|
|
6
|
+
withPromiseOrValue(() => hook(...args), () => {}, (error) => {
|
|
7
|
+
console.error(`resumable stream ${name} hook failed:`, error);
|
|
8
|
+
});
|
|
9
|
+
};
|
|
3
10
|
function createResumableStreamContext(options) {
|
|
4
11
|
const { store, waitUntil, onAcquire, onAppend, onFinalize, onError } = options;
|
|
5
12
|
const acquireOptions = options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : void 0;
|
|
6
13
|
return {
|
|
7
14
|
async run(streamId, makeStream) {
|
|
8
15
|
const role = await store.acquire(streamId, acquireOptions);
|
|
9
|
-
onAcquire
|
|
16
|
+
invokeObservabilityHook("onAcquire", onAcquire, streamId, role);
|
|
10
17
|
if (role === "producer") startProducerTask(store, streamId, makeStream, {
|
|
11
18
|
waitUntil,
|
|
12
19
|
onAppend,
|
|
@@ -42,13 +49,13 @@ function startProducerTask(store, streamId, makeStream, hooks) {
|
|
|
42
49
|
const { done, value } = await reader.read();
|
|
43
50
|
if (done) break;
|
|
44
51
|
await store.append(streamId, value);
|
|
45
|
-
onAppend
|
|
52
|
+
invokeObservabilityHook("onAppend", onAppend, streamId, value.byteLength);
|
|
46
53
|
}
|
|
47
54
|
await store.finalize(streamId, "done");
|
|
48
|
-
onFinalize
|
|
55
|
+
invokeObservabilityHook("onFinalize", onFinalize, streamId, "done");
|
|
49
56
|
} catch (err) {
|
|
50
57
|
cancelled = true;
|
|
51
|
-
onError
|
|
58
|
+
invokeObservabilityHook("onError", onError, streamId, err);
|
|
52
59
|
const message = err instanceof Error ? err.message : String(err);
|
|
53
60
|
try {
|
|
54
61
|
await reader?.cancel(err);
|
|
@@ -57,7 +64,7 @@ function startProducerTask(store, streamId, makeStream, hooks) {
|
|
|
57
64
|
}
|
|
58
65
|
try {
|
|
59
66
|
await store.finalize(streamId, "error", message);
|
|
60
|
-
onFinalize
|
|
67
|
+
invokeObservabilityHook("onFinalize", onFinalize, streamId, "error", message);
|
|
61
68
|
} catch (finalizeErr) {
|
|
62
69
|
console.error("resumable stream finalize failed:", finalizeErr);
|
|
63
70
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResumableStreamContext.js","names":[],"sources":["../../src/resumable/ResumableStreamContext.ts"],"sourcesContent":["import { ResumableStreamError } from \"./errors\";\nimport type {\n ResumableStreamRole,\n ResumableStreamStatus,\n ResumableStreamStore,\n} from \"./types\";\n\nexport type ResumableStreamContextOptions = {\n readonly store: ResumableStreamStore;\n readonly ttlMs?: number;\n /**\n * Required on serverless runtimes that terminate background work when the\n * request handler returns (Vercel, Cloudflare). Pass `after` from\n * `next/server` so the producer task outlives the response.\n */\n readonly waitUntil?: (promise: Promise<unknown>) => void;\n readonly onAcquire?: (streamId: string, role: ResumableStreamRole) => void;\n readonly onAppend?: (streamId: string, byteLength: number) => void;\n readonly onFinalize?: (\n streamId: string,\n status: \"done\" | \"error\",\n error?: string,\n ) => void;\n readonly onError?: (streamId: string, error: unknown) => void;\n};\n\nexport interface ResumableStreamContext {\n /** Producer or consumer entrypoint. Atomically elects the role. */\n run(\n streamId: string,\n makeStream: () => ReadableStream<Uint8Array>,\n ): Promise<ReadableStream<Uint8Array>>;\n\n /** Returns `null` when the stream does not exist. */\n resume(streamId: string): Promise<ReadableStream<Uint8Array> | null>;\n\n /** Throws `ResumableStreamError(\"missing\")` when the stream does not exist. */\n requireResume(streamId: string): Promise<ReadableStream<Uint8Array>>;\n\n status(streamId: string): Promise<ResumableStreamStatus>;\n\n delete(streamId: string): Promise<void>;\n}\n\nexport function createResumableStreamContext(\n options: ResumableStreamContextOptions,\n): ResumableStreamContext {\n const { store, waitUntil, onAcquire, onAppend, onFinalize, onError } =\n options;\n const acquireOptions =\n options.ttlMs !== undefined ? { ttlMs: options.ttlMs } : undefined;\n\n return {\n async run(streamId, makeStream) {\n const role = await store.acquire(streamId, acquireOptions);\n onAcquire
|
|
1
|
+
{"version":3,"file":"ResumableStreamContext.js","names":[],"sources":["../../src/resumable/ResumableStreamContext.ts"],"sourcesContent":["import { ResumableStreamError } from \"./errors\";\nimport { withPromiseOrValue } from \"../core/utils/withPromiseOrValue\";\nimport type {\n ResumableStreamRole,\n ResumableStreamStatus,\n ResumableStreamStore,\n} from \"./types\";\n\nexport type ResumableStreamContextOptions = {\n readonly store: ResumableStreamStore;\n readonly ttlMs?: number;\n /**\n * Required on serverless runtimes that terminate background work when the\n * request handler returns (Vercel, Cloudflare). Pass `after` from\n * `next/server` so the producer task outlives the response.\n */\n readonly waitUntil?: (promise: Promise<unknown>) => void;\n readonly onAcquire?: (streamId: string, role: ResumableStreamRole) => void;\n readonly onAppend?: (streamId: string, byteLength: number) => void;\n readonly onFinalize?: (\n streamId: string,\n status: \"done\" | \"error\",\n error?: string,\n ) => void;\n readonly onError?: (streamId: string, error: unknown) => void;\n};\n\nexport interface ResumableStreamContext {\n /** Producer or consumer entrypoint. Atomically elects the role. */\n run(\n streamId: string,\n makeStream: () => ReadableStream<Uint8Array>,\n ): Promise<ReadableStream<Uint8Array>>;\n\n /** Returns `null` when the stream does not exist. */\n resume(streamId: string): Promise<ReadableStream<Uint8Array> | null>;\n\n /** Throws `ResumableStreamError(\"missing\")` when the stream does not exist. */\n requireResume(streamId: string): Promise<ReadableStream<Uint8Array>>;\n\n status(streamId: string): Promise<ResumableStreamStatus>;\n\n delete(streamId: string): Promise<void>;\n}\n\nconst invokeObservabilityHook = <TArgs extends unknown[]>(\n name: string,\n hook: ((...args: TArgs) => void) | undefined,\n ...args: TArgs\n): void => {\n if (!hook) return;\n void withPromiseOrValue(\n () => hook(...args),\n () => {},\n (error) => {\n console.error(`resumable stream ${name} hook failed:`, error);\n },\n );\n};\n\nexport function createResumableStreamContext(\n options: ResumableStreamContextOptions,\n): ResumableStreamContext {\n const { store, waitUntil, onAcquire, onAppend, onFinalize, onError } =\n options;\n const acquireOptions =\n options.ttlMs !== undefined ? { ttlMs: options.ttlMs } : undefined;\n\n return {\n async run(streamId, makeStream) {\n const role = await store.acquire(streamId, acquireOptions);\n invokeObservabilityHook(\"onAcquire\", onAcquire, streamId, role);\n if (role === \"producer\") {\n startProducerTask(store, streamId, makeStream, {\n waitUntil,\n onAppend,\n onFinalize,\n onError,\n });\n }\n return readFromStore(store, streamId);\n },\n\n async resume(streamId) {\n const status = await store.status(streamId);\n if (status === \"missing\") return null;\n return readFromStore(store, streamId);\n },\n\n async requireResume(streamId) {\n const status = await store.status(streamId);\n if (status === \"missing\") {\n throw new ResumableStreamError(\n \"missing\",\n `resumable stream not found: ${streamId}`,\n );\n }\n return readFromStore(store, streamId);\n },\n\n async status(streamId) {\n return store.status(streamId);\n },\n\n async delete(streamId) {\n await store.delete(streamId);\n },\n };\n}\n\ntype ProducerHooks = {\n readonly waitUntil: ((promise: Promise<unknown>) => void) | undefined;\n readonly onAppend:\n | ((streamId: string, byteLength: number) => void)\n | undefined;\n readonly onFinalize:\n | ((streamId: string, status: \"done\" | \"error\", error?: string) => void)\n | undefined;\n readonly onError: ((streamId: string, error: unknown) => void) | undefined;\n};\n\nfunction startProducerTask(\n store: ResumableStreamStore,\n streamId: string,\n makeStream: () => ReadableStream<Uint8Array>,\n hooks: ProducerHooks,\n): void {\n const { waitUntil, onAppend, onFinalize, onError } = hooks;\n const task = (async () => {\n let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;\n let cancelled = false;\n try {\n reader = makeStream().getReader();\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n await store.append(streamId, value);\n invokeObservabilityHook(\n \"onAppend\",\n onAppend,\n streamId,\n value.byteLength,\n );\n }\n await store.finalize(streamId, \"done\");\n invokeObservabilityHook(\"onFinalize\", onFinalize, streamId, \"done\");\n } catch (err) {\n cancelled = true;\n invokeObservabilityHook(\"onError\", onError, streamId, err);\n const message = err instanceof Error ? err.message : String(err);\n try {\n await reader?.cancel(err);\n } catch (cancelErr) {\n console.error(\"resumable stream reader cancel failed:\", cancelErr);\n }\n try {\n await store.finalize(streamId, \"error\", message);\n invokeObservabilityHook(\n \"onFinalize\",\n onFinalize,\n streamId,\n \"error\",\n message,\n );\n } catch (finalizeErr) {\n console.error(\"resumable stream finalize failed:\", finalizeErr);\n }\n } finally {\n if (!cancelled) reader?.releaseLock();\n }\n })();\n\n if (waitUntil) waitUntil(task);\n task.catch((err) => {\n console.error(\"resumable producer task failed:\", err);\n });\n}\n\nfunction readFromStore(\n store: ResumableStreamStore,\n streamId: string,\n): ReadableStream<Uint8Array> {\n const ac = new AbortController();\n let iterator: AsyncIterator<{ chunk: Uint8Array }> | undefined;\n\n return new ReadableStream<Uint8Array>({\n start() {\n iterator = store.read(streamId, \"\", ac.signal)[Symbol.asyncIterator]();\n },\n async pull(controller) {\n try {\n if (!iterator) return;\n const { done, value } = await iterator.next();\n if (done) {\n controller.close();\n return;\n }\n controller.enqueue(value.chunk);\n } catch (err) {\n // the platform never calls cancel() on errored streams, so unwind\n // the store iterator and abort the signal explicitly.\n ac.abort();\n try {\n await iterator?.return?.();\n } catch {}\n controller.error(err);\n }\n },\n cancel() {\n ac.abort();\n iterator?.return?.().catch(() => {});\n },\n });\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","names":[],"sources":["../../src/resumable/errors.ts"],"mappings":";KAAY;cAMC,6BAA6B;WAC/B,MAAM;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","names":[],"sources":["../../src/resumable/errors.ts"],"mappings":";KAAY;cAMC,6BAA6B;WAC/B,MAAM;EAEH,YAAA,MAAM,0BAA0B;;iBAS9B,iBAAiB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis-impl.d.ts","names":[],"sources":["../../../src/resumable/stores/redis-impl.ts"],"mappings":";;KAsBY;WAEG;WACA;WACA,QAAQ,wBAAwB;;WAEhC;WAAyB;WAAsB;;WAE/C;WACA;WACA;WACA;;;;;;;UAQE;EACf,MAAM,aAAa,eAAe,iBAAiB;EACnD,IAAI,aAAa,eAAe,iBAAiB;EACjD,IAAI,cAAc;EAClB,OAAO,aAAa,iBAAiB;EACrC,OAAO,cAAc;EACrB,IAAI,iBAAiB;EACrB,KACE,aACA,QAAQ,wBAAwB,cAC/B;EACH,OACE,aACA,eACA,cACC,QACD;IAAQ;IAAY,QAAQ,wBAAwB;;;EAGtD,SAAS,mBAAmB,oBAAoB;;KAGtC;WACD;WACA;;WAEA;WACA;;cAGE,qCAAqC;mBAC/B;mBACA;mBACA;mBACA;mBACA;
|
|
1
|
+
{"version":3,"file":"redis-impl.d.ts","names":[],"sources":["../../../src/resumable/stores/redis-impl.ts"],"mappings":";;KAsBY;WAEG;WACA;WACA,QAAQ,wBAAwB;;WAEhC;WAAyB;WAAsB;;WAE/C;WACA;WACA;WACA;;;;;;;UAQE;EACf,MAAM,aAAa,eAAe,iBAAiB;EACnD,IAAI,aAAa,eAAe,iBAAiB;EACjD,IAAI,cAAc;EAClB,OAAO,aAAa,iBAAiB;EACrC,OAAO,cAAc;EACrB,IAAI,iBAAiB;EACrB,KACE,aACA,QAAQ,wBAAwB,cAC/B;EACH,OACE,aACA,eACA,cACC,QACD;IAAQ;IAAY,QAAQ,wBAAwB;;;EAGtD,SAAS,mBAAmB,oBAAoB;;KAGtC;WACD;WACA;;WAEA;WACA;;cAGE,qCAAqC;mBAC/B;mBACA;mBACA;mBACA;mBACA;EAGf,YAAA,QAAQ,iBACR,UAAS;EASL,QACJ,kBACA,UAAU,gCACT,QAAQ;EAiBL,OAAO,kBAAkB,OAAO,aAAa;EA8B7C,SACJ,kBACA,0BACA,iBACC;EA6BI,KACL,kBACA,gBACA,QAAQ,cACP,cAAc;EA2CX,OAAO,mBAAmB,QAAQ;EAWlC,OAAO,mBAAmB;UAKlB;UAQN;UAIA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AsyncIterableStream.d.ts","names":[],"sources":["../../src/utils/AsyncIterableStream.ts"],"mappings":";KAAY,oBAAoB,KAAK,cAAc,KAAK,eAAe;
|
|
1
|
+
{"version":3,"file":"AsyncIterableStream.d.ts","names":[],"sources":["../../src/utils/AsyncIterableStream.ts"],"mappings":";KAAY,oBAAoB,KAAK,cAAc,KAAK,eAAe;iBAgCvD,sBAAsB,GACpC,QAAQ,eAAe,KACtB,oBAAoB"}
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
//#region src/utils/AsyncIterableStream.ts
|
|
2
2
|
async function* streamGeneratorPolyfill() {
|
|
3
3
|
const reader = this.getReader();
|
|
4
|
+
let shouldCancel = true;
|
|
4
5
|
try {
|
|
5
6
|
while (true) {
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
let result;
|
|
8
|
+
try {
|
|
9
|
+
result = await reader.read();
|
|
10
|
+
} catch (error) {
|
|
11
|
+
shouldCancel = false;
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
if (result.done) {
|
|
15
|
+
shouldCancel = false;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
const { value } = result;
|
|
8
19
|
yield value;
|
|
9
20
|
}
|
|
10
21
|
} finally {
|
|
11
|
-
|
|
22
|
+
try {
|
|
23
|
+
if (shouldCancel) await reader.cancel();
|
|
24
|
+
} finally {
|
|
25
|
+
reader.releaseLock();
|
|
26
|
+
}
|
|
12
27
|
}
|
|
13
28
|
}
|
|
14
29
|
function asAsyncIterableStream(source) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AsyncIterableStream.js","names":[],"sources":["../../src/utils/AsyncIterableStream.ts"],"sourcesContent":["export type AsyncIterableStream<T> = AsyncIterable<T> & ReadableStream<T>;\n\nasync function* streamGeneratorPolyfill<T>(\n this: ReadableStream<T>,\n): AsyncGenerator<T, undefined, unknown> {\n const reader = this.getReader();\n try {\n while (true) {\n
|
|
1
|
+
{"version":3,"file":"AsyncIterableStream.js","names":[],"sources":["../../src/utils/AsyncIterableStream.ts"],"sourcesContent":["export type AsyncIterableStream<T> = AsyncIterable<T> & ReadableStream<T>;\n\nasync function* streamGeneratorPolyfill<T>(\n this: ReadableStream<T>,\n): AsyncGenerator<T, undefined, unknown> {\n const reader = this.getReader();\n let shouldCancel = true;\n try {\n while (true) {\n let result: ReadableStreamReadResult<T>;\n try {\n result = await reader.read();\n } catch (error) {\n shouldCancel = false;\n throw error;\n }\n if (result.done) {\n shouldCancel = false;\n break;\n }\n const { value } = result;\n yield value;\n }\n } finally {\n try {\n if (shouldCancel) await reader.cancel();\n } finally {\n reader.releaseLock();\n }\n }\n}\n\nexport function asAsyncIterableStream<T>(\n source: ReadableStream<T>,\n): AsyncIterableStream<T> {\n (source as AsyncIterableStream<T>)[Symbol.asyncIterator] ??=\n streamGeneratorPolyfill;\n return source as AsyncIterableStream<T>;\n}\n"],"mappings":";AAEA,gBAAgB,0BAEyB;CACvC,MAAM,SAAS,KAAK,UAAU;CAC9B,IAAI,eAAe;CACnB,IAAI;EACF,OAAO,MAAM;GACX,IAAI;GACJ,IAAI;IACF,SAAS,MAAM,OAAO,KAAK;GAC7B,SAAS,OAAO;IACd,eAAe;IACf,MAAM;GACR;GACA,IAAI,OAAO,MAAM;IACf,eAAe;IACf;GACF;GACA,MAAM,EAAE,UAAU;GAClB,MAAM;EACR;CACF,UAAU;EACR,IAAI;GACF,IAAI,cAAc,MAAM,OAAO,OAAO;EACxC,UAAU;GACR,OAAO,YAAY;EACrB;CACF;AACF;AAEA,SAAgB,sBACd,QACwB;CACxB,OAAmC,OAAO,mBACxC;CACF,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"promiseWithResolvers.d.ts","names":[],"sources":["../../src/utils/promiseWithResolvers.ts"],"mappings":";cAAa,uBAAwB;;
|
|
1
|
+
{"version":3,"file":"promiseWithResolvers.d.ts","names":[],"sources":["../../src/utils/promiseWithResolvers.ts"],"mappings":";cAAa,uBAAwB;;EACb,UAAA,OAAA,IAAI,YAAY;EACf,SAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assistant-stream",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.27",
|
|
4
4
|
"description": "Streaming utilities for AI assistants",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"ioredis": "^5.11.1",
|
|
68
68
|
"redis": "^6.1.0",
|
|
69
69
|
"vitest": "^4.1.10",
|
|
70
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
70
|
+
"@assistant-ui/x-buildutils": "0.0.19"
|
|
71
71
|
},
|
|
72
72
|
"publishConfig": {
|
|
73
73
|
"access": "public",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReadonlyJSONValue } from "../utils/json/json-value";
|
|
2
|
-
import type {
|
|
2
|
+
import type { AssistantTransportStateOperation } from "./gorp/types";
|
|
3
3
|
import type { ToolModelContentPart } from "./tool/tool-types";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -136,8 +136,8 @@ export type AssistantStreamChunk = { readonly path: readonly number[] } & (
|
|
|
136
136
|
readonly severity?: "critical" | "warning" | "info";
|
|
137
137
|
}
|
|
138
138
|
| {
|
|
139
|
-
/** Applies
|
|
139
|
+
/** Applies gorp-stream operations to state carried by this stream. */
|
|
140
140
|
readonly type: "update-state";
|
|
141
|
-
readonly operations:
|
|
141
|
+
readonly operations: AssistantTransportStateOperation[];
|
|
142
142
|
}
|
|
143
143
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
import { AssistantMessageAccumulator } from "./assistant-message-accumulator";
|
|
3
3
|
import type { AssistantStreamChunk } from "../AssistantStreamChunk";
|
|
4
4
|
import type { AssistantMessage } from "../utils/types";
|
|
@@ -224,6 +224,88 @@ describe("AssistantMessageAccumulator timing", () => {
|
|
|
224
224
|
expect(last.metadata.timing).toBeDefined();
|
|
225
225
|
expect(last.metadata.timing!.firstTokenTime).toBeTypeOf("number");
|
|
226
226
|
});
|
|
227
|
+
|
|
228
|
+
it("does not record firstTokenTime when a text-delta is dropped for an out-of-range path", async () => {
|
|
229
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
230
|
+
const messages = await collectStream([
|
|
231
|
+
{ type: "part-start", path: [0], part: { type: "text" } },
|
|
232
|
+
{ type: "text-delta", path: [5], textDelta: "x" },
|
|
233
|
+
]);
|
|
234
|
+
const last = messages.at(-1)!;
|
|
235
|
+
|
|
236
|
+
expect(last.metadata.timing).toBeDefined();
|
|
237
|
+
expect(last.metadata.timing!.firstTokenTime).toBeUndefined();
|
|
238
|
+
warn.mockRestore();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("does not record firstTokenTime when a text-delta is dropped for a nested path", async () => {
|
|
242
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
243
|
+
const messages = await collectStream([
|
|
244
|
+
{ type: "part-start", path: [0], part: { type: "text" } },
|
|
245
|
+
{ type: "text-delta", path: [0, 1], textDelta: "x" },
|
|
246
|
+
]);
|
|
247
|
+
const last = messages.at(-1)!;
|
|
248
|
+
|
|
249
|
+
expect(last.metadata.timing).toBeDefined();
|
|
250
|
+
expect(last.metadata.timing!.firstTokenTime).toBeUndefined();
|
|
251
|
+
warn.mockRestore();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("does not record firstTokenTime when a text-delta is dropped for a wrong part type", async () => {
|
|
255
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
256
|
+
const messages = await collectStream([
|
|
257
|
+
{
|
|
258
|
+
type: "part-start",
|
|
259
|
+
path: [0],
|
|
260
|
+
part: {
|
|
261
|
+
type: "source",
|
|
262
|
+
sourceType: "url",
|
|
263
|
+
id: "s1",
|
|
264
|
+
url: "https://example.com",
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
{ type: "text-delta", path: [0], textDelta: "x" },
|
|
268
|
+
]);
|
|
269
|
+
const last = messages.at(-1)!;
|
|
270
|
+
|
|
271
|
+
expect(last.metadata.timing).toBeDefined();
|
|
272
|
+
expect(last.metadata.timing!.firstTokenTime).toBeUndefined();
|
|
273
|
+
warn.mockRestore();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("records firstTokenTime on the first applied text-delta, not a preceding dropped one", async () => {
|
|
277
|
+
vi.useFakeTimers();
|
|
278
|
+
vi.setSystemTime(1000);
|
|
279
|
+
|
|
280
|
+
const accumulator = new AssistantMessageAccumulator();
|
|
281
|
+
const writer = accumulator.writable.getWriter();
|
|
282
|
+
const messages: AssistantMessage[] = [];
|
|
283
|
+
const reading = (async () => {
|
|
284
|
+
const reader = accumulator.readable.getReader();
|
|
285
|
+
for (;;) {
|
|
286
|
+
const { done, value } = await reader.read();
|
|
287
|
+
if (done) break;
|
|
288
|
+
messages.push(value);
|
|
289
|
+
}
|
|
290
|
+
})();
|
|
291
|
+
|
|
292
|
+
await writer.write({
|
|
293
|
+
type: "part-start",
|
|
294
|
+
path: [0],
|
|
295
|
+
part: { type: "text" },
|
|
296
|
+
});
|
|
297
|
+
vi.setSystemTime(2000);
|
|
298
|
+
await writer.write({ type: "text-delta", path: [5], textDelta: "x" });
|
|
299
|
+
vi.setSystemTime(3000);
|
|
300
|
+
await writer.write({ type: "text-delta", path: [0], textDelta: "hi" });
|
|
301
|
+
await writer.close();
|
|
302
|
+
await reading;
|
|
303
|
+
vi.useRealTimers();
|
|
304
|
+
|
|
305
|
+
const last = messages.at(-1)!;
|
|
306
|
+
expect(last.metadata.timing).toBeDefined();
|
|
307
|
+
expect(last.metadata.timing!.firstTokenTime).toBe(2000);
|
|
308
|
+
});
|
|
227
309
|
});
|
|
228
310
|
|
|
229
311
|
describe("AssistantMessageAccumulator error chunks", () => {
|
|
@@ -240,6 +322,19 @@ describe("AssistantMessageAccumulator error chunks", () => {
|
|
|
240
322
|
});
|
|
241
323
|
});
|
|
242
324
|
|
|
325
|
+
it("defaults a missing error message", async () => {
|
|
326
|
+
const messages = await collectStream([
|
|
327
|
+
{ type: "error", path: [] } as unknown as AssistantStreamChunk,
|
|
328
|
+
]);
|
|
329
|
+
const last = messages.at(-1)!;
|
|
330
|
+
|
|
331
|
+
expect(last.status).toEqual({
|
|
332
|
+
type: "incomplete",
|
|
333
|
+
reason: "error",
|
|
334
|
+
error: { code: "unknown", message: "unknown error" },
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
243
338
|
it("carries code and severity through unchanged", async () => {
|
|
244
339
|
const messages = await collectStream([
|
|
245
340
|
{
|
|
@@ -288,3 +383,281 @@ describe("AssistantMessageAccumulator error chunks", () => {
|
|
|
288
383
|
});
|
|
289
384
|
});
|
|
290
385
|
});
|
|
386
|
+
|
|
387
|
+
describe("AssistantMessageAccumulator part path bounds", () => {
|
|
388
|
+
it("drops a text-delta whose path is out of range with a warning", async () => {
|
|
389
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
390
|
+
const messages = await collectStream([
|
|
391
|
+
{ type: "part-start", path: [0], part: { type: "text" } },
|
|
392
|
+
{ type: "text-delta", path: [5], textDelta: "x" },
|
|
393
|
+
{ type: "text-delta", path: [0], textDelta: "hi" },
|
|
394
|
+
]);
|
|
395
|
+
const last = messages.at(-1)!;
|
|
396
|
+
|
|
397
|
+
expect(last.parts).toHaveLength(1);
|
|
398
|
+
expect(last.parts[0]).toMatchObject({ type: "text", text: "hi" });
|
|
399
|
+
expect(warn).toHaveBeenCalledWith(
|
|
400
|
+
"Dropped text-delta chunk: no part at path [5]",
|
|
401
|
+
);
|
|
402
|
+
warn.mockRestore();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("drops an out-of-range part-finish instead of fabricating a part", async () => {
|
|
406
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
407
|
+
const messages = await collectStream([
|
|
408
|
+
{ type: "part-start", path: [0], part: { type: "text" } },
|
|
409
|
+
{ type: "part-finish", path: [3] },
|
|
410
|
+
]);
|
|
411
|
+
const last = messages.at(-1)!;
|
|
412
|
+
|
|
413
|
+
expect(last.parts).toHaveLength(1);
|
|
414
|
+
expect(last.parts[0]!.type).toBe("text");
|
|
415
|
+
warn.mockRestore();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("drops part chunks when no parts exist", async () => {
|
|
419
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
420
|
+
const messages = await collectStream([
|
|
421
|
+
{ type: "text-delta", path: [0], textDelta: "x" },
|
|
422
|
+
{ type: "part-start", path: [0], part: { type: "text" } },
|
|
423
|
+
{ type: "text-delta", path: [0], textDelta: "ok" },
|
|
424
|
+
]);
|
|
425
|
+
const last = messages.at(-1)!;
|
|
426
|
+
|
|
427
|
+
expect(last.parts).toHaveLength(1);
|
|
428
|
+
expect(last.parts[0]).toMatchObject({ type: "text", text: "ok" });
|
|
429
|
+
warn.mockRestore();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("drops chunks with nested paths", async () => {
|
|
433
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
434
|
+
const messages = await collectStream([
|
|
435
|
+
{ type: "part-start", path: [0], part: { type: "text" } },
|
|
436
|
+
{ type: "text-delta", path: [0, 1], textDelta: "x" },
|
|
437
|
+
{ type: "text-delta", path: [0], textDelta: "ok" },
|
|
438
|
+
]);
|
|
439
|
+
const last = messages.at(-1)!;
|
|
440
|
+
|
|
441
|
+
expect(last.parts).toHaveLength(1);
|
|
442
|
+
expect(last.parts[0]).toMatchObject({ type: "text", text: "ok" });
|
|
443
|
+
warn.mockRestore();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("drops an out-of-range result without touching existing tool calls", async () => {
|
|
447
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
448
|
+
const messages = await collectStream([
|
|
449
|
+
{
|
|
450
|
+
type: "part-start",
|
|
451
|
+
path: [0],
|
|
452
|
+
part: { type: "tool-call", toolCallId: "t1", toolName: "f" },
|
|
453
|
+
},
|
|
454
|
+
{ type: "text-delta", path: [0], textDelta: "{}" },
|
|
455
|
+
{ type: "result", path: [7], result: { ok: true }, isError: false },
|
|
456
|
+
{ type: "result", path: [0], result: { ok: true }, isError: false },
|
|
457
|
+
]);
|
|
458
|
+
const last = messages.at(-1)!;
|
|
459
|
+
|
|
460
|
+
expect(last.parts).toHaveLength(1);
|
|
461
|
+
expect(last.parts[0]).toMatchObject({
|
|
462
|
+
type: "tool-call",
|
|
463
|
+
state: "result",
|
|
464
|
+
result: { ok: true },
|
|
465
|
+
});
|
|
466
|
+
warn.mockRestore();
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe("AssistantMessageAccumulator wrong part type chunks", () => {
|
|
471
|
+
const sourcePart = {
|
|
472
|
+
type: "source",
|
|
473
|
+
sourceType: "url",
|
|
474
|
+
id: "s1",
|
|
475
|
+
url: "https://example.com",
|
|
476
|
+
} as const;
|
|
477
|
+
|
|
478
|
+
it("drops a text-delta addressed to a source part", async () => {
|
|
479
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
480
|
+
const messages = await collectStream([
|
|
481
|
+
{ type: "part-start", path: [0], part: sourcePart },
|
|
482
|
+
{ type: "text-delta", path: [0], textDelta: "x" },
|
|
483
|
+
]);
|
|
484
|
+
const last = messages.at(-1)!;
|
|
485
|
+
|
|
486
|
+
expect(last.parts[0]).toMatchObject({ type: "source", id: "s1" });
|
|
487
|
+
expect(warn).toHaveBeenCalledWith(
|
|
488
|
+
"Dropped text-delta chunk: part is neither text nor tool-call",
|
|
489
|
+
);
|
|
490
|
+
warn.mockRestore();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("drops a result addressed to a text part", async () => {
|
|
494
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
495
|
+
const messages = await collectStream([
|
|
496
|
+
{ type: "part-start", path: [0], part: { type: "text" } },
|
|
497
|
+
{ type: "text-delta", path: [0], textDelta: "hi" },
|
|
498
|
+
{ type: "result", path: [0], result: { ok: true }, isError: false },
|
|
499
|
+
]);
|
|
500
|
+
const last = messages.at(-1)!;
|
|
501
|
+
|
|
502
|
+
expect(last.parts[0]).toMatchObject({ type: "text", text: "hi" });
|
|
503
|
+
expect(warn).toHaveBeenCalledWith(
|
|
504
|
+
"Dropped result chunk: part is not a tool-call",
|
|
505
|
+
);
|
|
506
|
+
warn.mockRestore();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("drops an args-text-finish addressed to a text part", async () => {
|
|
510
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
511
|
+
const messages = await collectStream([
|
|
512
|
+
{ type: "part-start", path: [0], part: { type: "text" } },
|
|
513
|
+
{ type: "tool-call-args-text-finish", path: [0] },
|
|
514
|
+
]);
|
|
515
|
+
const last = messages.at(-1)!;
|
|
516
|
+
|
|
517
|
+
expect(last.parts[0]).toMatchObject({ type: "text" });
|
|
518
|
+
expect(warn).toHaveBeenCalledWith(
|
|
519
|
+
"Dropped tool-call-args-text-finish chunk: part is not a tool-call",
|
|
520
|
+
);
|
|
521
|
+
warn.mockRestore();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it("inserts a placeholder for an unsupported part-start so later part indices stay aligned", async () => {
|
|
525
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
526
|
+
const messages = await collectStream([
|
|
527
|
+
{
|
|
528
|
+
type: "part-start",
|
|
529
|
+
path: [0],
|
|
530
|
+
part: { type: "bogus" },
|
|
531
|
+
} as unknown as AssistantStreamChunk,
|
|
532
|
+
{ type: "part-start", path: [1], part: { type: "text" } },
|
|
533
|
+
{ type: "text-delta", path: [1], textDelta: "ok" },
|
|
534
|
+
{ type: "part-finish", path: [0] },
|
|
535
|
+
]);
|
|
536
|
+
const last = messages.at(-1)!;
|
|
537
|
+
|
|
538
|
+
expect(last.parts).toHaveLength(2);
|
|
539
|
+
expect(last.parts[0]).toMatchObject({
|
|
540
|
+
type: "reasoning",
|
|
541
|
+
text: "",
|
|
542
|
+
status: { type: "complete", reason: "unknown" },
|
|
543
|
+
});
|
|
544
|
+
expect(last.parts[1]).toMatchObject({ type: "text", text: "ok" });
|
|
545
|
+
expect(warn).toHaveBeenCalledWith(
|
|
546
|
+
"Unsupported part-start type bogus: inserting an empty reasoning part to preserve part indices",
|
|
547
|
+
);
|
|
548
|
+
warn.mockRestore();
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
describe("AssistantMessageAccumulator warn dedup", () => {
|
|
553
|
+
it("warns once per drop class per instance", async () => {
|
|
554
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
555
|
+
await collectStream([
|
|
556
|
+
{ type: "part-start", path: [0], part: { type: "text" } },
|
|
557
|
+
{ type: "text-delta", path: [5], textDelta: "a" },
|
|
558
|
+
{ type: "text-delta", path: [5], textDelta: "b" },
|
|
559
|
+
{ type: "text-delta", path: [6], textDelta: "c" },
|
|
560
|
+
{ type: "part-finish", path: [9] },
|
|
561
|
+
]);
|
|
562
|
+
|
|
563
|
+
expect(warn).toHaveBeenCalledTimes(2);
|
|
564
|
+
expect(warn).toHaveBeenCalledWith(
|
|
565
|
+
"Dropped text-delta chunk: no part at path [5]",
|
|
566
|
+
);
|
|
567
|
+
expect(warn).toHaveBeenCalledWith(
|
|
568
|
+
"Dropped part-finish chunk: no part at path [9]",
|
|
569
|
+
);
|
|
570
|
+
warn.mockRestore();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("resets dedup per accumulator instance", async () => {
|
|
574
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
575
|
+
const stream = [
|
|
576
|
+
{ type: "part-start", path: [0], part: { type: "text" } },
|
|
577
|
+
{ type: "text-delta", path: [5], textDelta: "x" },
|
|
578
|
+
] satisfies AssistantStreamChunk[];
|
|
579
|
+
await collectStream(stream);
|
|
580
|
+
await collectStream(stream);
|
|
581
|
+
|
|
582
|
+
expect(warn).toHaveBeenCalledTimes(2);
|
|
583
|
+
warn.mockRestore();
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("dedupes repeated wrong-part-type drops", async () => {
|
|
587
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
588
|
+
await collectStream([
|
|
589
|
+
{
|
|
590
|
+
type: "part-start",
|
|
591
|
+
path: [0],
|
|
592
|
+
part: { type: "source", sourceType: "url", id: "s", url: "https://x" },
|
|
593
|
+
},
|
|
594
|
+
{ type: "text-delta", path: [0], textDelta: "a" },
|
|
595
|
+
{ type: "text-delta", path: [0], textDelta: "b" },
|
|
596
|
+
]);
|
|
597
|
+
|
|
598
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
599
|
+
warn.mockRestore();
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
describe("AssistantMessageAccumulator warn dedup key independence", () => {
|
|
604
|
+
it("keeps drop classes as independent keys within one stream", async () => {
|
|
605
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
606
|
+
await collectStream([
|
|
607
|
+
{
|
|
608
|
+
type: "part-start",
|
|
609
|
+
path: [0],
|
|
610
|
+
part: { type: "source", sourceType: "url", id: "s", url: "https://x" },
|
|
611
|
+
},
|
|
612
|
+
{ type: "text-delta", path: [0], textDelta: "wrong-part" },
|
|
613
|
+
{ type: "text-delta", path: [7], textDelta: "no-part" },
|
|
614
|
+
]);
|
|
615
|
+
|
|
616
|
+
expect(warn).toHaveBeenCalledTimes(2);
|
|
617
|
+
warn.mockRestore();
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it("keys unsupported part-starts per type", async () => {
|
|
621
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
622
|
+
const messages = await collectStream([
|
|
623
|
+
{
|
|
624
|
+
type: "part-start",
|
|
625
|
+
path: [0],
|
|
626
|
+
part: { type: "bogus" },
|
|
627
|
+
} as unknown as AssistantStreamChunk,
|
|
628
|
+
{
|
|
629
|
+
type: "part-start",
|
|
630
|
+
path: [1],
|
|
631
|
+
part: { type: "bogus" },
|
|
632
|
+
} as unknown as AssistantStreamChunk,
|
|
633
|
+
{
|
|
634
|
+
type: "part-start",
|
|
635
|
+
path: [2],
|
|
636
|
+
part: { type: "video" },
|
|
637
|
+
} as unknown as AssistantStreamChunk,
|
|
638
|
+
]);
|
|
639
|
+
const last = messages.at(-1)!;
|
|
640
|
+
|
|
641
|
+
expect(last.parts).toHaveLength(3);
|
|
642
|
+
expect(warn).toHaveBeenCalledTimes(2);
|
|
643
|
+
warn.mockRestore();
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("caps the number of warned keys per instance", async () => {
|
|
647
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
648
|
+
await collectStream(
|
|
649
|
+
Array.from(
|
|
650
|
+
{ length: 20 },
|
|
651
|
+
(_, i) =>
|
|
652
|
+
({
|
|
653
|
+
type: "part-start",
|
|
654
|
+
path: [i],
|
|
655
|
+
part: { type: `bogus-${i}` },
|
|
656
|
+
}) as unknown as AssistantStreamChunk,
|
|
657
|
+
),
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
expect(warn).toHaveBeenCalledTimes(16);
|
|
661
|
+
warn.mockRestore();
|
|
662
|
+
});
|
|
663
|
+
});
|