@sqlrooms/ai-core 0.26.0-rc.2 → 0.26.0-rc.4

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 (48) hide show
  1. package/README.md +238 -348
  2. package/dist/AiSlice.d.ts +37 -7
  3. package/dist/AiSlice.d.ts.map +1 -1
  4. package/dist/AiSlice.js +203 -147
  5. package/dist/AiSlice.js.map +1 -1
  6. package/dist/chatTransport.d.ts +48 -0
  7. package/dist/chatTransport.d.ts.map +1 -0
  8. package/dist/chatTransport.js +270 -0
  9. package/dist/chatTransport.js.map +1 -0
  10. package/dist/components/AiThinkingDots.d.ts +10 -0
  11. package/dist/components/AiThinkingDots.d.ts.map +1 -0
  12. package/dist/components/AiThinkingDots.js +18 -0
  13. package/dist/components/AiThinkingDots.js.map +1 -0
  14. package/dist/components/AnalysisResult.d.ts +1 -2
  15. package/dist/components/AnalysisResult.d.ts.map +1 -1
  16. package/dist/components/AnalysisResult.js +48 -18
  17. package/dist/components/AnalysisResult.js.map +1 -1
  18. package/dist/components/AnalysisResultsContainer.d.ts.map +1 -1
  19. package/dist/components/AnalysisResultsContainer.js +5 -10
  20. package/dist/components/AnalysisResultsContainer.js.map +1 -1
  21. package/dist/components/QueryControls.d.ts.map +1 -1
  22. package/dist/components/QueryControls.js +6 -3
  23. package/dist/components/QueryControls.js.map +1 -1
  24. package/dist/components/ToolCallInfo.d.ts +25 -0
  25. package/dist/components/ToolCallInfo.d.ts.map +1 -0
  26. package/dist/components/ToolCallInfo.js +32 -0
  27. package/dist/components/ToolCallInfo.js.map +1 -0
  28. package/dist/components/tools/ToolResult.d.ts +12 -7
  29. package/dist/components/tools/ToolResult.d.ts.map +1 -1
  30. package/dist/components/tools/ToolResult.js +8 -6
  31. package/dist/components/tools/ToolResult.js.map +1 -1
  32. package/dist/hooks/useAiChat.d.ts +60 -0
  33. package/dist/hooks/useAiChat.d.ts.map +1 -0
  34. package/dist/hooks/useAiChat.js +92 -0
  35. package/dist/hooks/useAiChat.js.map +1 -0
  36. package/dist/index.d.ts +3 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +3 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/utils.d.ts +23 -1
  41. package/dist/utils.d.ts.map +1 -1
  42. package/dist/utils.js +24 -0
  43. package/dist/utils.js.map +1 -1
  44. package/package.json +15 -14
  45. package/dist/analysis.d.ts +0 -51
  46. package/dist/analysis.d.ts.map +0 -1
  47. package/dist/analysis.js +0 -43
  48. package/dist/analysis.js.map +0 -1
package/dist/AiSlice.d.ts CHANGED
@@ -1,17 +1,23 @@
1
- import { ExtendedTool } from '@openassistant/utils';
2
- import { AiSliceConfig, AnalysisSessionSchema } from '@sqlrooms/ai-config';
1
+ import { AiSliceConfig, AnalysisResultSchema, AnalysisSessionSchema } from '@sqlrooms/ai-config';
3
2
  import { BaseRoomConfig, RoomState, type StateCreator } from '@sqlrooms/room-store';
