@dxos/assistant 0.7.4-df37807 → 0.7.5-feature-compute.4d9d99a

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.
Files changed (96) hide show
  1. package/dist/lib/browser/{chunk-AKP45LQH.mjs → chunk-LVLSJQB3.mjs} +506 -357
  2. package/dist/lib/browser/chunk-LVLSJQB3.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +17 -17
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/testing/index.mjs +10 -3
  6. package/dist/lib/browser/testing/index.mjs.map +4 -4
  7. package/dist/lib/node/{chunk-G6CGWEMO.cjs → chunk-SHOSAIOD.cjs} +507 -362
  8. package/dist/lib/node/chunk-SHOSAIOD.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +42 -42
  10. package/dist/lib/node/index.cjs.map +1 -1
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/testing/index.cjs +18 -12
  13. package/dist/lib/node/testing/index.cjs.map +4 -4
  14. package/dist/lib/node-esm/{chunk-WUPUPEFB.mjs → chunk-ZLBTRSUY.mjs} +506 -357
  15. package/dist/lib/node-esm/chunk-ZLBTRSUY.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +17 -17
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +10 -3
  19. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  20. package/dist/types/src/ai-service/client.d.ts +15 -32
  21. package/dist/types/src/ai-service/client.d.ts.map +1 -1
  22. package/dist/types/src/ai-service/index.d.ts +2 -1
  23. package/dist/types/src/ai-service/index.d.ts.map +1 -1
  24. package/dist/types/src/ai-service/stream.d.ts +34 -0
  25. package/dist/types/src/ai-service/stream.d.ts.map +1 -0
  26. package/dist/types/src/ai-service/stream.test.d.ts +2 -0
  27. package/dist/types/src/ai-service/stream.test.d.ts.map +1 -0
  28. package/dist/types/src/ai-service/types/index.d.ts +3 -0
  29. package/dist/types/src/ai-service/types/index.d.ts.map +1 -0
  30. package/dist/types/src/ai-service/types/message.d.ts +191 -0
  31. package/dist/types/src/ai-service/types/message.d.ts.map +1 -0
  32. package/dist/types/src/ai-service/types/schema.d.ts +175 -0
  33. package/dist/types/src/ai-service/types/schema.d.ts.map +1 -0
  34. package/dist/types/src/ai-service/util/index.d.ts +2 -0
  35. package/dist/types/src/ai-service/util/index.d.ts.map +1 -0
  36. package/dist/types/src/ai-service/{sse.d.ts → util/sse.d.ts} +5 -0
  37. package/dist/types/src/ai-service/util/sse.d.ts.map +1 -0
  38. package/dist/types/src/conversation/conversation.d.ts +54 -4
  39. package/dist/types/src/conversation/conversation.d.ts.map +1 -1
  40. package/dist/types/src/conversation/types.d.ts +35 -4
  41. package/dist/types/src/conversation/types.d.ts.map +1 -1
  42. package/dist/types/src/cypher/combinators.d.ts.map +1 -1
  43. package/dist/types/src/cypher/parjs.d.ts +1 -1
  44. package/dist/types/src/cypher/parjs.d.ts.map +1 -1
  45. package/dist/types/src/cypher/schema.d.ts.map +1 -1
  46. package/dist/types/src/testing/artifacts.test.d.ts +1 -0
  47. package/dist/types/src/testing/artifacts.test.d.ts.map +1 -0
  48. package/dist/types/src/testing/data.d.ts +2 -0
  49. package/dist/types/src/testing/data.d.ts.map +1 -0
  50. package/dist/types/src/testing/defs.d.ts +5 -0
  51. package/dist/types/src/testing/defs.d.ts.map +1 -0
  52. package/dist/types/src/testing/index.d.ts +1 -0
  53. package/dist/types/src/testing/index.d.ts.map +1 -1
  54. package/dist/types/src/testing/test-data.d.ts +1 -1
  55. package/dist/types/src/testing/test-data.d.ts.map +1 -1
  56. package/package.json +20 -18
  57. package/src/ai-service/client.test.ts +25 -26
  58. package/src/ai-service/client.ts +29 -177
  59. package/src/ai-service/index.ts +2 -1
  60. package/src/ai-service/stream.test.ts +186 -0
  61. package/src/ai-service/stream.ts +181 -0
  62. package/src/ai-service/types/index.ts +6 -0
  63. package/src/ai-service/types/message.ts +147 -0
  64. package/src/ai-service/types/schema.ts +128 -0
  65. package/src/ai-service/util/index.ts +5 -0
  66. package/src/ai-service/{sse.ts → util/sse.ts} +5 -0
  67. package/src/bin/chat.ts +6 -9
  68. package/src/conversation/conversation.test.ts +10 -14
  69. package/src/conversation/conversation.ts +109 -14
  70. package/src/conversation/types.ts +47 -6
  71. package/src/cypher/combinators.test.ts +3 -3
  72. package/src/cypher/combinators.ts +0 -1
  73. package/src/cypher/execution-plan.test.ts +5 -3
  74. package/src/cypher/parjs-shims.d.ts +4 -0
  75. package/src/cypher/parjs.ts +2 -2
  76. package/src/cypher/query-executor.test.ts +1 -1
  77. package/src/cypher/schema.ts +0 -1
  78. package/src/index.ts +1 -1
  79. package/src/testing/artifacts.test.ts +182 -0
  80. package/src/testing/data.ts +13 -0
  81. package/src/testing/defs.ts +8 -0
  82. package/src/testing/index.ts +1 -0
  83. package/src/testing/query.test.ts +9 -9
  84. package/src/testing/test-data.ts +1 -1
  85. package/vendor/parjs-browser.mjs +4 -0
  86. package/vendor/parjs-default.mjs +8 -0
  87. package/dist/lib/browser/chunk-AKP45LQH.mjs.map +0 -7
  88. package/dist/lib/node/chunk-G6CGWEMO.cjs.map +0 -7
  89. package/dist/lib/node-esm/chunk-WUPUPEFB.mjs.map +0 -7
  90. package/dist/types/src/ai-service/schema.d.ts +0 -296
  91. package/dist/types/src/ai-service/schema.d.ts.map +0 -1
  92. package/dist/types/src/ai-service/sse.d.ts.map +0 -1
  93. package/dist/types/src/sanity.test.d.ts +0 -2
  94. package/dist/types/src/sanity.test.d.ts.map +0 -1
  95. package/src/ai-service/schema.ts +0 -260
  96. package/src/sanity.test.ts +0 -9
