@tuturuuu/ai 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/package.json +41 -41
  2. package/src/chat/google/assistant-message-parts.ts +105 -0
  3. package/src/chat/google/chat-tools.ts +7 -0
  4. package/src/chat/google/default-system-instruction.ts +5 -194
  5. package/src/chat/google/google-search-usage.ts +62 -0
  6. package/src/chat/google/message-file-processing.ts +8 -2
  7. package/src/chat/google/mira-step-preparation.ts +37 -16
  8. package/src/chat/google/route-chat-resolution.ts +11 -18
  9. package/src/chat/google/route-credits.ts +6 -7
  10. package/src/chat/google/route-message-preparation.ts +33 -0
  11. package/src/chat/google/route-mira-runtime.ts +56 -32
  12. package/src/chat/google/route.ts +32 -77
  13. package/src/chat/google/stream-finish-persistence.ts +39 -150
  14. package/src/chat/mira-artifact-intent.ts +38 -0
  15. package/src/chat/mira-prompt.ts +29 -0
  16. package/src/chat/mira-render-ui-policy.ts +65 -20
  17. package/src/chat/mira-system-instruction.ts +10 -172
  18. package/src/chat/mira-tool-loop-guard.ts +84 -0
  19. package/src/credits/cap-output-tokens.ts +48 -15
  20. package/src/credits/check-credits.ts +14 -3
  21. package/src/credits/constants.ts +3 -0
  22. package/src/credits/index.ts +1 -0
  23. package/src/generate/route.ts +6 -3
  24. package/src/meetings/audio-contract.ts +8 -0
  25. package/src/meetings/audio-transcripts.ts +25 -0
  26. package/src/meetings/chat-source-footer.ts +1 -0
  27. package/src/meetings/chat.ts +30 -7
  28. package/src/meetings/gemini.ts +84 -23
  29. package/src/meetings/public-chat-search.ts +4 -1
  30. package/src/meetings/transcript.ts +63 -0
  31. package/src/meetings/workspace-tools.ts +3 -1
  32. package/src/studio/metering.ts +5 -1
  33. package/src/tools/definitions/calendar-automation.ts +43 -0
  34. package/src/tools/definitions/calendar.ts +27 -1
  35. package/src/tools/definitions/meta.ts +17 -1
  36. package/src/tools/definitions/tasks.ts +82 -31
  37. package/src/tools/definitions/workspace-user-chat.ts +24 -0
  38. package/src/tools/executors/calendar-automation.ts +207 -0
  39. package/src/tools/executors/calendar.ts +30 -1
  40. package/src/tools/executors/helpers/encryption.ts +12 -9
  41. package/src/tools/executors/search.ts +40 -9
  42. package/src/tools/executors/task-create.ts +232 -0
  43. package/src/tools/executors/task-dashboard.ts +59 -0
  44. package/src/tools/executors/task-read.ts +76 -0
  45. package/src/tools/executors/tasks.ts +50 -205
  46. package/src/tools/executors/theme.ts +48 -0
  47. package/src/tools/google-search-events.ts +4 -0
  48. package/src/tools/mira-sdk-tool-search.ts +73 -0
  49. package/src/tools/mira-task-write-guard.ts +61 -0
  50. package/src/tools/mira-tool-discovery.ts +138 -0
  51. package/src/tools/mira-tool-dispatcher.ts +33 -1
  52. package/src/tools/mira-tool-metadata.ts +29 -1
  53. package/src/tools/mira-tool-names.ts +13 -0
  54. package/src/tools/mira-tool-types.ts +7 -0
  55. package/src/tools/mira-tools.ts +70 -2
  56. package/src/workspace-artifacts.ts +58 -0