4
- import { z } from 'zod';
5
- export type AiSliceTool = ExtendedTool<z.ZodTypeAny, unknown, unknown, unknown>;
3
+ import { UIMessage, DefaultChatTransport, LanguageModel } from 'ai';
4
+ import { OpenAssistantToolSet } from '@openassistant/utils';
5
+ type ExtendedChatOnToolCallCallback = (args: {
6
+ toolCall: any;
7
+ addToolResult?: any;
8
+ }) => Promise<any> | any;
6
9
  export type AiSliceState = {
7
10
  ai: {
8
11
  config: AiSliceConfig;
9
12
  analysisPrompt: string;
10
13
  isRunningAnalysis: boolean;
11
- tools: Record<string, AiSliceTool>;
14
+ tools: OpenAssistantToolSet;
12
15
  analysisAbortController?: AbortController;
13
16
  setAnalysisPrompt: (prompt: string) => void;
14
- startAnalysis: () => Promise<void>;
17
+ addAnalysisResult: (message: UIMessage) => void;
18
+ startAnalysis: (sendMessage: (message: {
19
+ text: string;
20
+ }) => void) => Promise<void>;
15
21
  cancelAnalysis: () => void;
16
22
  setAiModel: (modelProvider: string, model: string) => void;
17
23
  createSession: (name?: string, modelProvider?: string, model?: string) => void;
@@ -19,12 +25,29 @@ export type AiSliceState = {
19
25
  renameSession: (sessionId: string, name: string) => void;
20
26
  deleteSession: (sessionId: string) => void;
21
27
  getCurrentSession: () => AnalysisSessionSchema | undefined;
28
+ setSessionUiMessages: (sessionId: string, uiMessages: UIMessage[]) => void;
29
+ setSessionToolAdditionalData: (sessionId: string, toolCallId: string, additionalData: unknown) => void;
30
+ getAnalysisResults: () => AnalysisResultSchema[];
22
31
  deleteAnalysisResult: (sessionId: string, resultId: string) => void;
32
+ getAssistantMessageParts: (analysisResultId: string) => UIMessage['parts'];
23
33
  findToolComponent: (toolName: string) => React.ComponentType | undefined;
24
34
  getApiKeyFromSettings: () => string;
25
35
  getBaseUrlFromSettings: () => string | undefined;
26
36
  getMaxStepsFromSettings: () => number;
27
37
  getFullInstructions: () => string;
38
+ getLocalChatTransport: () => DefaultChatTransport<UIMessage>;
39
+ /** Optional remote endpoint to use for chat; if empty, local transport is used */
40
+ chatEndPoint: string;
41
+ chatHeaders: Record<string, string>;
42
+ getRemoteChatTransport: (endpoint: string, headers?: Record<string, string>) => DefaultChatTransport<UIMessage>;
43
+ onChatToolCall: ExtendedChatOnToolCallCallback;
44
+ onChatData: (dataPart: any) => void;
45
+ onChatFinish: (args: {
46
+ message: UIMessage;
47
+ messages: UIMessage[];
48
+ isError?: boolean;
49
+ }) => void;
50
+ onChatError: (error: unknown) => void;
28
51
  };
29
52
  };
30
53
  /**
@@ -35,7 +58,7 @@ export interface AiSliceOptions {
35
58
  /** Initial prompt to display in the analysis input */
36
59
  initialAnalysisPrompt?: string;
37
60
  /** Tools to add to the AI assistant */
38
- tools: Record<string, AiSliceTool>;
61
+ tools: OpenAssistantToolSet;
39
62
  /**
40
63
  * Function to get custom instructions for the AI assistant
41
64
  * @returns The instructions string to use
@@ -43,10 +66,17 @@ export interface AiSliceOptions {
43
66
  getInstructions: () => string;
44
67
  defaultProvider?: string;
45
68
  defaultModel?: string;
69
+ /** Provide a pre-configured model client for a provider (e.g., Azure). */
70
+ getCustomModel?: () => LanguageModel | undefined;
46
71
  maxSteps?: number;
47
72
  getApiKey?: (modelProvider: string) => string;
48
73
  getBaseUrl?: () => string;
74
+ /** Optional remote endpoint to use for chat; if empty, local transport is used */
75
+ chatEndPoint?: string;
76
+ /** Optional headers to send with remote endpoint */
77
+ chatHeaders?: Record<string, string>;
49
78
  }
50
79
  export declare function createAiSlice<PC extends BaseRoomConfig>(params: AiSliceOptions): StateCreator<AiSliceState>;
51
80
  export declare function useStoreWithAi<T, PC extends BaseRoomConfig, S extends RoomState<PC> & AiSliceState>(selector: (state: S) => T): T;
81
+ export {};
52
82
  //# sourceMappingURL=AiSlice.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AiSlice.d.ts","sourceRoot":"","sources":["../src/AiSlice.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AiSlice.d.ts","sourceRoot":"","sources":["../src/AiSlice.ts"],"names":[],"mappings":"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"}
package/dist/AiSlice.js CHANGED
@@ -2,10 +2,10 @@ import { createId } from '@paralleldrive/cuid2';
2
2
  import { createDefaultAiConfig, } from '@sqlrooms/ai-config';
