@tuturuuu/ai 0.13.0 → 0.14.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 (53) 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/google-search-usage.ts +62 -0
  5. package/src/chat/google/mira-step-preparation.ts +37 -16
  6. package/src/chat/google/route-credits.ts +6 -7
  7. package/src/chat/google/route-message-preparation.ts +33 -0
  8. package/src/chat/google/route-mira-runtime.ts +56 -32
  9. package/src/chat/google/route.ts +32 -71
  10. package/src/chat/google/stream-finish-persistence.ts +39 -150
  11. package/src/chat/mira-artifact-intent.ts +38 -0
  12. package/src/chat/mira-prompt.ts +29 -0
  13. package/src/chat/mira-render-ui-policy.ts +65 -20
  14. package/src/chat/mira-system-instruction.ts +8 -172
  15. package/src/chat/mira-tool-loop-guard.ts +84 -0
  16. package/src/credits/cap-output-tokens.ts +48 -15
  17. package/src/credits/check-credits.ts +14 -3
  18. package/src/credits/constants.ts +3 -0
  19. package/src/credits/index.ts +1 -0
  20. package/src/generate/route.ts +6 -3
  21. package/src/meetings/audio-contract.ts +8 -0
  22. package/src/meetings/audio-transcripts.ts +25 -0
  23. package/src/meetings/chat-source-footer.ts +1 -0
  24. package/src/meetings/chat.ts +4 -1
  25. package/src/meetings/gemini.ts +72 -20
  26. package/src/meetings/public-chat-search.ts +4 -1
  27. package/src/meetings/transcript.ts +63 -0
  28. package/src/meetings/workspace-tools.ts +3 -1
  29. package/src/studio/metering.ts +5 -1
  30. package/src/tools/definitions/calendar-automation.ts +43 -0
  31. package/src/tools/definitions/calendar.ts +27 -1
  32. package/src/tools/definitions/meta.ts +17 -1
  33. package/src/tools/definitions/tasks.ts +82 -31
  34. package/src/tools/definitions/workspace-user-chat.ts +24 -0
  35. package/src/tools/executors/calendar-automation.ts +207 -0
  36. package/src/tools/executors/calendar.ts +30 -1
  37. package/src/tools/executors/helpers/encryption.ts +12 -9
  38. package/src/tools/executors/search.ts +40 -9
  39. package/src/tools/executors/task-create.ts +232 -0
  40. package/src/tools/executors/task-dashboard.ts +59 -0
  41. package/src/tools/executors/task-read.ts +76 -0
  42. package/src/tools/executors/tasks.ts +50 -205
  43. package/src/tools/executors/theme.ts +48 -0
  44. package/src/tools/google-search-events.ts +4 -0
  45. package/src/tools/mira-sdk-tool-search.ts +73 -0
  46. package/src/tools/mira-task-write-guard.ts +61 -0
  47. package/src/tools/mira-tool-discovery.ts +138 -0
  48. package/src/tools/mira-tool-dispatcher.ts +33 -1
  49. package/src/tools/mira-tool-metadata.ts +29 -1
  50. package/src/tools/mira-tool-names.ts +13 -0
  51. package/src/tools/mira-tool-types.ts +7 -0
  52. package/src/tools/mira-tools.ts +70 -2
  53. package/src/workspace-artifacts.ts +58 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tuturuuu/ai",
3
3
  "license": "MIT",
4
- "version": "0.13.0",
4
+ "version": "0.14.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tutur3u/platform",
@@ -22,34 +22,34 @@
22
22
  "type-check": "tsc --project tsconfig.typecheck.json"
23
23
  },
