assistant-stream 0.3.25 → 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/README.md +4 -1
- package/dist/core/AssistantStream.d.ts +0 -1
- package/dist/core/AssistantStream.d.ts.map +1 -1
- package/dist/core/AssistantStreamChunk.d.ts +26 -14
- package/dist/core/AssistantStreamChunk.d.ts.map +1 -1
- package/dist/core/accumulators/AssistantMessageStream.d.ts +0 -1
- package/dist/core/accumulators/AssistantMessageStream.d.ts.map +1 -1
- package/dist/core/accumulators/TimingTracker.d.ts +0 -1
- package/dist/core/accumulators/TimingTracker.d.ts.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.d.ts +3 -9
- package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.js +70 -28
- package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
- package/dist/core/converters/toGenericMessages.d.ts.map +1 -1
- package/dist/core/converters/toGenericMessages.js +2 -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/gorp/types.d.ts +20 -0
- package/dist/core/gorp/types.d.ts.map +1 -0
- package/dist/core/modules/assistant-stream.d.ts +18 -11
- 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/text.d.ts +0 -1
- package/dist/core/modules/text.d.ts.map +1 -1
- package/dist/core/modules/tool-call.d.ts +0 -1
- package/dist/core/modules/tool-call.d.ts.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/PlainText.d.ts +0 -1
- package/dist/core/serialization/PlainText.d.ts.map +1 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts +0 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts.map +1 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.js +93 -53
- package/dist/core/serialization/assistant-transport/AssistantTransport.js.map +1 -1
- package/dist/core/serialization/data-stream/DataStream.d.ts +0 -1
- package/dist/core/serialization/data-stream/DataStream.d.ts.map +1 -1
- package/dist/core/serialization/data-stream/chunk-types.d.ts +3 -4
- package/dist/core/serialization/data-stream/chunk-types.d.ts.map +1 -1
- package/dist/core/serialization/data-stream/chunk-types.js.map +1 -1
- package/dist/core/serialization/data-stream/serialization.d.ts +0 -1
- package/dist/core/serialization/data-stream/serialization.d.ts.map +1 -1
- package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts +0 -1
- package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts.map +1 -1
- package/dist/core/serialization/ui-message-stream/UIMessageStream.js +76 -53
- package/dist/core/serialization/ui-message-stream/UIMessageStream.js.map +1 -1
- package/dist/core/serialization/ui-message-stream/chunk-types.d.ts +0 -1
- package/dist/core/serialization/ui-message-stream/chunk-types.d.ts.map +1 -1
- package/dist/core/tool/ToolCallReader.d.ts +1 -0
- 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 +0 -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 +6 -4
- package/dist/core/tool/ToolResponse.d.ts.map +1 -1
- package/dist/core/tool/schema-utils.d.ts +0 -1
- package/dist/core/tool/schema-utils.d.ts.map +1 -1
- package/dist/core/tool/tool-types.d.ts +71 -33
- package/dist/core/tool/tool-types.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.d.ts +3 -2
- 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/tool/type-path-utils.d.ts +2 -2
- package/dist/core/tool/type-path-utils.d.ts.map +1 -1
- package/dist/core/utils/Counter.d.ts.map +1 -1
- package/dist/core/utils/generateId.d.ts.map +1 -1
- package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts +0 -1
- package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts.map +1 -1
- package/dist/core/utils/stream/AssistantTransformStream.d.ts +0 -1
- package/dist/core/utils/stream/AssistantTransformStream.d.ts.map +1 -1
- package/dist/core/utils/stream/LineDecoderStream.d.ts +1 -0
- package/dist/core/utils/stream/LineDecoderStream.d.ts.map +1 -1
- package/dist/core/utils/stream/LineDecoderStream.js +19 -6
- package/dist/core/utils/stream/LineDecoderStream.js.map +1 -1
- package/dist/core/utils/stream/PipeableTransformStream.d.ts.map +1 -1
- package/dist/core/utils/stream/SSE.d.ts +0 -1
- package/dist/core/utils/stream/SSE.d.ts.map +1 -1
- package/dist/core/utils/stream/SSE.js +2 -52
- package/dist/core/utils/stream/SSE.js.map +1 -1
- package/dist/core/utils/stream/SSEEventDecoder.d.ts +27 -0
- package/dist/core/utils/stream/SSEEventDecoder.d.ts.map +1 -0
- package/dist/core/utils/stream/SSEEventDecoder.js +84 -0
- package/dist/core/utils/stream/SSEEventDecoder.js.map +1 -0
- package/dist/core/utils/stream/SSEEventDecoderStream.d.ts +13 -0
- package/dist/core/utils/stream/SSEEventDecoderStream.d.ts.map +1 -0
- package/dist/core/utils/stream/SSEEventDecoderStream.js +24 -0
- package/dist/core/utils/stream/SSEEventDecoderStream.js.map +1 -0
- package/dist/core/utils/stream/UnderlyingReadable.d.ts.map +1 -1
- package/dist/core/utils/stream/merge.d.ts +2 -1
- 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 +0 -1
- package/dist/core/utils/stream/path-utils.d.ts.map +1 -1
- package/dist/core/utils/types.d.ts +16 -8
- package/dist/core/utils/types.d.ts.map +1 -1
- package/dist/core/utils/withPromiseOrValue.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/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
- package/dist/resumable/ResumableStreamContext.d.ts +0 -1
- 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/constants.d.ts.map +1 -1
- package/dist/resumable/createResumableAssistantStreamResponse.d.ts +6 -4
- package/dist/resumable/createResumableAssistantStreamResponse.d.ts.map +1 -1
- package/dist/resumable/errors.d.ts.map +1 -1
- package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts +0 -1
- package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts.map +1 -1
- package/dist/resumable/stores/InMemoryResumableStreamStore.js +2 -1
- package/dist/resumable/stores/InMemoryResumableStreamStore.js.map +1 -1
- package/dist/resumable/stores/ioredis.d.ts +0 -1
- package/dist/resumable/stores/ioredis.d.ts.map +1 -1
- package/dist/resumable/stores/redis-impl.d.ts +2 -2
- package/dist/resumable/stores/redis-impl.d.ts.map +1 -1
- package/dist/resumable/stores/redis.d.ts +0 -1
- package/dist/resumable/stores/redis.d.ts.map +1 -1
- package/dist/resumable/types.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/json/fix-json.d.ts.map +1 -1
- package/dist/utils/json/json-value.d.ts.map +1 -1
- package/dist/utils/json/parse-partial-json-object.d.ts +0 -1
- package/dist/utils/json/parse-partial-json-object.d.ts.map +1 -1
- package/dist/utils/promiseWithResolvers.d.ts.map +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +2 -1
- package/package.json +5 -5
- package/src/core/AssistantStreamChunk.ts +5 -3
- package/src/core/accumulators/assistant-message-accumulator.test.ts +437 -1
- package/src/core/accumulators/assistant-message-accumulator.ts +89 -26
- package/src/core/{object/ObjectStream.test.ts → gorp/GorpStream.test.ts} +104 -23
- 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/gorp/GorpStreamResponse.test.ts +32 -0
- 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 +215 -1
- package/src/core/serialization/assistant-transport/AssistantTransport.ts +83 -71
- package/src/core/serialization/data-stream/chunk-types.ts +2 -2
- package/src/core/serialization/ui-message-stream/UIMessageStream.test.ts +323 -4
- package/src/core/serialization/ui-message-stream/UIMessageStream.ts +81 -71
- 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/LineDecoderStream.test.ts +26 -2
- package/src/core/utils/stream/LineDecoderStream.ts +21 -11
- package/src/core/utils/stream/SSE.ts +6 -70
- package/src/core/utils/stream/SSEEventDecoder.test.ts +146 -0
- package/src/core/utils/stream/SSEEventDecoder.ts +109 -0
- package/src/core/utils/stream/SSEEventDecoderStream.test.ts +42 -0
- package/src/core/utils/stream/SSEEventDecoderStream.ts +34 -0
- 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/src/utils.ts +4 -0
- 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 -17
- 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 -19
- 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 +0 -18
- package/dist/core/object/types.d.ts.map +0 -1
- package/dist/utils/json/is-json.d.ts +0 -9
- package/dist/utils/json/is-json.d.ts.map +0 -1
- package/dist/utils/json/is-json.js +0 -17
- package/dist/utils/json/is-json.js.map +0 -1
- package/src/core/object/ObjectStreamResponse.ts +0 -109
- package/src/utils/json/is-json.ts +0 -43
- /package/dist/core/{object → gorp}/types.js +0 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
diffKeys,
|
|
4
|
+
lookupChange,
|
|
5
|
+
markChanged,
|
|
6
|
+
mergeChanged,
|
|
7
|
+
type ChangeNode,
|
|
8
|
+
} from "./changeTree";
|
|
9
|
+
|
|
10
|
+
describe("markChanged", () => {
|
|
11
|
+
it("collapses to true when path is empty", () => {
|
|
12
|
+
expect(markChanged({}, [])).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("keeps `true` after attempting to descend into it", () => {
|
|
16
|
+
const node: ChangeNode = { a: true };
|
|
17
|
+
const result = markChanged(node, ["a", "b"]);
|
|
18
|
+
expect(result).toBe(node);
|
|
19
|
+
expect(node.a).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("overwrites a nested subtree with `true` when the path collapses to the parent", () => {
|
|
23
|
+
const node: ChangeNode = { a: { b: true } };
|
|
24
|
+
markChanged(node, ["a"]);
|
|
25
|
+
expect(node.a).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("creates intermediate nodes lazily without disturbing siblings", () => {
|
|
29
|
+
const node: ChangeNode = { a: { existing: true } };
|
|
30
|
+
markChanged(node, ["a", "b", "c"]);
|
|
31
|
+
expect(node).toEqual({
|
|
32
|
+
a: { existing: true, b: { c: true } },
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("is idempotent when the same leaf is marked twice", () => {
|
|
37
|
+
const node: ChangeNode = {};
|
|
38
|
+
markChanged(node, ["a", "b"]);
|
|
39
|
+
markChanged(node, ["a", "b"]);
|
|
40
|
+
expect(node).toEqual({ a: { b: true } });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("rejects unsafe path segments", () => {
|
|
44
|
+
const node: ChangeNode = {};
|
|
45
|
+
expect(() => markChanged(node, ["__proto__", "polluted"])).toThrow(
|
|
46
|
+
/Unsafe gorp path segment/,
|
|
47
|
+
);
|
|
48
|
+
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("mergeChanged", () => {
|
|
53
|
+
it("returns true when either side is true", () => {
|
|
54
|
+
expect(mergeChanged(true, {})).toBe(true);
|
|
55
|
+
expect(mergeChanged({}, true)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("merges disjoint subtrees in place", () => {
|
|
59
|
+
const target: ChangeNode = { a: true };
|
|
60
|
+
const source: ChangeNode = { b: true };
|
|
61
|
+
const result = mergeChanged(target, source);
|
|
62
|
+
expect(result).toBe(target);
|
|
63
|
+
expect(target).toEqual({ a: true, b: true });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("clones source subtrees before assigning them", () => {
|
|
67
|
+
const target: ChangeNode = {};
|
|
68
|
+
const source: ChangeNode = { a: { b: true } };
|
|
69
|
+
mergeChanged(target, source);
|
|
70
|
+
(source as { a: { c?: true } }).a.c = true;
|
|
71
|
+
expect(target).toEqual({ a: { b: true } });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("deep-merges overlapping subtrees", () => {
|
|
75
|
+
const target: ChangeNode = { a: { x: true } };
|
|
76
|
+
const source: ChangeNode = { a: { y: true } };
|
|
77
|
+
mergeChanged(target, source);
|
|
78
|
+
expect(target).toEqual({ a: { x: true, y: true } });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("propagates `true` upward when one side has it at a shared key", () => {
|
|
82
|
+
const target: ChangeNode = { a: { x: true } };
|
|
83
|
+
const source: ChangeNode = { a: true };
|
|
84
|
+
mergeChanged(target, source);
|
|
85
|
+
expect(target).toEqual({ a: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("rejects unsafe source keys", () => {
|
|
89
|
+
const target: ChangeNode = {};
|
|
90
|
+
const source = JSON.parse('{"__proto__":true}') as ChangeNode;
|
|
91
|
+
expect(() => mergeChanged(target, source)).toThrow(
|
|
92
|
+
/Unsafe gorp path segment/,
|
|
93
|
+
);
|
|
94
|
+
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("rejects nested unsafe source keys before assigning a subtree", () => {
|
|
98
|
+
const target: ChangeNode = {};
|
|
99
|
+
const source = JSON.parse('{"a":{"constructor":true}}') as ChangeNode;
|
|
100
|
+
expect(() => mergeChanged(target, source)).toThrow(
|
|
101
|
+
/Unsafe gorp path segment/,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("lookupChange", () => {
|
|
107
|
+
it("returns false for paths that don't exist", () => {
|
|
108
|
+
expect(lookupChange({ a: true }, ["b"])).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns true when traversal hits a `true` sentinel", () => {
|
|
112
|
+
expect(lookupChange({ a: true }, ["a", "b", "c"])).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("returns the subtree when the path lands on an interior node", () => {
|
|
116
|
+
const node: ChangeNode = { a: { b: true } };
|
|
117
|
+
const result = lookupChange(node, ["a"]);
|
|
118
|
+
expect(result).toEqual({ b: true });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("returns the root node for the empty path", () => {
|
|
122
|
+
const node: ChangeNode = { a: true };
|
|
123
|
+
expect(lookupChange(node, [])).toBe(node);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("returns false for inherited object members", () => {
|
|
127
|
+
expect(lookupChange({}, ["toString"])).toBe(false);
|
|
128
|
+
expect(lookupChange({ a: {} }, ["a", "hasOwnProperty"])).toBe(false);
|
|
129
|
+
expect(lookupChange({ a: true }, ["constructor"])).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("inherited object members", () => {
|
|
134
|
+
it("markChanged does not descend into inherited members", () => {
|
|
135
|
+
const node: ChangeNode = {};
|
|
136
|
+
markChanged(node, ["toString", "x"]);
|
|
137
|
+
expect(node).toEqual({ toString: { x: true } });
|
|
138
|
+
expect(lookupChange(node, ["toString", "x"])).toBe(true);
|
|
139
|
+
expect(lookupChange(node, ["valueOf"])).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("mergeChanged ignores inherited target members", () => {
|
|
143
|
+
const target: ChangeNode = {};
|
|
144
|
+
const source: ChangeNode = { toString: { x: true } };
|
|
145
|
+
mergeChanged(target, source);
|
|
146
|
+
expect(target).toEqual({ toString: { x: true } });
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("diffKeys", () => {
|
|
151
|
+
it("returns new keys in order, then deleted keys in reverse", () => {
|
|
152
|
+
expect(diffKeys({ b: 1, c: 2 }, { a: 1, b: 2, d: 3 })).toEqual([
|
|
153
|
+
"b",
|
|
154
|
+
"c",
|
|
155
|
+
"d",
|
|
156
|
+
"a",
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("returns new keys when the old value is not an object", () => {
|
|
161
|
+
expect(diffKeys({ a: 1 }, null)).toEqual(["a"]);
|
|
162
|
+
expect(diffKeys({ a: 1 }, "str")).toEqual(["a"]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("returns deleted keys when the new value is not an object", () => {
|
|
166
|
+
expect(diffKeys(null, { a: 1, b: 2 })).toEqual(["b", "a"]);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export type ChangeNode = true | { [key: string]: ChangeNode };
|
|
2
|
+
|
|
3
|
+
export function createChangeNode(): { [key: string]: ChangeNode } {
|
|
4
|
+
return Object.create(null);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const UNSAFE_PATH_SEGMENTS = new Set(["__proto__", "constructor", "prototype"]);
|
|
8
|
+
|
|
9
|
+
export function assertSafePathSegment(segment: string): void {
|
|
10
|
+
if (UNSAFE_PATH_SEGMENTS.has(segment)) {
|
|
11
|
+
throw new Error(`Unsafe gorp path segment: ${segment}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function markChanged(
|
|
16
|
+
node: ChangeNode,
|
|
17
|
+
path: readonly string[],
|
|
18
|
+
): ChangeNode {
|
|
19
|
+
if (node === true || path.length === 0) return true;
|
|
20
|
+
let cursor = node;
|
|
21
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
22
|
+
const key = path[i]!;
|
|
23
|
+
assertSafePathSegment(key);
|
|
24
|
+
const next = Object.hasOwn(cursor, key) ? cursor[key] : undefined;
|
|
25
|
+
if (next === true) return node;
|
|
26
|
+
if (next === undefined) cursor[key] = createChangeNode();
|
|
27
|
+
cursor = cursor[key] as { [k: string]: ChangeNode };
|
|
28
|
+
}
|
|
29
|
+
const leaf = path[path.length - 1]!;
|
|
30
|
+
assertSafePathSegment(leaf);
|
|
31
|
+
cursor[leaf] = true;
|
|
32
|
+
return node;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function mergeChanged(
|
|
36
|
+
target: ChangeNode,
|
|
37
|
+
source: ChangeNode,
|
|
38
|
+
): ChangeNode {
|
|
39
|
+
if (target === true || source === true) return true;
|
|
40
|
+
for (const key of Object.keys(source)) {
|
|
41
|
+
assertSafePathSegment(key);
|
|
42
|
+
const sChild = source[key]!;
|
|
43
|
+
const tChild = Object.hasOwn(target, key) ? target[key] : undefined;
|
|
44
|
+
target[key] =
|
|
45
|
+
tChild === undefined
|
|
46
|
+
? cloneChangeNode(sChild)
|
|
47
|
+
: mergeChanged(tChild, sChild);
|
|
48
|
+
}
|
|
49
|
+
return target;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cloneChangeNode(source: ChangeNode): ChangeNode {
|
|
53
|
+
if (source === true) return true;
|
|
54
|
+
const clone = createChangeNode();
|
|
55
|
+
for (const key of Object.keys(source)) {
|
|
56
|
+
assertSafePathSegment(key);
|
|
57
|
+
clone[key] = cloneChangeNode(source[key]!);
|
|
58
|
+
}
|
|
59
|
+
return clone;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function lookupChange(
|
|
63
|
+
node: ChangeNode,
|
|
64
|
+
path: readonly string[],
|
|
65
|
+
): ChangeNode | false {
|
|
66
|
+
for (const key of path) {
|
|
67
|
+
if (node === true) return true;
|
|
68
|
+
const next = Object.hasOwn(node, key) ? node[key] : undefined;
|
|
69
|
+
if (next === undefined) return false;
|
|
70
|
+
node = next;
|
|
71
|
+
}
|
|
72
|
+
return node;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function lookupValue(state: unknown, path: readonly string[]): unknown {
|
|
76
|
+
let node = state;
|
|
77
|
+
for (const key of path) {
|
|
78
|
+
assertSafePathSegment(key);
|
|
79
|
+
if (typeof node !== "object" || node === null) return undefined;
|
|
80
|
+
node = Object.hasOwn(node, key)
|
|
81
|
+
? (node as Record<string, unknown>)[key]
|
|
82
|
+
: undefined;
|
|
83
|
+
}
|
|
84
|
+
return node;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function diffKeys(newNode: unknown, oldNode: unknown): string[] {
|
|
88
|
+
const newKeys =
|
|
89
|
+
typeof newNode === "object" && newNode !== null ? Object.keys(newNode) : [];
|
|
90
|
+
const oldKeys =
|
|
91
|
+
typeof oldNode === "object" && oldNode !== null ? Object.keys(oldNode) : [];
|
|
92
|
+
if (oldKeys.length === 0) return newKeys;
|
|
93
|
+
const newSet = new Set(newKeys);
|
|
94
|
+
const deletedKeys = oldKeys.filter((k) => !newSet.has(k)).reverse();
|
|
95
|
+
return [...newKeys, ...deletedKeys];
|
|
96
|
+
}
|
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
import type { ReadonlyJSONValue } from "../../utils";
|
|
2
2
|
import { withPromiseOrValue } from "../utils/withPromiseOrValue";
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
3
|
+
import { GorpStreamAccumulator } from "./GorpStreamAccumulator";
|
|
4
|
+
import type { GorpStreamOperation, GorpStreamChunk } from "./types";
|
|
5
5
|
|
|
6
|
-
type
|
|
6
|
+
type GorpStreamController = {
|
|
7
7
|
readonly abortSignal: AbortSignal;
|
|
8
8
|
|
|
9
|
-
enqueue(operations: readonly
|
|
9
|
+
enqueue(operations: readonly GorpStreamOperation[]): void;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
class
|
|
13
|
-
private _controller: ReadableStreamDefaultController<
|
|
12
|
+
class GorpStreamControllerImpl implements GorpStreamController {
|
|
13
|
+
private _controller: ReadableStreamDefaultController<GorpStreamChunk>;
|
|
14
14
|
private _abortController = new AbortController();
|
|
15
|
-
private _accumulator:
|
|
15
|
+
private _accumulator: GorpStreamAccumulator;
|
|
16
|
+
private _cancelled = false;
|
|
16
17
|
|
|
17
18
|
get abortSignal() {
|
|
18
19
|
return this._abortController.signal;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
constructor(
|
|
22
|
-
controller: ReadableStreamDefaultController<
|
|
23
|
+
controller: ReadableStreamDefaultController<GorpStreamChunk>,
|
|
23
24
|
defaultValue: ReadonlyJSONValue,
|
|
24
25
|
) {
|
|
25
26
|
this._controller = controller;
|
|
26
|
-
this._accumulator = new
|
|
27
|
+
this._accumulator = new GorpStreamAccumulator(defaultValue);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
enqueue(operations: readonly
|
|
30
|
+
enqueue(operations: readonly GorpStreamOperation[]) {
|
|
31
|
+
if (this._cancelled) return;
|
|
32
|
+
|
|
30
33
|
this._accumulator.append(operations);
|
|
31
34
|
|
|
32
35
|
this._controller.enqueue({
|
|
@@ -36,23 +39,26 @@ class ObjectStreamControllerImpl implements ObjectStreamController {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
__internalError(error: unknown) {
|
|
42
|
+
if (this._cancelled) return;
|
|
39
43
|
this._controller.error(error);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
__internalClose() {
|
|
47
|
+
if (this._cancelled) return;
|
|
43
48
|
this._controller.close();
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
__internalCancel(reason?: unknown) {
|
|
52
|
+
this._cancelled = true;
|
|
47
53
|
this._abortController.abort(reason);
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
const getStreamControllerPair = (defaultValue: ReadonlyJSONValue) => {
|
|
52
|
-
let controller!:
|
|
53
|
-
const stream = new ReadableStream<
|
|
58
|
+
let controller!: GorpStreamControllerImpl;
|
|
59
|
+
const stream = new ReadableStream<GorpStreamChunk>({
|
|
54
60
|
start(c) {
|
|
55
|
-
controller = new
|
|
61
|
+
controller = new GorpStreamControllerImpl(c, defaultValue);
|
|
56
62
|
},
|
|
57
63
|
cancel(reason: unknown) {
|
|
58
64
|
controller.__internalCancel(reason);
|
|
@@ -62,15 +68,15 @@ const getStreamControllerPair = (defaultValue: ReadonlyJSONValue) => {
|
|
|
62
68
|
return [stream, controller] as const;
|
|
63
69
|
};
|
|
64
70
|
|
|
65
|
-
type
|
|
66
|
-
execute: (controller:
|
|
71
|
+
type CreateGorpStreamOptions = {
|
|
72
|
+
execute: (controller: GorpStreamController) => void | PromiseLike<void>;
|
|
67
73
|
defaultValue?: ReadonlyJSONValue;
|
|
68
74
|
};
|
|
69
75
|
|
|
70
|
-
export const
|
|
76
|
+
export const createGorpStream = ({
|
|
71
77
|
execute,
|
|
72
78
|
defaultValue = {},
|
|
73
|
-
}:
|
|
79
|
+
}: CreateGorpStreamOptions) => {
|
|
74
80
|
const [stream, controller] = getStreamControllerPair(defaultValue);
|
|
75
81
|
|
|
76
82
|
withPromiseOrValue(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReadonlyJSONValue } from "../../utils";
|
|
2
2
|
|
|
3
|
-
export type
|
|
3
|
+
export type AssistantTransportStateOperation =
|
|
4
4
|
| {
|
|
5
5
|
readonly type: "set";
|
|
6
6
|
readonly path: readonly string[];
|
|
@@ -12,7 +12,9 @@ export type ObjectStreamOperation =
|
|
|
12
12
|
readonly value: string;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
export type
|
|
15
|
+
export type GorpStreamOperation = AssistantTransportStateOperation;
|
|
16
|
+
|
|
17
|
+
export type GorpStreamChunk = {
|
|
16
18
|
readonly snapshot: ReadonlyJSONValue;
|
|
17
|
-
readonly operations: readonly
|
|
19
|
+
readonly operations: readonly GorpStreamOperation[];
|
|
18
20
|
};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createAssistantStream,
|
|
4
|
+
createAssistantStreamResponse,
|
|
5
|
+
} from "./assistant-stream";
|
|
3
6
|
import { AssistantStream } from "../AssistantStream";
|
|
7
|
+
import type { AssistantStreamChunk } from "../AssistantStreamChunk";
|
|
4
8
|
import { DataStreamDecoder } from "../serialization/data-stream/DataStream";
|
|
5
9
|
import { AssistantMessageAccumulator } from "../accumulators/assistant-message-accumulator";
|
|
6
10
|
import type { AssistantMessage } from "../utils/types";
|
|
@@ -21,6 +25,154 @@ const accumulate = async (response: Response): Promise<AssistantMessage> => {
|
|
|
21
25
|
return last!;
|
|
22
26
|
};
|
|
23
27
|
|
|
28
|
+
const collectChunks = async (
|
|
29
|
+
stream: AssistantStream,
|
|
30
|
+
): Promise<AssistantStreamChunk[]> => {
|
|
31
|
+
const chunks: AssistantStreamChunk[] = [];
|
|
32
|
+
await stream.pipeTo(
|
|
33
|
+
new WritableStream({
|
|
34
|
+
write(chunk) {
|
|
35
|
+
chunks.push(chunk);
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
return chunks;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const captureUnhandledRejections = async (
|
|
43
|
+
callback: () => Promise<void>,
|
|
44
|
+
): Promise<unknown[]> => {
|
|
45
|
+
const reasons: unknown[] = [];
|
|
46
|
+
const listener = (reason: unknown) => reasons.push(reason);
|
|
47
|
+
process.on("unhandledRejection", listener);
|
|
48
|
+
try {
|
|
49
|
+
await callback();
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
51
|
+
return reasons;
|
|
52
|
+
} finally {
|
|
53
|
+
process.off("unhandledRejection", listener);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe("createAssistantStream task settlement", () => {
|
|
58
|
+
it("emits callback failures without leaking an unhandled rejection", async () => {
|
|
59
|
+
let chunks: AssistantStreamChunk[] = [];
|
|
60
|
+
const unhandledRejections = await captureUnhandledRejections(async () => {
|
|
61
|
+
chunks = await collectChunks(
|
|
62
|
+
createAssistantStream(async () => {
|
|
63
|
+
throw new Error("provider failed");
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(chunks).toEqual([
|
|
69
|
+
{
|
|
70
|
+
type: "error",
|
|
71
|
+
path: [],
|
|
72
|
+
error: "Error: provider failed",
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
expect(unhandledRejections).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("does not settle the stream again after cancellation", async () => {
|
|
79
|
+
let finishCallback!: () => void;
|
|
80
|
+
const callbackPending = new Promise<void>((resolve) => {
|
|
81
|
+
finishCallback = resolve;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const unhandledRejections = await captureUnhandledRejections(async () => {
|
|
85
|
+
const reader = createAssistantStream(() => callbackPending).getReader();
|
|
86
|
+
await reader.cancel("consumer stopped");
|
|
87
|
+
finishCallback();
|
|
88
|
+
await callbackPending;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(unhandledRejections).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("reports callback failures after the controller is explicitly closed", async () => {
|
|
95
|
+
const error = new Error("cleanup failed");
|
|
96
|
+
const consoleError = vi
|
|
97
|
+
.spyOn(console, "error")
|
|
98
|
+
.mockImplementation(() => {});
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const chunks = await collectChunks(
|
|
102
|
+
createAssistantStream((controller) => {
|
|
103
|
+
controller.close();
|
|
104
|
+
throw error;
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(chunks).toEqual([]);
|
|
109
|
+
expect(consoleError).toHaveBeenCalledOnce();
|
|
110
|
+
expect(consoleError).toHaveBeenCalledWith(error);
|
|
111
|
+
} finally {
|
|
112
|
+
consoleError.mockRestore();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("does not report callback failures caused after cancellation", async () => {
|
|
117
|
+
let failCallback!: (error: Error) => void;
|
|
118
|
+
const callbackPending = new Promise<void>((_, reject) => {
|
|
119
|
+
failCallback = reject;
|
|
120
|
+
});
|
|
121
|
+
const consoleError = vi
|
|
122
|
+
.spyOn(console, "error")
|
|
123
|
+
.mockImplementation(() => {});
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const unhandledRejections = await captureUnhandledRejections(async () => {
|
|
127
|
+
const reader = createAssistantStream(() => callbackPending).getReader();
|
|
128
|
+
await reader.cancel("consumer stopped");
|
|
129
|
+
failCallback(new Error("provider stopped"));
|
|
130
|
+
await callbackPending.catch(() => undefined);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(unhandledRejections).toEqual([]);
|
|
134
|
+
expect(consoleError).not.toHaveBeenCalled();
|
|
135
|
+
} finally {
|
|
136
|
+
consoleError.mockRestore();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("does not settle the outer stream again after a merged stream errors", async () => {
|
|
141
|
+
let finishCallback!: () => void;
|
|
142
|
+
const callbackPending = new Promise<void>((resolve) => {
|
|
143
|
+
finishCallback = resolve;
|
|
144
|
+
});
|
|
145
|
+
const streamError = new Error("merged stream failed");
|
|
146
|
+
const consoleError = vi
|
|
147
|
+
.spyOn(console, "error")
|
|
148
|
+
.mockImplementation(() => {});
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const unhandledRejections = await captureUnhandledRejections(async () => {
|
|
152
|
+
const stream = createAssistantStream(async (controller) => {
|
|
153
|
+
controller.merge(
|
|
154
|
+
new ReadableStream({
|
|
155
|
+
start(streamController) {
|
|
156
|
+
streamController.error(streamError);
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
await callbackPending;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await expect(collectChunks(stream)).rejects.toBe(streamError);
|
|
164
|
+
finishCallback();
|
|
165
|
+
await callbackPending;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(unhandledRejections).toEqual([]);
|
|
169
|
+
expect(consoleError).toHaveBeenCalledWith(streamError);
|
|
170
|
+
} finally {
|
|
171
|
+
consoleError.mockRestore();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
24
176
|
describe("AssistantStreamController withParentId", () => {
|
|
25
177
|
it("attaches parentId to text parts across a data-stream round trip", async () => {
|
|
26
178
|
const response = createAssistantStreamResponse((controller) => {
|
|
@@ -115,7 +115,15 @@ class AssistantStreamControllerImpl implements AssistantStreamController {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
get __internal_isClosed() {
|
|
118
|
-
return
|
|
118
|
+
return (
|
|
119
|
+
this._state.merger.isSealed() ||
|
|
120
|
+
this._state.merger.isCancelled() ||
|
|
121
|
+
this._state.merger.isErrored()
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
get __internal_isCancelled() {
|
|
126
|
+
return this._state.merger.isCancelled();
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
__internal_getReadable() {
|
|
@@ -284,10 +292,11 @@ class AssistantStreamControllerImpl implements AssistantStreamController {
|
|
|
284
292
|
* Creates an {@link AssistantStream} and writes to it with an
|
|
285
293
|
* {@link AssistantStreamController}.
|
|
286
294
|
*
|
|
287
|
-
* The callback may write synchronously or asynchronously. If it throws
|
|
288
|
-
* `error` chunk is emitted
|
|
289
|
-
*
|
|
290
|
-
*
|
|
295
|
+
* The callback may write synchronously or asynchronously. If it throws while
|
|
296
|
+
* the stream is open, an `error` chunk is emitted. Failures after an explicit
|
|
297
|
+
* close are logged, while failures after consumer cancellation are discarded.
|
|
298
|
+
* When the callback settles, the stream is closed automatically unless the
|
|
299
|
+
* controller was already closed.
|
|
291
300
|
*/
|
|
292
301
|
export function createAssistantStream(
|
|
293
302
|
callback: (controller: AssistantStreamController) => PromiseLike<void> | void,
|
|
@@ -304,8 +313,9 @@ export function createAssistantStream(
|
|
|
304
313
|
path: [],
|
|
305
314
|
error: String(e),
|
|
306
315
|
});
|
|
316
|
+
} else if (!controller.__internal_isCancelled) {
|
|
317
|
+
console.error(e);
|
|
307
318
|
}
|
|
308
|
-
throw e;
|
|
309
319
|
} finally {
|
|
310
320
|
if (!controller.__internal_isClosed) {
|
|
311
321
|
controller.close();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createAssistantStreamController } from "./assistant-stream";
|
|
3
|
+
import { ToolResponse } from "../tool/ToolResponse";
|
|
4
|
+
import { toolResultStream } from "../tool/toolResultStream";
|
|
5
|
+
import type { ToolCallReader } from "../tool/tool-types";
|
|
6
|
+
import type { AssistantStreamChunk } from "../AssistantStreamChunk";
|
|
7
|
+
|
|
8
|
+
type Reader = ToolCallReader<Record<string, unknown>, unknown>;
|
|
9
|
+
|
|
10
|
+
describe("ToolCallStreamController", () => {
|
|
11
|
+
it("delivers a backend response before an args parse failure", async () => {
|
|
12
|
+
const [stream, controller] = createAssistantStreamController();
|
|
13
|
+
let resolveToolReader!: (reader: Reader) => void;
|
|
14
|
+
const toolReaderPromise = new Promise<Reader>((resolve) => {
|
|
15
|
+
resolveToolReader = resolve;
|
|
16
|
+
});
|
|
17
|
+
const streamCall = vi.fn((reader: Reader) => {
|
|
18
|
+
resolveToolReader(reader);
|
|
19
|
+
});
|
|
20
|
+
const execute = vi.fn();
|
|
21
|
+
const output = stream.pipeThrough(
|
|
22
|
+
toolResultStream(
|
|
23
|
+
{
|
|
24
|
+
weatherSearch: {
|
|
25
|
+
parameters: { type: "object", properties: {} },
|
|
26
|
+
execute,
|
|
27
|
+
streamCall,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
new AbortController().signal,
|
|
31
|
+
async () => undefined,
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
const chunks: AssistantStreamChunk[] = [];
|
|
35
|
+
const drain = output.pipeTo(
|
|
36
|
+
new WritableStream({
|
|
37
|
+
write(chunk) {
|
|
38
|
+
chunks.push(chunk);
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const toolCall = controller.addToolCallPart({
|
|
44
|
+
toolCallId: "tool-1",
|
|
45
|
+
toolName: "weatherSearch",
|
|
46
|
+
});
|
|
47
|
+
toolCall.argsText.append('{"query":"London","longitude":0');
|
|
48
|
+
const reader = await toolReaderPromise;
|
|
49
|
+
expect(await reader.args.get("query")).toBe("London");
|
|
50
|
+
|
|
51
|
+
toolCall.setResponse(new ToolResponse({ result: { source: "backend" } }));
|
|
52
|
+
toolCall.close();
|
|
53
|
+
controller.close();
|
|
54
|
+
|
|
55
|
+
const response = await reader.response.get();
|
|
56
|
+
expect(response.result).toEqual({ source: "backend" });
|
|
57
|
+
expect(response.isError).toBe(false);
|
|
58
|
+
expect(execute).not.toHaveBeenCalled();
|
|
59
|
+
await drain;
|
|
60
|
+
const results = chunks.filter((chunk) => chunk.type === "result");
|
|
61
|
+
expect(results).toHaveLength(1);
|
|
62
|
+
expect(results[0]).toMatchObject({
|
|
63
|
+
result: { source: "backend" },
|
|
64
|
+
isError: false,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -65,9 +65,6 @@ class ToolCallStreamControllerImpl implements ToolCallStreamController {
|
|
|
65
65
|
private _argsTextController!: TextStreamController;
|
|
66
66
|
|
|
67
67
|
async setResponse(response: ToolResponseLike<ReadonlyJSONValue>) {
|
|
68
|
-
this._argsTextController.close();
|
|
69
|
-
await Promise.resolve(); // flush microtask queue
|
|
70
|
-
// TODO switch argsTextController to be something that doesn'#t require this
|
|
71
68
|
this._controller.enqueue({
|
|
72
69
|
type: "result",
|
|
73
70
|
path: [],
|
|
@@ -83,6 +80,9 @@ class ToolCallStreamControllerImpl implements ToolCallStreamController {
|
|
|
83
80
|
? { messages: response.messages }
|
|
84
81
|
: {}),
|
|
85
82
|
});
|
|
83
|
+
this._argsTextController.close();
|
|
84
|
+
await Promise.resolve(); // flush microtask queue
|
|
85
|
+
// TODO switch argsTextController to be something that doesn'#t require this
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
async close() {
|