3
3
  import { createBaseSlice, useBaseRoomStore, } from '@sqlrooms/room-store';
4
4
  import { produce } from 'immer';
5
- import { runAnalysis } from './analysis';
5
+ import { createLocalChatTransportFactory, createRemoteChatTransportFactory, createChatHandlers, } from './chatTransport';
6
6
  import { hasAiSettingsConfig } from './hasAiSettingsConfig';
7
7
  export function createAiSlice(params) {
8
- const { defaultProvider = 'openai', defaultModel = 'gpt-4.1', initialAnalysisPrompt = '', tools, getApiKey, getBaseUrl, maxSteps, getInstructions, } = params;
8
+ const { initialAnalysisPrompt = '', tools, getApiKey, getBaseUrl, maxSteps = 50, getInstructions, defaultProvider = 'openai', defaultModel = 'gpt-4.1', getCustomModel, chatEndPoint = '', chatHeaders = {}, } = params;
9
9
  return createBaseSlice((set, get, store) => {
10
10
  return {
11
11
  ai: {
@@ -71,6 +71,9 @@ export function createAiSlice(params) {
71
71
  model: model || currentSession?.model || 'gpt-4.1',
72
72
  analysisResults: [],
73
73
  createdAt: new Date(),
74
+ uiMessages: [],
75
+ toolAdditionalData: {},
76
+ messagesRevision: 0,
74
77
  });
75
78
  draft.ai.config.currentSessionId = newSessionId;
76
79
  }));
@@ -119,93 +122,34 @@ export function createAiSlice(params) {
119
122
  }));
120
123
  },
121
124
  /**
122
- * Start the analysis
123
- * TODO: how to pass the history analysisResults?
125
+ * Save the Ai SDK UI messages for a session
124
126
  */