@@ -3,17 +3,20 @@ import {
3
3
  buildActiveToolsFromSelected,
4
4
  countRenderUiAttemptsInSteps,
5
5
  extractSelectedToolsFromSteps,
6
+ getWorkspaceArtifactProgress,
6
7
  hasRenderableRenderUiInSteps,
7
8
  hasSuccessfulWorkspaceContextResolutionInSteps,
8
9
  hasToolCallInSteps,
9
10
  removeWorkspaceDiscoveryTools,
10
11
  wasToolEverSelectedInSteps,
11
12
  } from '../mira-render-ui-policy';
13
+ import { getMiraToolLoopReason } from '../mira-tool-loop-guard';
12
14
 
13
15
  export type PrepareMiraToolStepInput = {
14
16
  steps: unknown[];
15
17
  forceGoogleSearch: boolean;
16
18
  forceRenderUi: boolean;
19
+ forceWorkspaceArtifact?: boolean;
17
20
  needsParallelChecks: boolean;
18
21
  needsWorkspaceContextResolution: boolean;
19
22
  needsWorkspaceMembersTool: boolean;
@@ -24,26 +27,41 @@ export function prepareMiraToolStep({
24
27
  steps,
25
28
  forceGoogleSearch,
26
29
  forceRenderUi,
30
+ forceWorkspaceArtifact,
27
31
  needsParallelChecks,
28
32
  needsWorkspaceContextResolution,
29
33
  needsWorkspaceMembersTool,
30
34
  preferMarkdownTables,
31
35
  }: PrepareMiraToolStepInput): {
32
- toolChoice?: 'required';
36
+ toolChoice?: 'required' | 'none';
33
37
  activeTools: string[];
34
38
  } {
39
+ if (forceWorkspaceArtifact) forceRenderUi = false;
40
+ const artifact = getWorkspaceArtifactProgress(steps);
41
+ if (forceWorkspaceArtifact && !artifact.completed && artifact.attempts >= 3)
42
+ return { toolChoice: 'none', activeTools: [] };
43
+ if (
44
+ forceWorkspaceArtifact &&
45
+ !artifact.completed &&
46
+ (!needsWorkspaceContextResolution ||
47
+ hasSuccessfulWorkspaceContextResolutionInSteps(steps))
48
+ ) {
49
+ return { toolChoice: 'required', activeTools: ['show_workspace_artifact'] };
50
+ }
51
+ if (getMiraToolLoopReason(steps))
52
+ return { toolChoice: 'none', activeTools: [] };
35
53
  if (steps.length === 0) {
36
54
  if (forceGoogleSearch) {
37
55
  return {
38
56
  toolChoice: 'required',
39
- activeTools: ['google_search', 'select_tools'],
57
+ activeTools: ['google_search'],
40
58
  };
41
59
  }
42
60
 
43
61
  if (needsParallelChecks) {
44
62
  return {
45
63
  toolChoice: 'required',
46
- activeTools: ['run_parallel_checks', 'select_tools'],
64
+ activeTools: ['run_parallel_checks', 'search_tools', 'select_tools'],
47
65
  };
48
66
  }
49
67
 
@@ -54,6 +72,7 @@ export function prepareMiraToolStep({
54
72
  'list_accessible_workspaces',
55
73
  'get_workspace_context',
56
74
  'set_workspace_context',
75
+ 'search_tools',
57
76
  'select_tools',
58
77
  ],
59
78
  };
@@ -65,13 +84,14 @@ export function prepareMiraToolStep({
65
84
  activeTools: [
66
85
  'get_workspace_context',
67
86
  'list_workspace_members',
87
+ 'search_tools',
68
88
  'select_tools',
69
89
  ],
70
90
  };
71
91
  }
72
92
 
73
93
  return {
74
- activeTools: ['select_tools', 'no_action_needed'],
94
+ activeTools: ['search_tools', 'select_tools', 'no_action_needed'],
75
95
  };
76
96
  }
77
97
 
@@ -106,11 +126,17 @@ export function prepareMiraToolStep({
106
126
  return {
107
127
  toolChoice: 'required',
108
128
  activeTools: hasListedAccessibleWorkspaces
109
- ? ['get_workspace_context', 'set_workspace_context', 'select_tools']
129
+ ? [
130
+ 'get_workspace_context',
131
+ 'set_workspace_context',
132
+ 'search_tools',
133
+ 'select_tools',
134
+ ]
110
135
  : [
111
136
  'list_accessible_workspaces',
112
137
  'get_workspace_context',
113
138
  'set_workspace_context',
139
+ 'search_tools',
114
140
  'select_tools',
115
141
  ],
116
142
  };
@@ -131,6 +157,7 @@ export function prepareMiraToolStep({
131
157
  'get_workspace_context',
132
158
  'list_workspace_members',
133
159
  ...selected,
160
+ 'search_tools',
134
161
  'select_tools',
135
162
  ];
136
163
 
@@ -141,14 +168,7 @@ export function prepareMiraToolStep({
141
168
  }
142
169
 
143
170
  if (forceGoogleSearch && !hasToolCallInSteps(steps, 'google_search')) {
144
- const active = buildActiveToolsFromSelected(toolsForBuild)
145
- .filter((toolName) => toolName !== 'no_action_needed')
146
- .concat('google_search', 'select_tools');
147
-
148
- return {
149
- toolChoice: 'required',
150
- activeTools: Array.from(new Set(active)),
151
- };
171
+ return { toolChoice: 'required', activeTools: ['google_search'] };
152
172
  }
153
173
 
154
174
  if (
@@ -157,7 +177,7 @@ export function prepareMiraToolStep({
157
177
  ) {
158
178
  const active = buildActiveToolsFromSelected(toolsForBuild)
159
179
  .filter((toolName) => toolName !== 'no_action_needed')
160
- .concat('run_parallel_checks', 'select_tools');
180
+ .concat('run_parallel_checks', 'search_tools', 'select_tools');
161
181
 
162
182
  return {
163
183
  toolChoice: 'required',
@@ -178,6 +198,7 @@ export function prepareMiraToolStep({
178
198
  toolName !== 'select_tools' && toolName !== 'no_action_needed'
179
199
  ),
180
200
  'render_ui',
201
+ 'search_tools',
181
202
  'select_tools',
182
203
  ];
183
204
 
@@ -199,7 +220,7 @@ export function prepareMiraToolStep({
199
220
  ) {
200
221
  const active = buildActiveToolsFromSelected(toolsForBuild)
201
222
  .filter((toolName) => toolName !== 'no_action_needed')
202
- .concat('render_ui', 'select_tools');
223
+ .concat('render_ui', 'search_tools', 'select_tools');
203
224
  return {
204
225
  toolChoice: 'required',
205
226
  activeTools: Array.from(new Set(active)),
@@ -209,7 +230,7 @@ export function prepareMiraToolStep({
209
230
  if (hasRenderableRenderUiInSteps(steps)) {
210
231
  const active = buildActiveToolsFromSelected(toolsForBuild)
211
232
  .filter((toolName) => toolName !== 'render_ui')
212
- .concat('select_tools');
233
+ .concat('search_tools', 'select_tools');
213
234
  return {
214
235
  activeTools: Array.from(new Set(active)),
215
236
  };
@@ -59,10 +59,6 @@ export async function resolveChatIdForUser(
59
59
  }
60
60
 
61
61
  type MoveTempFilesToThreadParams = {
62
- loadThread: () => PromiseLike<{
63
- data: { role?: string }[] | null;
64
- error: { message: string } | null;
65
- }>;
66
62
  listFiles: (tempStoragePath: string) => PromiseLike<{
67
63
  data: { name: string }[] | null;
68
64
  error: AdminClientLike | null;
@@ -77,7 +73,6 @@ type MoveTempFilesToThreadParams = {
77
73
  };
78
74
 
79
75
  export async function moveTempFilesToThread({
80
- loadThread,
81
76
  listFiles,
82
77
  moveFile,
83
78
  wsId,
@@ -88,30 +83,21 @@ export async function moveTempFilesToThread({
88
83
  return null;
89
84
  }
90
85
 
91
- const { data: thread, error: threadError } = await loadThread();
92
-
93
- if (threadError) {
94
- console.error('Error getting thread:', threadError);
95
- return new Response(threadError.message, { status: 500 });
96
- }
97
-
98
- if (!thread || thread.length === 0) {
99
- return null;
100
- }
101
-
102
86
  const tempStoragePath = `${wsId}/chats/ai/resources/temp/${userId}`;
103
87
  const { data: files, error: listError } = await listFiles(tempStoragePath);
104
88
 
105
89
  if (listError) {
106
90
  console.error('Error getting files:', listError);
107
- return null;
91
+ return new Response('Could not load chat attachments. Please retry.', {
92
+ status: 502,
93
+ });
108
94
  }
109
95
 
110
96
  if (!files?.length) {
111
97
  return null;
112
98
  }
113
99
 
114
- await Promise.all(
100
+ const moves = await Promise.all(
115
101
  files.map(async (file) => {
116
102
  const fileName = file.name;
117
103
  const { error: copyError } = await moveFile(
@@ -122,8 +108,15 @@ export async function moveTempFilesToThread({
122
108
  if (copyError) {
123
109
  console.error('File copy error:', { fileName, copyError });
124
110
  }
111
+ return copyError;
125
112
  })
126
113
  );
127
114
 
115
+ if (moves.some(Boolean)) {
116
+ return new Response('Could not attach every file. Please retry.', {
117
+ status: 502,
118
+ });
119
+ }
120
+
128
121
  return null;
129
122
  }
@@ -44,15 +44,14 @@ export async function performCreditPreflight({
44
44
  )
45
45
  : null;
46
46
 
47
- if (
48
- cappedMaxOutput === null &&
49
- creditCheck &&
50
- creditCheck.remainingCredits <= 0
51
- ) {
47
+ if (cappedMaxOutput === null) {
52
48
  return {
53
49
  error: Response.json(
54
- { error: 'AI credits insufficient', code: 'CREDITS_EXHAUSTED' },
55
- { status: 403 }
50
+ {
51
+ error: 'AI request budget could not be verified',
52
+ code: 'CREDIT_CHECK_FAILED',
53
+ },
54
+ { status: 503 }
56
55
  ),
57
56
  };
58
57
  }
@@ -377,3 +377,36 @@ export async function persistLatestUserMessage({
377
377
  console.log('User message saved to database');
378
378
  return null;
379
379
  }
380
+
381
+ export function splitSystemMessages(messages: ModelMessage[]) {
382
+ const systemMessages: string[] = [];
383
+ const nonSystemMessages: ModelMessage[] = [];
384
+ for (const message of messages) {
385
+ if (message.role === 'system') {
386
+ const systemMessage = getTextContent(message.content);
387
+ if (systemMessage) systemMessages.push(systemMessage);
388
+ continue;
389
+ }
390
+ nonSystemMessages.push(message);
391
+ }
392
+ return {
393
+ messages: nonSystemMessages,
394
+ system: systemMessages.join('\n\n').trim(),
395
+ };
396
+ }
397
+ function getTextContent(content: ModelMessage['content']) {
398
+ if (typeof content === 'string') return content.trim();
399
+ if (!Array.isArray(content)) return '';
400
+ return content
401
+ .map((part) => (part.type === 'text' ? part.text.trim() : ''))
402
+ .filter(Boolean)
403
+ .join('\n')
404
+ .trim();
405
+ }
406
+
407
+ export function mergeSystemInstructions(...instructions: Array<string | null>) {
408
+ return instructions
409
+ .map((instruction) => instruction?.trim())
410
+ .filter(Boolean)
411
+ .join('\n\n');
412
+ }
@@ -1,19 +1,21 @@
1
1
  import type { TypedSupabaseClient } from '@tuturuuu/supabase/next/client';
2
+ import type { PermissionId } from '@tuturuuu/types';
2
3
  import { normalizeWorkspaceContextId } from '@tuturuuu/utils/constants';
3
4
  import { getPermissions } from '@tuturuuu/utils/workspace-helper';
4
5
  import type { NextRequest } from 'next/server';
5
- import { buildMiraContext } from '../../tools/context-builder';
6
+ import { createMiraSdkToolSearch } from '../../tools/mira-sdk-tool-search';
6
7
  import {
7
8
  createMiraStreamTools,
8
9
  type MiraToolContext,
9
10
  } from '../../tools/mira-tools';
10
11
  import type { MiraWorkspaceContextState } from '../../tools/workspace-context';
11
12
  import { resolveWorkspaceContextState } from '../../tools/workspace-context';
13
+ import { buildMiraPrompt } from '../mira-prompt';
12
14
  import { buildMiraSystemInstruction } from '../mira-system-instruction';
13
15
  import type { ChatRequestTaskBoardContext } from './chat-request-schema';
14
16
 
15
17
  type PermissionResultLike = {
16
- withoutPermission?: (permission: unknown) => boolean;
18
+ withoutPermission?: (permission: PermissionId) => boolean;
17
19
  };
18
20
 
19
21
  type SupabaseClientLike = TypedSupabaseClient;
@@ -171,6 +173,9 @@ export async function prepareMiraRuntime({
171
173
  }: PrepareMiraRuntimeParams): Promise<{
172
174
  miraSystemPrompt?: string;
173
175
  miraTools?: ReturnType<typeof createMiraStreamTools>;
176
+ prepareMiraDiscoveryStep?: ReturnType<
177
+ typeof createMiraSdkToolSearch
178
+ >['prepareStep'];
174
179
  }> {
175
180
  if (!isMiraMode || !wsId) {
176
181
  return {};
@@ -188,26 +193,29 @@ export async function prepareMiraRuntime({
188
193
  });
189
194
  } catch (workspaceContextErr) {
190
195
  console.error(
191
- 'Failed to resolve Mira workspace context, falling back to current workspace:',
196
+ 'Failed to verify Mira workspace context:',
192
197
  workspaceContextErr
193
198
  );
194
- resolvedWorkspaceContext = {
195
- workspaceContextId: wsId,
196
- wsId,
197
- name: 'Current workspace',
198
- personal: false,
199
- memberCount: 0,
199
+ return {
200
+ miraSystemPrompt:
201
+ 'You are Mira. Workspace access could not be verified for this request. Do not claim to read or change workspace data. Explain that the user can retry after access is restored; answer unrelated general questions normally.',
200
202
  };
201
203
  }
202
204
 
203
205
  let withoutPermission: PermissionResultLike['withoutPermission'];
204
206
  const denyPermissionByDefault = () => true;
205
207
  withoutPermission = denyPermissionByDefault;
208
+ const permissionCache = new Map<
209
+ string,
210
+ Promise<PermissionResultLike | null>
211
+ >();
206
212
  try {
207
- const permissionsResult = (await getPermissions({
213
+ const pending = getPermissions({
208
214
  wsId: resolvedWorkspaceContext.wsId,
209
215
  ...(user ? { user } : { request }),
210
- })) as PermissionResultLike | null;
216
+ });
217
+ permissionCache.set(resolvedWorkspaceContext.wsId, pending);
218
+ const permissionsResult = await pending;
211
219
  if (permissionsResult?.withoutPermission) {
212
220
  withoutPermission = permissionsResult.withoutPermission;
213
221
  }
@@ -223,36 +231,46 @@ export async function prepareMiraRuntime({
223
231
  chatId,
224
232
  supabase: miraSupabase,
225
233
  timezone,
234
+ requestHeaders: request.headers,
235
+ authorizeWorkspaceTools: async (targetWsId, permissions) => {
236
+ let pending = permissionCache.get(targetWsId);
237
+ if (!pending) {
238
+ pending = getPermissions({
239
+ wsId: targetWsId,
240
+ ...(user ? { user } : { request }),
241
+ });
242
+ permissionCache.set(targetWsId, pending);
243
+ }
244
+ try {
245
+ const resolved = await pending;
246
+ return (
247
+ !!resolved?.withoutPermission &&
248
+ permissions.every(
249
+ (permission) => !resolved.withoutPermission?.(permission)
250
+ )
251
+ );
252
+ } catch {
253
+ return false;
254
+ }
255
+ },
226
256
  };
227
257
 
228
258
  let miraSystemPrompt: string;
229
259
  try {
230
- const { contextString, soul, isFirstInteraction } = await buildMiraContext({
260
+ const taskBoardInstruction = await buildTaskBoardContextInstruction({
261
+ resolvedWorkspaceContext,
262
+ supabase: miraSupabase,
263
+ taskBoardContext,
264
+ });
265
+ miraSystemPrompt = await buildMiraPrompt({
231
266
  userId,
232
267
  wsId: resolvedWorkspaceContext.wsId,
233
268
  supabase: miraSupabase,
234
269
  timezone,
235
270
  withoutPermission,
271
+ workspaceContext: resolvedWorkspaceContext,
272
+ taskBoardInstruction,
236
273
  });
237
- const dynamicInstruction = buildMiraSystemInstruction({
238
- soul,
239
- isFirstInteraction,
240
- withoutPermission,
241
- });
242
- const workspaceContextInstruction = `## Workspace Context\n\nCurrent task/calendar/finance workspace context: ${resolvedWorkspaceContext.name} (${resolvedWorkspaceContext.personal ? 'personal' : 'shared'} workspace).\nUse this workspace for "my tasks", "my calendar", and "my finance" requests. Only switch to another workspace when the user explicitly names a different workspace.`;
243
- const taskBoardContextInstruction = await buildTaskBoardContextInstruction({
244
- resolvedWorkspaceContext,
245
- supabase: miraSupabase,
246
- taskBoardContext,
247
- });
248
- miraSystemPrompt = [
249
- contextString,
250
- workspaceContextInstruction,
251
- taskBoardContextInstruction,
252
- dynamicInstruction,
253
- ]
254
- .filter(Boolean)
255
- .join('\n\n');
256
274
  } catch (ctxErr) {
257
275
  console.error(
258
276
  'Failed to build Mira context (continuing with default instruction):',
@@ -261,8 +279,14 @@ export async function prepareMiraRuntime({
261
279
  miraSystemPrompt = buildMiraSystemInstruction({ withoutPermission });
262
280
  }
263
281
 
282
+ const discovery = createMiraSdkToolSearch(
283
+ createMiraStreamTools(ctx, withoutPermission, getSteps),
284
+ ctx,
285
+ withoutPermission
286
+ );
264
287
  return {
265
288
  miraSystemPrompt,
266
- miraTools: createMiraStreamTools(ctx, withoutPermission, getSteps),
289
+ miraTools: discovery.tools,
290
+ prepareMiraDiscoveryStep: discovery.prepareStep,
267
291
  };
268
292
  }
@@ -4,13 +4,7 @@ import {
4
4
  normalizeWorkspaceId,
5
5
  verifyWorkspaceMembershipType,
6
6
  } from '@tuturuuu/utils/workspace-helper';
7
- import {
8
- consumeStream,
9
- type ModelMessage,
10
- smoothStream,
11
- stepCountIs,
12
- streamText,
13
- } from 'ai';
7
+ import { consumeStream, smoothStream, stepCountIs, streamText } from 'ai';
14
8
  import { type NextRequest, NextResponse } from 'next/server';
15
9
  import { normalizeStableModelId } from '../../credits/model-mapping';
16
10
  import {
@@ -18,8 +12,8 @@ import {
18
12
  resolvePlanModel,
19
13
  } from '../../credits/resolve-plan-model';
20
14
  import { withAiMemory } from '../../memory';
21
- import { createGoogleSearchToolSet } from '../../tools/google-search-tool';
22
15
  import type { CreditSource as SharedCreditSource } from '../credit-source';
16
+ import { shouldPresentWorkspaceArtifact } from '../mira-artifact-intent';
23
17
  import {
24
18
  shouldForceGoogleSearchForLatestUserMessage,
25
19
  shouldForceRenderUiForLatestUserMessage,
@@ -29,6 +23,7 @@ import {
29
23
  shouldUseParallelChecksForLatestUserMessage,
30
24
  } from '../mira-render-ui-policy';
31
25
  import { ChatRequestBodySchema, mapToUIMessages } from './chat-request-schema';
26
+ import { resolveChatTools } from './chat-tools';
32
27
  import { systemInstruction } from './default-system-instruction';
33
28
  import { prepareMiraToolStep } from './mira-step-preparation';
34
29
  import { resolveChatReasoningSettings } from './reasoning-settings';
@@ -50,9 +45,11 @@ import {
50
45
  import { performCreditPreflight } from './route-credits';
51
46
  import {
52
47
  extractLatestUserMessageContent,
48
+ mergeSystemInstructions,
53
49
  persistLatestUserMessage,
54
50
  persistRequestScopedUserMessage,
55
51
  prepareProcessedMessages,
52
+ splitSystemMessages,
56
53
  } from './route-message-preparation';
57
54
  import { prepareMiraRuntime } from './route-mira-runtime';
58
55
  import {
@@ -60,44 +57,6 @@ import {
60
57
  persistAssistantResponse,
61
58
  } from './stream-finish-persistence';
62
59
 
63
- function splitSystemMessages(messages: ModelMessage[]) {
64
- const systemMessages: string[] = [];
65
- const nonSystemMessages: ModelMessage[] = [];
66
-
67
- for (const message of messages) {
68
- if (message.role === 'system') {
69
- const systemMessage = getTextContent(message.content);
70
- if (systemMessage) systemMessages.push(systemMessage);
71
- continue;
72
- }
73
-
74
- nonSystemMessages.push(message);
75
- }
76
-
77
- return {
78
- messages: nonSystemMessages,
79
- system: systemMessages.join('\n\n').trim(),
80
- };
81
- }
82
-
83
- function getTextContent(content: ModelMessage['content']) {
84
- if (typeof content === 'string') return content.trim();
85
- if (!Array.isArray(content)) return '';
86
-
87
- return content
88
- .map((part) => (part.type === 'text' ? part.text.trim() : ''))
89
- .filter(Boolean)
90
- .join('\n')
91
- .trim();
92
- }
93
-
94
- function mergeSystemInstructions(...instructions: Array<string | null>) {
95
- return instructions
96
- .map((instruction) => instruction?.trim())
97
- .filter(Boolean)
98
- .join('\n\n');
99
- }
100
-
101
60
  export function createPOST(
102
61
  _options: {
103
62
  serverAPIKeyFallback?: boolean;
@@ -368,12 +327,6 @@ export function createPOST(
368
327
  const chatId = resolvedChatId.chatId;
369
328
 
370
329
  const moveFilesError = await moveTempFilesToThread({
371
- loadThread: () =>
372
- sbAdmin
373
- .from('ai_chat_messages')
374
- .select('role, ai_chats!chat_id!inner(creator_id)')
375
- .eq('chat_id', chatId)
376
- .eq('ai_chats.creator_id', user.id),
377
330
  listFiles: (tempStoragePath) =>
378
331
  sbAdmin.storage.from('workspaces').list(tempStoragePath),
379
332
  moveFile: (fromPath, toPath) =>
@@ -480,21 +433,22 @@ export function createPOST(
480
433
 
481
434
  const stepsRef: { current: unknown[] } = { current: [] };
482
435
 
483
- const { miraSystemPrompt, miraTools } = await prepareMiraRuntime({
484
- isMiraMode,
485
- wsId: normalizedWsId ?? undefined,
486
- workspaceContextId,
487
- creditWsId: billingWsId ?? normalizedWsId ?? undefined,
488
- request: req,
489
- user,
490
- userId: user.id,
491
- chatId,
492
- supabase,
493
- toolSupabase: sbAdmin as typeof supabase,
494
- timezone,
495
- taskBoardContext,
496
- getSteps: () => stepsRef.current,
497
- });
436
+ const { miraSystemPrompt, miraTools, prepareMiraDiscoveryStep } =
437
+ await prepareMiraRuntime({
438
+ isMiraMode,
439
+ wsId: normalizedWsId ?? undefined,
440
+ workspaceContextId,
441
+ creditWsId: billingWsId ?? normalizedWsId ?? undefined,
442
+ request: req,
443
+ user,
444
+ userId: user.id,
445
+ chatId,
446
+ supabase,
447
+ toolSupabase: sbAdmin as typeof supabase,
448
+ timezone,
449
+ taskBoardContext,
450
+ getSteps: () => stepsRef.current,
451
+ });
498
452
 
499
453
  const effectiveSource = isMiraMode ? 'Mira' : 'Rewise';
500
454
 
@@ -509,6 +463,9 @@ export function createPOST(
509
463
  });
510
464
 
511
465
  const reasoningSettings = resolveChatReasoningSettings(thinkingMode);
466
+ const forceWorkspaceArtifact = shouldPresentWorkspaceArtifact(
467
+ promptMessages.messages
468
+ );
512
469
  const forceRenderUi = shouldForceRenderUiForLatestUserMessage(
513
470
  promptMessages.messages
514
471
  );
@@ -529,14 +486,13 @@ export function createPOST(
529
486
  promptMessages.messages
530
487
  );
531
488
 
532
- const googleSearchTool = createGoogleSearchToolSet();
533
-
534
489
  type PrepareStep = NonNullable<
535
490
  NonNullable<Parameters<typeof streamText>[0]>['prepareStep']
536
491
  >;
537
- const prepareStep: PrepareStep = ({ steps }) => {
492
+ const prepareStep: PrepareStep = async ({ steps }) => {
538
493
  stepsRef.current = steps;
539
- return prepareMiraToolStep({
494
+ const policy = prepareMiraToolStep({
495
+ forceWorkspaceArtifact,
540
496
  steps,
541
497
  forceGoogleSearch,
542
498
  forceRenderUi,
@@ -545,6 +501,9 @@ export function createPOST(
545
501
  needsWorkspaceMembersTool,
546
502
  preferMarkdownTables,
547
503
  });
504
+ return prepareMiraDiscoveryStep
505
+ ? prepareMiraDiscoveryStep(policy)
506
+ : policy;
548
507
  };
549
508
 
550
509
  const { lease: claimedLease, response: leaseResponse } =
@@ -597,20 +556,16 @@ export function createPOST(
597
556
  promptMessages.system
598
557
  ),
599
558
  ...(cappedMaxOutput ? { maxOutputTokens: cappedMaxOutput } : {}),
559
+ tools: resolveChatTools(miraTools),
600
560
  ...(miraTools
601
561
  ? {
602
- tools: { ...miraTools, ...googleSearchTool } as NonNullable<
603
- Parameters<typeof streamText>[0]
604
- >['tools'],
605
562
  stopWhen: stepCountIs(25),
606
563
  toolChoice: 'auto' as const,
607
564
  prepareStep: prepareStep as NonNullable<
608
565
  NonNullable<Parameters<typeof streamText>[0]>['prepareStep']
609
566
  >,
610
567
  }
611
- : {
612
- tools: googleSearchTool,
613
- }),
568
+ : {}),
614
569
  providerOptions: {
615
570
  google: {
616
571
  thinkingConfig: {