@@ -2,40 +2,35 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- /* eslint-disable no-console */
6
-
7
5
  import { Schema as S } from '@effect/schema';
8
6
 
9
- import { Trigger } from '@dxos/async';
10
7
  import { invariant } from '@dxos/invariant';
11
- import type { SpaceId } from '@dxos/keys';
12
-
13
- import {
14
- Message,
15
- type GenerateRequest,
16
- type ObjectId,
17
- type ResultStreamEvent,
18
- type Space,
19
- type Thread,
20
- } from './schema';
21
- import { iterSSEMessages } from './sse';
22
- import { log } from '@dxos/log';
8
+ import { type SpaceId } from '@dxos/keys';
23
9
 
24
- export type AIServiceClientParams = {
25
- endpoint: string;
26
- };
10
+ import { GenerationStream } from './stream';
11
+ import { Message, type GenerateRequest, type Space, type Thread } from './types';
27
12
 
28
13
  export interface AIServiceClient {
29
14
  getSpace(spaceId: SpaceId): Promise<Space>;
30
15
  getThread(spaceId: SpaceId, threadId: string): Promise<Thread>;
31
- getMessagesInThread(spaceId: SpaceId, threadId: string): Promise<Message[]>;
32
- insertMessages(messages: Message[]): Promise<void>;
16
+ getMessages(spaceId: SpaceId, threadId: string): Promise<Message[]>;
17
+ appendMessages(messages: Message[]): Promise<void>;
33
18
  updateMessage(spaceId: SpaceId, threadId: string, messageId: string, message: Message): Promise<void>;
34
19
  generate(request: GenerateRequest): Promise<GenerationStream>;
35
20
  }
36
21
 