125
- startAnalysis: async () => {
126
- const resultId = createId();
127
- const abortController = new AbortController();
128
- const currentSession = get().ai.getCurrentSession();
129
- if (!currentSession) {
130
- console.error('No current session found');
131
- return;
132
- }
127
+ setSessionUiMessages: (sessionId, uiMessages) => {
133
128
  set((state) => produce(state, (draft) => {
134
- draft.ai.analysisAbortController = abortController;
135
- draft.ai.isRunningAnalysis = true;
136
- const session = draft.ai.config.sessions.find((s) => s.id === draft.ai.config.currentSessionId);
129
+ const session = draft.ai.config.sessions.find((s) => s.id === sessionId);
137
130
  if (session) {
138
- session.analysisResults.push({
139
- id: resultId,
140
- prompt: get().ai.analysisPrompt,
141
- streamMessage: {
142
- parts: [
143
- {
144
- type: 'text',
145
- text: '',
146
- },
147
- ],
148
- },
149
- isCompleted: false,
150
- });
131
+ // store the latest UI messages from the chat hook
132
+ // Create a deep copy to avoid read-only property issues
133
+ session.uiMessages = JSON.parse(JSON.stringify(uiMessages));
151
134
  }
152
135
  }));
153
- try {
154
- await runAnalysis({
155
- modelProvider: currentSession.modelProvider || defaultProvider,
156
- model: currentSession.model || defaultModel,
157
- apiKey: get().ai.getApiKeyFromSettings(),
158
- baseUrl: get().ai.getBaseUrlFromSettings(),
159
- prompt: get().ai.analysisPrompt,
160
- abortController,
161
- tools: get().ai.tools,
162
- maxSteps: get().ai.getMaxStepsFromSettings(),
163
- getInstructions: get().ai.getFullInstructions,
164
- historyAnalysis: currentSession.analysisResults,
165
- onStreamResult: (isCompleted, streamMessage) => {
166
- set(makeResultsAppender({
167
- resultId,
168
- streamMessage,
169
- isCompleted,
170
- }));
171
- },
172
- });
173
- }
174
- catch (err) {
175
- set(makeResultsAppender({
176
- resultId,
177
- isCompleted: true,
178
- errorMessage: {
179
- error: err instanceof Error ? err.message : String(err),
180
- },
181
- }));
182
- }
183
- finally {
184
- set((state) => produce(state, (draft) => {
185
- draft.ai.isRunningAnalysis = false;
186
- draft.ai.analysisPrompt = '';
187
- }));
188
- }
189
- },
190
- cancelAnalysis: () => {
191
- set((state) => produce(state, (draft) => {
192
- draft.ai.isRunningAnalysis = false;
193
- }));
194
- get().ai.analysisAbortController?.abort('Analysis cancelled');
195
136
  },
196
137
  /**
197
- * Delete an analysis result from a session
138
+ * Save additional data for a session
198
139
  */
199
- deleteAnalysisResult: (sessionId, resultId) => {
140
+ setSessionToolAdditionalData: (sessionId, toolCallId, additionalData) => {
200
141
  set((state) => produce(state, (draft) => {
201
142
  const session = draft.ai.config.sessions.find((s) => s.id === sessionId);
202
143
  if (session) {
203
- session.analysisResults = session.analysisResults.filter((r) => r.id !== resultId);
144
+ if (!session.toolAdditionalData) {
145
+ session.toolAdditionalData = {};
146
+ }
147
+ session.toolAdditionalData[toolCallId] = additionalData;
204
148
  }
205
149
  }));
206
150
  },
207
151
  findToolComponent: (toolName) => {
208
- return Object.entries(get().ai.tools).find(([name]) => name === toolName)?.[1]?.component;
152
+ return get().ai.tools[toolName]?.component;
209
153
  },
210
154
  getBaseUrlFromSettings: () => {
211
155
  // First try the getBaseUrl function if provided
@@ -233,7 +177,7 @@ export function createAiSlice(params) {
233
177
  const currentSession = getCurrentSessionFromState(store);
234
178
  if (currentSession) {
235
179
  // First try the getApiKey function if provided
236
- const apiKeyFromFunction = getApiKey?.(currentSession.modelProvider || defaultProvider);
180
+ const apiKeyFromFunction = getApiKey?.(currentSession.modelProvider || 'openai');
237
181
  if (apiKeyFromFunction) {
238
182
  return apiKeyFromFunction;
239
183
  }
@@ -264,7 +208,7 @@ export function createAiSlice(params) {
264
208
  return settingsMaxSteps;
265
209
  }
266
210
  }
267
- return 5;
211
+ return 50;
268
212
  },
269
213
  getFullInstructions: () => {
270
214
  const store = get();
@@ -279,6 +223,189 @@ export function createAiSlice(params) {
279
223
  }
280
224
  return instructions;
281
225
  },
226
+ /**
227
+ * Start the analysis
228
+ * TODO: how to pass the history analysisResults?
229
+ */
230
+ startAnalysis: async (sendMessage) => {
231
+ const abortController = new AbortController();
232
+ const currentSession = get().ai.getCurrentSession();
233
+ if (!currentSession) {
234
+ console.error('No current session found');
235
+ return;
236
+ }
237
+ const promptText = get().ai.analysisPrompt;
238
+ set((state) => produce(state, (draft) => {
239
+ draft.ai.analysisAbortController = abortController;
240
+ draft.ai.isRunningAnalysis = true;
241
+ // Add incomplete analysis result to session immediately for instant UI rendering
242
+ const session = draft.ai.config.sessions.find((s) => s.id === currentSession.id);
243
+ if (session) {
244
+ // Add incomplete analysis result with a temporary ID
245
+ // This will be updated in onChatFinish with the actual user message ID
246
+ session.analysisResults.push({
247
+ id: '__pending__',
248
+ prompt: promptText,
249
+ isCompleted: false,
250
+ });
251
+ }
252
+ }));
253
+ // The pending analysis result will be updated in onChatFinish with the correct message ID
254
+ sendMessage({ text: promptText });
255
+ },
256
+ cancelAnalysis: () => {
257
+ const currentSession = get().ai.getCurrentSession();
258
+ set((state) => produce(state, (draft) => {
259
+ draft.ai.isRunningAnalysis = false;
260
+ // Remove pending analysis result if it exists
261
+ if (currentSession) {
262
+ const session = draft.ai.config.sessions.find((s) => s.id === currentSession.id);
263
+ if (session) {
264
+ const pendingIndex = session.analysisResults.findIndex((result) => result.id === '__pending__');
265
+ if (pendingIndex !== -1) {
266
+ session.analysisResults.splice(pendingIndex, 1);
267
+ }
268
+ }
269
+ }
270
+ }));
271
+ get().ai.analysisAbortController?.abort('Analysis cancelled');
272
+ },
273
+ /**
274
+ * Get the assistant message parts for a given analysis result ID
275
+ * @param analysisResultId - The ID of the analysis result (user message ID)
276
+ * @returns Array of message parts from the assistant's response
277
+ */
278
+ getAssistantMessageParts: (analysisResultId) => {
279
+ const currentSession = get().ai.getCurrentSession();
280
+ if (!currentSession)
281
+ return [];
282
+ const uiMessages = currentSession.uiMessages;
283
+ // Find the user message with analysisResultId
284
+ const userMessageIndex = uiMessages.findIndex((msg) => msg.id === analysisResultId && msg.role === 'user');
285
+ if (userMessageIndex === -1)
286
+ return [];
287
+ // Find the next assistant message after this user message
288
+ for (let i = userMessageIndex + 1; i < uiMessages.length; i++) {
289
+ const msg = uiMessages[i];
290
+ if (msg?.role === 'assistant') {
291
+ return msg.parts;
292
+ }
293
+ if (msg?.role === 'user') {
294
+ // Hit next user message without finding assistant response
295
+ break;
296
+ }
297
+ }
298
+ return [];
299
+ },
300
+ /**
301
+ * Delete an analysis result from a session
302
+ * - remove the corresponding prompt-response pair from uiMessages
303
+ * - remove the associated toolAdditionalData
304
+ */
305
+ deleteAnalysisResult: (sessionId, resultId) => {
306
+ set((state) => produce(state, (draft) => {
307
+ const session = draft.ai.config.sessions.find((s) => s.id === sessionId);
308
+ if (session) {
309
+ session.analysisResults = session.analysisResults.filter((r) => r.id !== resultId);
310
+ // Remove corresponding prompt-response pair from uiMessages
311
+ const uiMessages = session.uiMessages;
312
+ const userMessageIndex = uiMessages.findIndex((msg) => msg.id === resultId && msg.role === 'user');
313
+ if (userMessageIndex !== -1) {
314
+ // Find the next user message (or end of array) to determine response boundary
315
+ let nextUserIndex = userMessageIndex + 1;
316
+ const toolCallIdsToDelete = new Set();
317
+ while (nextUserIndex < uiMessages.length &&
318
+ uiMessages[nextUserIndex]?.role !== 'user') {
319
+ const msg = uiMessages[nextUserIndex];
320
+ // Extract toolCallId from message parts
321
+ if (msg?.parts) {
322
+ for (const part of msg.parts) {
323
+ // Check for tool-* or dynamic-tool parts that have toolCallId
324
+ if ('toolCallId' in part &&
325
+ typeof part.toolCallId === 'string') {
326
+ toolCallIdsToDelete.add(part.toolCallId);
327
+ }
328
+ }
329
+ }
330
+ nextUserIndex++;
331
+ }
332
+ // Remove the user message and all assistant messages until the next user message
333
+ session.uiMessages.splice(userMessageIndex, nextUserIndex - userMessageIndex);
334
+ // Increment messagesRevision to force useChat reset
335
+ session.messagesRevision =
336
+ (session.messagesRevision || 0) + 1;
337
+ // Clean up toolAdditionalData for deleted messages
338
+ if (session.toolAdditionalData) {
339
+ // Remove data keyed by the toolCallId from the deleted messages
340
+ toolCallIdsToDelete.forEach((toolCallId) => {
341
+ if (session.toolAdditionalData[toolCallId]) {
342
+ delete session.toolAdditionalData[toolCallId];
343
+ }
344
+ });
345
+ }
346
+ }
347
+ }
348
+ }));
349
+ },
350
+ /**
351
+ * Get analysis results for the current session by transforming UI messages
352
+ * into structured analysis results (user prompt → AI response pairs).
353
+ *
354
+ * @returns Array of analysis results for the current session
355
+ */
356
+ getAnalysisResults: () => {
357
+ const currentSession = get().ai.getCurrentSession();
358
+ if (!currentSession)
359
+ return [];
360
+ return currentSession.analysisResults;
361
+ },
362
+ /**
363
+ * Add an analysis result to the current session
364
+ * - add the message to the uiMessages
365
+ * - add the analysis result to the analysisResults
366
+ */
367
+ addAnalysisResult: (message) => {
368
+ const currentSession = get().ai.getCurrentSession();
369
+ if (!currentSession) {
370
+ console.error('No current session found');
371
+ return;
372
+ }
373
+ set((state) => produce(state, (draft) => {
374
+ // Extract text content from message parts
375
+ const textContent = message.parts
376
+ ?.filter((part) => part.type === 'text')
377
+ ?.map((part) => part.text)
378
+ ?.join('') || '';
379
+ draft.ai.config.sessions
380
+ .find((s) => s.id === currentSession?.id)
381
+ ?.analysisResults.push({
382
+ id: message.id,
383
+ prompt: textContent,
384
+ isCompleted: true,
385
+ });
386
+ }));
387
+ },
388
+ // Chat transport configuration
389
+ chatEndPoint,
390
+ chatHeaders,
391
+ getLocalChatTransport: () => {
392
+ const state = get();
393
+ return createLocalChatTransportFactory({
394
+ store,
395
+ defaultProvider: defaultProvider,
396
+ defaultModel: defaultModel,
397
+ apiKey: state.ai.getApiKeyFromSettings(),
398
+ baseUrl: state.ai.getBaseUrlFromSettings(),
399
+ getInstructions: () => store.getState().ai.getFullInstructions(),
400
+ getCustomModel,
401
+ })();
402
+ },
403
+ getRemoteChatTransport: (endpoint, headers) => createRemoteChatTransportFactory({
404
+ store,
405
+ defaultProvider,
406
+ defaultModel,
407
+ })(endpoint, headers),
408
+ ...createChatHandlers({ store }),
282
409
  },
283
410
  };
284
411
  });