24
24
  "dependencies": {
25
- "@ai-sdk/amazon-bedrock": "^5.0.78",
26
- "@ai-sdk/anthropic": "^4.0.50",
27
- "@ai-sdk/azure": "^4.0.65",
28
- "@ai-sdk/cerebras": "^3.0.45",
29
- "@ai-sdk/cohere": "^4.0.38",
30
- "@ai-sdk/deepgram": "^3.1.8",
31
- "@ai-sdk/deepinfra": "^3.0.45",
32
- "@ai-sdk/deepseek": "^3.0.40",
33
- "@ai-sdk/elevenlabs": "^3.0.38",
34
- "@ai-sdk/fal": "^3.0.38",
35
- "@ai-sdk/fireworks": "^3.0.48",
36
- "@ai-sdk/gateway": "^4.0.76",
37
- "@ai-sdk/gladia": "^3.0.37",
38
- "@ai-sdk/google": "^4.0.65",
39
- "@ai-sdk/google-vertex": "^5.0.77",
40
- "@ai-sdk/groq": "^4.0.38",
41
- "@ai-sdk/hume": "^3.0.37",
25
+ "@ai-sdk/amazon-bedrock": "^5.0.82",
26
+ "@ai-sdk/anthropic": "^4.0.53",
27
+ "@ai-sdk/azure": "^4.0.70",
28
+ "@ai-sdk/cerebras": "^3.0.48",
29
+ "@ai-sdk/cohere": "^4.0.41",
30
+ "@ai-sdk/deepgram": "^3.1.11",
31
+ "@ai-sdk/deepinfra": "^3.0.48",
32
+ "@ai-sdk/deepseek": "^3.0.44",
33
+ "@ai-sdk/elevenlabs": "^3.0.41",
34
+ "@ai-sdk/fal": "^3.0.41",
35
+ "@ai-sdk/fireworks": "^3.0.51",
36
+ "@ai-sdk/gateway": "^4.0.80",
37
+ "@ai-sdk/gladia": "^3.0.40",
38
+ "@ai-sdk/google": "^4.0.69",
39
+ "@ai-sdk/google-vertex": "^5.0.81",
40
+ "@ai-sdk/groq": "^4.0.41",
41
+ "@ai-sdk/hume": "^3.0.40",
42
42
  "@ai-sdk/lmnt": "^3.0.36",
43
- "@ai-sdk/luma": "^3.0.38",
44
- "@ai-sdk/mistral": "^4.0.40",
45
- "@ai-sdk/openai": "^4.0.62",
46
- "@ai-sdk/openai-compatible": "^3.0.45",
47
- "@ai-sdk/perplexity": "^4.0.40",
48
- "@ai-sdk/react": "^4.0.97",
49
- "@ai-sdk/replicate": "^3.0.38",
50
- "@ai-sdk/revai": "^3.0.37",
51
- "@ai-sdk/togetherai": "^3.0.46",
52
- "@ai-sdk/xai": "^4.0.55",
43
+ "@ai-sdk/luma": "^3.0.41",
44
+ "@ai-sdk/mistral": "^4.0.43",
45
+ "@ai-sdk/openai": "^4.0.66",
46
+ "@ai-sdk/openai-compatible": "^3.0.48",
47
+ "@ai-sdk/perplexity": "^4.0.43",
48
+ "@ai-sdk/react": "^4.0.110",
49
+ "@ai-sdk/replicate": "^3.0.41",
50
+ "@ai-sdk/revai": "^3.0.40",
51
+ "@ai-sdk/togetherai": "^3.0.49",
52
+ "@ai-sdk/xai": "^4.0.58",
53
53
  "@beeper/chat-adapter-matrix": "^0.2.0",
54
54
  "@bitbasti/chat-adapter-webex": "^0.1.0",
55
55
  "@chat-adapter/discord": "^4.40.0",
@@ -75,25 +75,25 @@
75
75
  "@streamdown/code": "^1.1.1",
76
76
  "@streamdown/math": "^1.0.2",
77
77
  "@streamdown/mermaid": "^1.0.2",
78
- "@tuturuuu/google": "0.1.0",
79
- "@tuturuuu/internal-api": "0.38.1",
80
- "@tuturuuu/supabase": "0.5.1",
81
- "@tuturuuu/utils": "0.28.0",
82
- "@vercel/sandbox": "^3.2.1",
78
+ "@tuturuuu/google": "0.1.1",
79
+ "@tuturuuu/internal-api": "0.39.0",
80
+ "@tuturuuu/supabase": "0.5.2",
81
+ "@tuturuuu/utils": "0.29.0",
82
+ "@vercel/sandbox": "^3.3.0",
83
83
  "@zernio/chat-sdk-adapter": "^0.5.1",
84
- "ai": "^7.0.94",
84
+ "ai": "^7.0.107",
85
85
  "bash-tool": "^1.3.19",
86
86
  "chat": "^4.40.0",
87
87
  "chat-adapter-baileys": "^2.1.0",
88
88
  "chat-adapter-blooio": "^0.1.0",
89
89
  "chat-adapter-imessage": "^1.1.0",
90
- "chat-adapter-mattermost": "^1.1.3",
90
+ "chat-adapter-mattermost": "^2.0.0",
91
91
  "chat-adapter-sendblue": "^0.2.0",
92
92
  "chat-adapter-zalo": "^0.1.0",
93
93
  "chat-state-cloudflare-do": "^0.2.0",
94
94
  "chat-state-mysql": "^0.1.0",
95
95
  "dayjs": "^1.11.23",
96
- "next": "^16.3.4",
96
+ "next": "^16.3.5",
97
97
  "qrcode": "^1.5.4",
98
98
  "react": "^19.2.8",
99
99
  "react-dom": "^19.2.8",
@@ -101,15 +101,15 @@
101
101
  "streamdown": "^2.6.0",
102
102
  "unified": "^11.0.5",
103
103
  "uuid": "^14.0.2",
104
- "zca-js": "^2.1.2",
105
- "zod": "^4.5.4"
104
+ "zca-js": "^2.2.0",
105
+ "zod": "^4.6.2"
106
106
  },