22
+ // TODO(burdon): Create mock.
23
+
24
+ export type AIServiceClientParams = {
25
+ endpoint: string;
26
+ };
27
+
28
+ /**
29
+ * Edge GPT client.
30
+ */
37
31
  export class AIServiceClientImpl implements AIServiceClient {
38
32
  private readonly _endpoint: string;
33
+
39
34
  constructor({ endpoint }: AIServiceClientParams) {
40
35
  this._endpoint = endpoint;
41
36
  }
@@ -48,13 +43,14 @@ export class AIServiceClientImpl implements AIServiceClient {
48
43
  throw new Error('Not implemented');
49
44
  }
50
45
 
51
- async getMessagesInThread(spaceId: SpaceId, threadId: string): Promise<Message[]> {
46
+ async getMessages(spaceId: SpaceId, threadId: string): Promise<Message[]> {
52
47
  const res = await fetch(`${this._endpoint}/space/${spaceId}/thread/${threadId}/message`);
53
48
  return S.decodePromise(S.Array(Message).pipe(S.mutable))((await res.json()).data.results);
54
49
  }
55
50
 
56
- async insertMessages(messages: Message[]): Promise<void> {
57
- const res = await fetch(`${this._endpoint}/message`, {
51
+ async appendMessages(messages: Message[]): Promise<void> {
52
+ const url = `${this._endpoint}/message`;
53
+ const res = await fetch(url, {
58
54
  method: 'POST',
59
55
  headers: { 'Content-Type': 'application/json' },
60
56
  body: JSON.stringify({
@@ -63,7 +59,7 @@ export class AIServiceClientImpl implements AIServiceClient {
63
59
  });
64
60
 
65
61
  if (!res.ok) {
66
- throw new Error(await res.text());
62
+ throw new Error(`${await res.text()} [${url}]`);
67
63
  }
68
64
  }
69
65
 
@@ -72,172 +68,28 @@ export class AIServiceClientImpl implements AIServiceClient {
72
68
  throw new Error('Not implemented');
73
69
  }
74
70
 
71
+ /**
72
+ * Open message stream.
73
+ */
75
74
  async generate(request: GenerateRequest): Promise<GenerationStream> {
75
+ const controller = new AbortController();
76
+ const signal = controller.signal;
77
+
76
78
  const response = await fetch(`${this._endpoint}/generate`, {
77
79
  method: 'POST',
78
80
  headers: { 'Content-Type': 'application/json' },
79
81
  body: JSON.stringify(request),
82
+ signal,
80
83
  });
81
- invariant(response.body instanceof ReadableStream);
82
84
 
85
+ invariant(response.body instanceof ReadableStream);
83
86
  return GenerationStream.fromSSEResponse(
84
87
  {
85
88
  spaceId: request.spaceId,
86
89
  threadId: request.threadId,
87
90
  },
88
91
  response,
92
+ controller,
89
93
  );
90
94
  }
91
95
  }
92
-
93
- type GenerationParams = {
94
- spaceId: SpaceId;
95
- threadId: ObjectId;
96
- };
97
-
98
- export class GenerationStream implements AsyncIterable<ResultStreamEvent> {
99
- static fromSSEResponse(params: GenerationParams, response: Response) {
100
- const controller = new AbortController();
101
- const iterator = async function* () {
102
- for await (const sse of iterSSEMessages(response, controller)) {
103
- if (sse.event === 'completion') {
104
- try {
105
- yield JSON.parse(sse.data);
106
- } catch (e) {
107
- console.error('Could not parse message into JSON:', sse.data);
108
- console.error('From chunk:', sse.raw);
109
- throw e;
110
- }
111
- }
112
-
113
- if (
114
- sse.event === 'message_start' ||
115
- sse.event === 'message_delta' ||
116
- sse.event === 'message_stop' ||
117
- sse.event === 'content_block_start' ||
118
- sse.event === 'content_block_delta' ||
119
- sse.event === 'content_block_stop'
120
- ) {
121
- try {
122
- yield JSON.parse(sse.data);
123
- } catch (e) {
124
- console.error('Could not parse message into JSON:', sse.data);
125
- console.error('From chunk:', sse.raw);
126
- throw e;
127
- }
128
- }
129
-
130
- if (sse.event === 'ping') {
131
- continue;
132
- }
133
-
134
- if (sse.event === 'error') {
135
- throw new Error(`Message generation error: ${sse.data}`);
136
- }
137
- }
138
- };
139
- return new GenerationStream(params, iterator);
140
- }
141
-
142
- private _previousMessages: Message[] = [];
143
- private _accumulatedMessage?: Message = undefined;
144
- private _done = new Trigger();
145
- private _iterator?: AsyncIterator<ResultStreamEvent> = undefined;
146
-
147
- constructor(
148
- private readonly _params: GenerationParams,
149
- private readonly _getIterator: () => AsyncIterableIterator<ResultStreamEvent>,
150
- ) {}
151
-
152
- [Symbol.asyncIterator](): AsyncIterator<ResultStreamEvent> {
153
- return this._createIterator();
154
- }
155
-
156
- cancel() {
157
- throw new Error('Not implemented');
158
- }
159
-
160
- get accumulatedMessages(): Message[] {
161
- // TODO(dmaretskyi): Support multiple accumulated messages.
162
- return [...this._previousMessages, ...(this._accumulatedMessage == null ? [] : [this._accumulatedMessage])];
163
- }
164
-
165
- /**
166
- * Returns the complete message list generated by the AI service.
167
- */
168
- async complete(): Promise<Message[]> {
169
- await this._done.wait();
170
- return this.accumulatedMessages;
171
- }
172
-
173
- private _createIterator(): AsyncIterator<ResultStreamEvent> {
174
- const self = this;
175
- return (this._iterator ??= (async function* () {
176
- for await (const event of self._getIterator()) {
177
- self._processEvent(event);
178
- yield event;
179
- }
180
- })());
181
- }
182
-
183
- private _processEvent(event: ResultStreamEvent) {
184
- // TODO(dmaretskyi): Support multiple message, e.g. for service-defined tools.
185
- switch (event.type) {
186
- case 'message_start': {
187
- if (this._accumulatedMessage) {
188
- this._previousMessages.push(this._accumulatedMessage);
189
- this._accumulatedMessage = undefined;
190
- }
191
- this._accumulatedMessage = {
192
- ...event.message,
193
- threadId: this._params.threadId,
194
- spaceId: this._params.spaceId,
195
- };
196
- break;
197
- }
198
- case 'message_delta': {
199
- if (this._accumulatedMessage === undefined) {
200
- throw new Error('Received message delta without a message start');
201
- }
202
- break;
203
- }
204
- case 'message_stop': {
205
- if (this._accumulatedMessage === undefined) {
206
- throw new Error('Received message stop without a message start');
207
- }
208
-
209
- break;
210
- }
211
- case 'content_block_start': {
212
- invariant(this._accumulatedMessage);
213
- this._accumulatedMessage.content.push(event.content);
214
- break;
215
- }
216
- case 'content_block_delta': {
217
- invariant(this._accumulatedMessage);
218
- const snapshotContent = this._accumulatedMessage.content.at(event.index);
219
- if (snapshotContent?.type === 'text' && event.delta.type === 'text_delta') {
220
- snapshotContent.text += event.delta.text;
221
- } else if (snapshotContent?.type === 'tool_use' && event.delta.type === 'input_json_delta') {
222
- snapshotContent.inputJson ??= '';
223
- snapshotContent.inputJson += event.delta.partial_json;
224
-
225
- // TODO(dmaretskyi): Partial parsing.
226
- // if (jsonBuf) {
227
- // snapshotContent.input = partialParse(jsonBuf);
228
- // }
229
- }
230
- break;
231
- }
232
- case 'content_block_stop': {
233
- invariant(this._accumulatedMessage);
234
- const lastBlock = this._accumulatedMessage.content.at(-1);
235
- if (lastBlock?.type === 'tool_use') {
236
- lastBlock.input = JSON.parse(lastBlock.inputJson ?? '');
237
- }
238
- break;
239
- }
240
- }
241
- this._done.wake();
242
- }
243
- }
@@ -3,4 +3,5 @@
3
3
  //
4
4
 
5
5
  export * from './client';
6
- export * from './schema';
6
+ export * from './stream';
7
+ export * from './types';
@@ -0,0 +1,186 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { SaxesParser } from 'saxes';
6
+ import { describe, it } from 'vitest';
7
+
8
+ import { ObjectId } from '@dxos/echo-schema';
9
+ import { log } from '@dxos/log';
10
+
11
+ import { GenerationStream } from './stream';
12
+ import { type Message } from './types';
13
+
14
+ type Part = { event: string; data: any };
15
+
16
+ export const createTestStream = (blocks: string[]): ReadableStream => {
17
+ const encoder = new TextEncoder();
18
+ return new ReadableStream({
19
+ start: (controller) => {
20
+ const push = (message: Part) => {
21
+ controller.enqueue(encoder.encode(`event: ${message.event}\ndata: ${JSON.stringify(message.data)}\n\n`));
22
+ };
23
+
24
+ push({
25
+ event: 'message_start',
26
+ data: { type: 'message_start', message: { id: '123', role: 'assistant', content: [] } },
27
+ });
28
+
29
+ for (const block of blocks) {
30
+ push({
31
+ event: 'content_block_start',
32
+ data: { type: 'content_block_start', content: { type: 'text', text: '' } },
33
+ });
34
+
35
+ let index = 0;
36
+ for (const word of block.split(' ')) {
37
+ push({
38
+ event: 'content_block_delta',
39
+ data: { type: 'content_block_delta', index, delta: { type: 'text_delta', text: word + ' ' } },
40
+ });
41
+
42
+ index += word.length + 1;
43
+ }
44
+
45
+ push({
46
+ event: 'content_block_stop',
47
+ data: { type: 'content_block_stop' },
48
+ });
49
+ }
50
+
51
+ push({
52
+ event: 'message_stop',
53
+ data: { type: 'message_stop' },
54
+ });
55
+
56
+ controller.close();
57
+ },
58
+ });
59
+ };
60
+
61
+ describe('GenerationStream', () => {
62
+ it('should stream messages', async ({ expect }) => {
63
+ const input = [['<cot>', '1. Ready.', '2. Aim.', '3. Fire.', '</cot>'].join('\n'), 'Hello world!'];
64
+ const output = [];
65
+
66
+ let block: string[] = [];
67
+ const stream = GenerationStream.fromSSEResponse({}, new Response(createTestStream(input)));
68
+ for await (const event of stream) {
69
+ switch (event.type) {
70
+ case 'content_block_start': {
71
+ block = [(event.content as any).text];
72
+ break;
73
+ }
74
+
75
+ case 'content_block_delta': {
76
+ block.push((event.delta as any).text);
77
+ break;
78
+ }
79
+
80
+ case 'content_block_stop': {
81
+ output.push(block.join('').trim());
82
+ break;
83
+ }
84
+ }
85
+ }
86
+
87
+ expect(output.length).to.equal(input.length);
88
+ expect(output).to.deep.equal(input);
89
+ await stream.complete();
90
+ });
91
+
92
+ /**
93
+ * https://astexplorer.net
94
+ */
95
+ it('should parse the stream', async ({ expect }) => {
96
+ const input = [
97
+ // Text
98
+ `
99
+ Hello world!
100
+ `,
101
+
102
+ // COT
103
+ `
104
+ <cot>
105
+ 1. Prepare the ship.
106
+ 2. Arm the torpedoes.
107
+ 3. Fire lasers.
108
+ </cot>
109
+ `,
110
+
111
+ // Artifact
112
+ // <?xml version="1.0" encoding="UTF-8"?>
113
+ `
114
+ <artifact id="123" />
115
+ <choice id="123">
116
+ <artifact id="123" />
117
+ </choice>
118
+ `,
119
+
120
+ // JSON
121
+ `
122
+ <json>
123
+ <![CDATA[
124
+ {
125
+ "type": "text",
126
+ "text": "<test>OK</test>"
127
+ }
128
+ ]]>
129
+ </json>
130
+ `,
131
+
132
+ // Code/JSX
133
+ `,
134
+ <code>
135
+ <![CDATA[
136
+ const Test = () => {
137
+ return <div>Hello world!</div>;
138
+ }
139
+ ]]>
140
+ </code>
141
+ `,
142
+ ];
143
+
144
+ // TODO(burdon): Delimiter for JSON blocks.
145
+
146
+ const output: Message = { id: ObjectId.random(), role: 'assistant', content: [] };
147
+
148
+ /**
149
+ * https://www.npmjs.com/package/saxex
150
+ */
151
+ const parser = new SaxesParser({
152
+ fragment: true,
153
+ });
154
+
155
+ parser.on('error', (error) => {
156
+ log.error('error', { error });
157
+ });
158
+ parser.on('opentag', (node) => {
159
+ log.info('opentag', { node });
160
+ });
161
+ parser.on('closetag', (node) => {
162
+ log.info('closetag', { node });
163
+ });
164
+ parser.on('cdata', (text) => {
165
+ log.info('cdata', { text });
166
+ });
167
+ parser.on('text', (text) => {
168
+ log.info('text', { text });
169
+ });
170
+ parser.on('end', () => {
171
+ log.info('end');
172
+ });
173
+
174
+ const stream = GenerationStream.fromSSEResponse({}, new Response(createTestStream(input)));
175
+ for await (const event of stream) {
176
+ if (event.type === 'content_block_delta') {
177
+ // log.info('delta', { delta: (event.delta as any).text });
178
+ parser.write((event.delta as any).text);
179
+ }
180
+ }
181
+
182
+ await stream.complete();
183
+
184
+ expect(output.content).to.deep.equal([]);
185
+ });
186
+ });
@@ -0,0 +1,181 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Trigger } from '@dxos/async';
6
+ import { type ObjectId } from '@dxos/echo-schema';
7
+ import { invariant } from '@dxos/invariant';
8
+ import { type SpaceId } from '@dxos/keys';
9
+ import { log } from '@dxos/log';
10
+
11
+ import { type Message, type ResultStreamEvent } from './types';
12
+ import { iterSSEMessages } from './util';
13
+
14
+ export type GenerationParams = {
15
+ spaceId?: SpaceId;
16
+ threadId?: ObjectId;
17
+ };
18
+
19
+ export class GenerationStream implements AsyncIterable<ResultStreamEvent> {
20
+ /**
21
+ * Creates a stream from an SSE response.
22
+ */
23
+ static fromSSEResponse(params: GenerationParams, response: Response, controller = new AbortController()) {
24
+ const iterator = async function* () {
25
+ for await (const sse of iterSSEMessages(response, controller)) {
26
+ if (sse.event === 'completion') {
27
+ try {
28
+ yield JSON.parse(sse.data);
29
+ } catch (err) {
30
+ log.error('could not parse message into JSON:', { data: sse.data, raw: sse.raw });
31
+ throw err;
32
+ }
33
+ }
34
+
35
+ if (
36
+ sse.event === 'message_start' ||
37
+ sse.event === 'message_delta' ||
38
+ sse.event === 'message_stop' ||
39
+ sse.event === 'content_block_start' ||
40
+ sse.event === 'content_block_delta' ||
41
+ sse.event === 'content_block_stop'
42
+ ) {
43
+ try {
44
+ yield JSON.parse(sse.data);
45
+ } catch (err) {
46
+ log.error('could not parse message into JSON:', { data: sse.data, raw: sse.raw });
47
+ throw err;
48
+ }
49
+ }
50
+
51
+ if (sse.event === 'ping') {
52
+ continue;
53
+ }
54
+
55
+ if (sse.event === 'error') {
56
+ throw new Error(`Message generation error: ${sse.data}`);
57
+ }
58
+ }
59
+ };
60
+
61
+ return new GenerationStream(controller, iterator, params);
62
+ }
63
+
64
+ private readonly _done = new Trigger();
65
+ private _previousMessages: Message[] = [];
66
+ private _accumulatedMessage?: Message = undefined;
67
+ private _iterator?: AsyncIterator<ResultStreamEvent> = undefined;
68
+
69
+ constructor(
70
+ private readonly _controller: AbortController,
71
+ private readonly _getIterator: () => AsyncIterableIterator<ResultStreamEvent>,
72
+ private readonly _params: GenerationParams,
73
+ ) {}
74
+
75
+ [Symbol.asyncIterator](): AsyncIterator<ResultStreamEvent> {
76
+ return this._createIterator();
77
+ }
78
+
79
+ get accumulatedMessages(): Message[] {
80
+ // TODO(dmaretskyi): Support multiple accumulated messages.
81
+ return [...this._previousMessages, ...(this._accumulatedMessage == null ? [] : [this._accumulatedMessage])];
82
+ }
83
+
84
+ /**
85
+ * Aborts the stream.
86
+ */
87
+ abort() {
88
+ this._controller.abort();
89
+ }
90
+
91
+ /**
92
+ * Returns the complete message list generated by the AI service.
93
+ */
94
+ async complete(): Promise<Message[]> {
95
+ await this._done.wait();
96
+ return this.accumulatedMessages;
97
+ }
98
+
99
+ private _createIterator(): AsyncIterator<ResultStreamEvent> {
100
+ const self = this;
101
+ return (this._iterator ??= (() => {
102
+ const generator = async function* (this: GenerationStream) {
103
+ try {
104
+ for await (const event of self._getIterator()) {
105
+ self._processEvent(event);
106
+ yield event;
107
+ }
108
+ this._done.wake();
109
+ } catch (err: any) {
110
+ this._done.throw(err);
111
+ }
112
+ };
113
+ return generator.call(this);
114
+ })());
115
+ }
116
+
117
+ private _processEvent(event: ResultStreamEvent) {
118
+ // TODO(dmaretskyi): Support multiple message, e.g., for service-defined tools.
119
+ switch (event.type) {
120
+ case 'message_start': {
121
+ if (this._accumulatedMessage) {
122
+ this._previousMessages.push(this._accumulatedMessage);
123
+ this._accumulatedMessage = undefined;
124
+ }
125
+ this._accumulatedMessage = {
126
+ ...event.message,
127
+ threadId: this._params.threadId,
128
+ spaceId: this._params.spaceId,
129
+ };
130
+ break;
131
+ }
132
+
133
+ case 'message_delta': {
134
+ if (this._accumulatedMessage === undefined) {
135
+ throw new Error('Received message delta without a message start');
136
+ }
137
+ break;
138
+ }
139
+
140
+ case 'message_stop': {
141
+ if (this._accumulatedMessage === undefined) {
142
+ throw new Error('Received message stop without a message start');
143
+ }
144
+ break;
145
+ }
146
+
147
+ case 'content_block_start': {
148
+ invariant(this._accumulatedMessage);
149
+ this._accumulatedMessage.content.push(event.content);
150
+ break;
151
+ }
152
+
153
+ case 'content_block_delta': {
154
+ invariant(this._accumulatedMessage);
155
+ const snapshotContent = this._accumulatedMessage.content.at(event.index);
156
+ if (snapshotContent?.type === 'text' && event.delta.type === 'text_delta') {
157
+ snapshotContent.text += event.delta.text;
158
+ } else if (snapshotContent?.type === 'tool_use' && event.delta.type === 'input_json_delta') {
159
+ snapshotContent.inputJson ??= '';
160
+ snapshotContent.inputJson += event.delta.partial_json;
161
+
162
+ // TODO(dmaretskyi): Partial parsing.
163
+ // if (jsonBuf) {
164
+ // snapshotContent.input = partialParse(jsonBuf);
165
+ // }
166
+ }
167
+ break;
168
+ }
169
+
170
+ case 'content_block_stop': {
171
+ invariant(this._accumulatedMessage);
172
+ const lastBlock = this._accumulatedMessage.content.at(-1);
173
+ if (lastBlock?.type === 'tool_use') {
174
+ lastBlock.input =
175
+ lastBlock.inputJson == null || lastBlock.inputJson === '' ? {} : JSON.parse(lastBlock.inputJson ?? '');
176
+ }
177
+ break;
178
+ }
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './message';
6
+ export * from './schema';