@@ -290,77 +417,6 @@ function getCurrentSessionFromState(state) {
290
417
  const { currentSessionId, sessions } = state.ai.config;
291
418
  return sessions.find((session) => session.id === currentSessionId);
292
419
  }
293
- function findResultById(analysisResults, id) {
294
- return analysisResults.find((result) => result.id === id);
295
- }
296
- /**
297
- * Appends the tool results, tool calls, and analysis to the state
298
- *
299
- * @param resultId - The id of the result to append to
300
- * @param message - The message to append to the state. The structure of the message is defined as:
301
- * - reasoning: string The reasoning of the assistant
302
- * - toolCallMessages: ToolCallMessage[] The tool call messages
303
- * - text: string The final text message
304
- * @param isCompleted - Whether the analysis is completed
305
- * @returns The new state
306
- */
307
- function makeResultsAppender({ resultId, streamMessage, errorMessage, isCompleted, }) {
308
- return (state) => produce(state, (draft) => {
309
- const currentSession = getCurrentSessionFromState(draft);
310
- if (!currentSession) {
311
- console.error('No current session found');
312
- return;
313
- }
314
- const result = findResultById(currentSession.analysisResults, resultId);
315
- if (result) {
316
- if (streamMessage && streamMessage.parts) {
317
- // copy all properties from streamMessage
318
- const newStreamMessage = {
319
- parts: streamMessage.parts.map((part) => {
320
- if (part.type === 'text') {
321
- return {
322
- type: 'text',
323
- text: part.text,
324
- additionalData: part.additionalData,
325
- isCompleted: part.isCompleted,
326
- };
327
- }
328
- else if (part.type === 'tool-invocation') {
329
- return {
330
- type: 'tool-invocation',
331
- toolInvocation: {
332
- toolCallId: part.toolInvocation.toolCallId,
333
- toolName: part.toolInvocation.toolName,
334
- args: part.toolInvocation.args,
335
- state: part.toolInvocation.state,
336
- result: part.toolInvocation.state === 'result'
337
- ? part.toolInvocation.result
338
- : undefined,
339
- },
340
- additionalData: part.additionalData,
341
- isCompleted: part.isCompleted,
342
- };
343
- }
344
- else {
345
- // TODO: handle other part types later
346
- return part;
347
- }
348
- }),
349
- };
350
- result.streamMessage = newStreamMessage;
351
- }
352
- if (errorMessage) {
353
- result.errorMessage = errorMessage;
354
- }
355
- if (isCompleted) {
356
- result.isCompleted = isCompleted;
357
- }
358
- }
359
- else {
360
- console.error('Result not found', resultId);
361
- }
362
- });
363
- }
364
420
  export function useStoreWithAi(selector) {
365
421
  return useBaseRoomStore((state) => selector(state));
366
422
  }