107
107
  "devDependencies": {
108
- "@tuturuuu/types": "0.30.0",
108
+ "@tuturuuu/types": "0.31.0",
109
109
  "@tuturuuu/typescript-config": "0.1.2",
110
- "@types/node": "^26.5.0",
110
+ "@types/node": "^26.5.1",
111
111
  "@types/qrcode": "^1.5.6",
112
- "@types/react": "^19.2.18",
112
+ "@types/react": "~19.2.18",
113
113
  "typescript": "7.0.2"
114
114
  },
115
115
  "exports": {
@@ -0,0 +1,105 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ type Part = Record<string, unknown>;
4
+ type Step = {
5
+ content?: readonly unknown[];
6
+ text?: string;
7
+ reasoningText?: string;
8
+ toolCalls?: readonly Part[];
9
+ toolResults?: readonly Part[];
10
+ };
11
+
12
+ /** Keep provider content order; tool results update their original call slot. */
13
+ export function collectAssistantMessageParts(response: {
14
+ steps?: readonly Step[];
15
+ text?: string;
16
+ reasoningText?: string;
17
+ sources?: readonly { sourceId?: string; url?: string; title?: string }[];
18
+ }): Part[] {
19
+ const parts: Part[] = [];
20
+ const calls = new Map<string, Part>();
21
+ const append = (value: unknown): void => {
22
+ if (!value || typeof value !== 'object') return;
23
+ const part = value as Part;
24
+ if (part.type === 'text' || part.type === 'reasoning') {
25
+ if (typeof part.text === 'string' && part.text.length)
26
+ parts.push({ ...part });
27
+ } else if (
28
+ part.type === 'tool-call' ||
29
+ part.type === 'tool-result' ||
30
+ part.type === 'tool-error'
31
+ ) {
32
+ const id =
33
+ typeof part.toolCallId === 'string' ? part.toolCallId : randomUUID();
34
+ let call = calls.get(id);
35
+ if (!call) {
36
+ call = {
37
+ type: 'dynamic-tool',
38
+ toolName: part.toolName ?? 'tool',
39
+ toolCallId: id,
40
+ state: 'input-available',
41
+ };
42
+ calls.set(id, call);
43
+ parts.push(call);
44
+ }
45
+ if (part.type === 'tool-call') {
46
+ call.input = part.input ?? part.args ?? part.arguments ?? {};
47
+ } else if (part.type === 'tool-error') {
48
+ call.state = 'output-error';
49
+ call.errorText =
50
+ typeof part.error === 'string' ? part.error : 'Tool execution failed';
51
+ } else {
52
+ call.state = 'output-available';
53
+ call.output = part.output ?? part.result ?? null;
54
+ }
55
+ } else if (part.type === 'source' && part.sourceType === 'url') {
56
+ parts.push({ ...part, type: 'source-url' });
57
+ } else if (part.type === 'file') {
58
+ const file = part.file as
59
+ | { mediaType?: string; base64?: string }
60
+ | undefined;
61
+ if (file?.mediaType && file.base64)
62
+ parts.push({
63
+ type: 'file',
64
+ mediaType: file.mediaType,
65
+ url: `data:${file.mediaType};base64,${file.base64}`,
66
+ });
67
+ }
68
+ };
69
+ for (const step of response.steps ?? []) {
70
+ parts.push({ type: 'step-start' });
71
+ if (step.content?.length) {
72
+ step.content.forEach(append);
73
+ } else {
74
+ // Legacy callers have only per-step aggregates; never flatten across steps.
75
+ if (step.reasoningText)
76
+ append({ type: 'reasoning', text: step.reasoningText });
77
+ if (step.text) append({ type: 'text', text: step.text });
78
+ for (const part of step.toolCalls ?? [])
79
+ append({ ...part, type: 'tool-call' });
80
+ }
81
+ for (const part of step.toolResults ?? [])
82
+ append({ ...part, type: 'tool-result' });
83
+ }
84
+ if (!parts.some((part) => part.type === 'text') && response.text)
85
+ append({ type: 'text', text: response.text });
86
+ if (
87
+ !parts.some((part) => part.type === 'reasoning') &&
88
+ response.reasoningText
89
+ )
90
+ append({ type: 'reasoning', text: response.reasoningText });
91
+ for (const source of response.sources ?? []) {
92
+ if (
93
+ source.url &&
94
+ !parts.some(
95
+ (part) => part.type === 'source-url' && part.url === source.url
96
+ )
97
+ )
98
+ parts.push({
99
+ ...source,
100
+ type: 'source-url',
101
+ sourceId: source.sourceId ?? source.url,
102
+ });
103
+ }
104
+ return parts;
105
+ }
@@ -0,0 +1,7 @@
1
+ import type { ToolSet } from 'ai';
2
+ import { createGoogleSearchToolSet } from '../../tools/google-search-tool';
3
+
4
+ /** Mira search must retain its executor and grounding validation boundary. */
5
+ export function resolveChatTools(miraTools?: ToolSet | null): ToolSet {
6
+ return miraTools ?? createGoogleSearchToolSet();
7
+ }
@@ -0,0 +1,62 @@
1
+ import { isGoogleSearchToolName } from '../../tools/google-search-events';
2
+
3
+ type SearchStep = {
4
+ toolCalls?: ReadonlyArray<{ toolName?: string }>;
5
+ sources?: ReadonlyArray<unknown>;
6
+ providerMetadata?: {
7
+ google?: { groundingMetadata?: { webSearchQueries?: unknown } };
8
+ };
9
+ };
10
+
11
+ /** Count provider search units, without counting top-level/step metadata twice. */
12
+ export function countGoogleSearchQueries(
13
+ model: string,
14
+ response: SearchStep & { steps?: SearchStep[] },
15
+ allToolCalls: ReadonlyArray<{ toolName?: string }>
16
+ ): number {
17
+ const perQuery = /(?:^|\/)gemini-3(?:[.-]|$)/.test(model);
18
+ const legacyGemini = /(?:^|\/)gemini-[12](?:[.-]|$)/.test(model);
19
+ const countStep = (step: SearchStep): number | null => {
20
+ const raw =
21
+ step.providerMetadata?.google?.groundingMetadata?.webSearchQueries;
22
+ const queries = Array.isArray(raw)
23
+ ? new Set(
24
+ raw
25
+ .filter(
26
+ (query): query is string =>
27
+ typeof query === 'string' && query.trim().length > 0
28
+ )
29
+ .map((query) => query.trim())
30
+ ).size
31
+ : null;
32
+ if (perQuery && queries !== null) return queries;
33
+ const calls =
34
+ step.toolCalls?.filter((call) => isGoogleSearchToolName(call.toolName))
35
+ .length ?? 0;
36
+ const units = queries || calls || (step.sources?.length ? 1 : 0);
37
+ if (!units) return null;
38
+ return legacyGemini ? 1 : calls || units;
39
+ };
40
+ // Each SDK step is a separate provider prompt; Gemini 2.x uses one unit per
41
+ // grounded prompt, while Gemini 3 uses its distinct nonempty search queries.
42
+ let observedStep = false;
43
+ const steps =
44
+ response.steps?.reduce((total, step, index) => {
45
+ const finalMetadata =
46
+ response.providerMetadata?.google?.groundingMetadata?.webSearchQueries;
47
+ const stepMetadata =
48
+ step.providerMetadata?.google?.groundingMetadata?.webSearchQueries;
49
+ const effectiveStep =
50
+ index === response.steps!.length - 1 &&
51
+ !Array.isArray(stepMetadata) &&
52
+ Array.isArray(finalMetadata)
53
+ ? { ...step, providerMetadata: response.providerMetadata }
54
+ : step;
55
+ const units = countStep(effectiveStep);
56
+ observedStep ||= units !== null;
57
+ return total + (units ?? 0);
58
+ }, 0) ?? 0;
59
+ return observedStep
60
+ ? steps
61
+ : (countStep({ ...response, toolCalls: allToolCalls }) ?? 0);
62
+ }
@@ -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
  };
@@ -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
  }