@tuturuuu/ai 0.0.10

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 (130) hide show
  1. package/README.md +76 -0
  2. package/package.json +106 -0
  3. package/src/api-key-hash.ts +28 -0
  4. package/src/calendar/events.ts +34 -0
  5. package/src/calendar/route.ts +114 -0
  6. package/src/chat/credit-source.ts +1 -0
  7. package/src/chat/google/chat-request-schema.ts +150 -0
  8. package/src/chat/google/default-system-instruction.ts +198 -0
  9. package/src/chat/google/message-file-processing.ts +212 -0
  10. package/src/chat/google/mira-step-preparation.ts +221 -0
  11. package/src/chat/google/new/route.ts +368 -0
  12. package/src/chat/google/route-auth.ts +81 -0
  13. package/src/chat/google/route-chat-resolution.ts +98 -0
  14. package/src/chat/google/route-credits.ts +61 -0
  15. package/src/chat/google/route-message-preparation.ts +331 -0
  16. package/src/chat/google/route-mira-runtime.ts +206 -0
  17. package/src/chat/google/route.ts +632 -0
  18. package/src/chat/google/stream-finish-persistence.ts +722 -0
  19. package/src/chat/google/summary/route.ts +153 -0
  20. package/src/chat/mira-render-ui-policy.ts +540 -0
  21. package/src/chat/mira-system-instruction.ts +484 -0
  22. package/src/chat-sdk/adapters.ts +389 -0
  23. package/src/chat-sdk/registry.ts +197 -0
  24. package/src/chat-sdk.ts +33 -0
  25. package/src/core.ts +3 -0
  26. package/src/credits/cap-output-tokens.ts +90 -0
  27. package/src/credits/check-credits.ts +232 -0
  28. package/src/credits/constants.ts +30 -0
  29. package/src/credits/index.ts +46 -0
  30. package/src/credits/model-mapping.ts +92 -0
  31. package/src/credits/reservations.ts +514 -0
  32. package/src/credits/resolve-plan-model.ts +219 -0
  33. package/src/credits/sync-gateway-models.ts +351 -0
  34. package/src/credits/types.ts +109 -0
  35. package/src/credits/use-ai-credits.ts +3 -0
  36. package/src/embeddings/metered.ts +283 -0
  37. package/src/executions/route.ts +137 -0
  38. package/src/generate/route.ts +411 -0
  39. package/src/hooks.ts +7 -0
  40. package/src/meetings/summary/route.ts +7 -0
  41. package/src/meetings/transcription/route.ts +134 -0
  42. package/src/memory/client.ts +158 -0
  43. package/src/memory/config.ts +38 -0
  44. package/src/memory/index.ts +32 -0
  45. package/src/memory/ingest.ts +51 -0
  46. package/src/memory/middleware.ts +35 -0
  47. package/src/memory/operations.ts +480 -0
  48. package/src/memory/scope.ts +102 -0
  49. package/src/memory/settings.ts +121 -0
  50. package/src/memory/types.ts +101 -0
  51. package/src/memory/workspace.ts +36 -0
  52. package/src/memory.ts +1 -0
  53. package/src/mind/patch.ts +146 -0
  54. package/src/mind/route.ts +687 -0
  55. package/src/mind/tools.ts +1500 -0
  56. package/src/mind/types.ts +20 -0
  57. package/src/object/core.ts +3 -0
  58. package/src/object/flashcards/route.ts +140 -0
  59. package/src/object/quizzes/explanation/route.ts +145 -0
  60. package/src/object/quizzes/route.ts +142 -0
  61. package/src/object/types.ts +187 -0
  62. package/src/object/year-plan/route.ts +196 -0
  63. package/src/react.ts +1 -0
  64. package/src/scheduling/algorithm.ts +791 -0
  65. package/src/scheduling/default.ts +36 -0
  66. package/src/scheduling/duration-optimizer.ts +689 -0
  67. package/src/scheduling/index.ts +79 -0
  68. package/src/scheduling/priority-calculator.ts +187 -0
  69. package/src/scheduling/recurrence-calculator.ts +621 -0
  70. package/src/scheduling/templates.ts +892 -0
  71. package/src/scheduling/types.ts +136 -0
  72. package/src/scheduling/web-adapter.ts +308 -0
  73. package/src/scheduling.ts +6 -0
  74. package/src/supported-actions.ts +1 -0
  75. package/src/supported-providers.ts +6 -0
  76. package/src/tools/context-builder.ts +372 -0
  77. package/src/tools/core.ts +1 -0
  78. package/src/tools/definitions/calendar.ts +106 -0
  79. package/src/tools/definitions/finance.ts +197 -0
  80. package/src/tools/definitions/image.ts +74 -0
  81. package/src/tools/definitions/memory.ts +83 -0
  82. package/src/tools/definitions/meta.ts +154 -0
  83. package/src/tools/definitions/render-ui.ts +81 -0
  84. package/src/tools/definitions/tasks.ts +343 -0
  85. package/src/tools/definitions/time-tracking.ts +381 -0
  86. package/src/tools/definitions/workspace-context.ts +45 -0
  87. package/src/tools/definitions/workspace-user-chat.ts +111 -0
  88. package/src/tools/executors/calendar.ts +371 -0
  89. package/src/tools/executors/chat.ts +15 -0
  90. package/src/tools/executors/finance.ts +638 -0
  91. package/src/tools/executors/helpers/encryption.ts +107 -0
  92. package/src/tools/executors/image.ts +247 -0
  93. package/src/tools/executors/markitdown.ts +684 -0
  94. package/src/tools/executors/memory.ts +277 -0
  95. package/src/tools/executors/parallel-checks.ts +176 -0
  96. package/src/tools/executors/qr.ts +170 -0
  97. package/src/tools/executors/scope-helpers.ts +192 -0
  98. package/src/tools/executors/search.ts +149 -0
  99. package/src/tools/executors/settings.ts +40 -0
  100. package/src/tools/executors/tasks.ts +1087 -0
  101. package/src/tools/executors/theme.ts +23 -0
  102. package/src/tools/executors/timer/timer-categories-executor.ts +110 -0
  103. package/src/tools/executors/timer/timer-category-mutations.ts +240 -0
  104. package/src/tools/executors/timer/timer-goal-mutations.ts +323 -0
  105. package/src/tools/executors/timer/timer-goals-executor.ts +272 -0
  106. package/src/tools/executors/timer/timer-helpers.ts +372 -0
  107. package/src/tools/executors/timer/timer-mutation-schemas.ts +160 -0
  108. package/src/tools/executors/timer/timer-mutation-types.ts +212 -0
  109. package/src/tools/executors/timer/timer-mutations.ts +19 -0
  110. package/src/tools/executors/timer/timer-queries.ts +18 -0
  111. package/src/tools/executors/timer/timer-session-lifecycle.ts +299 -0
  112. package/src/tools/executors/timer/timer-session-mutations.ts +10 -0
  113. package/src/tools/executors/timer/timer-session-queries.ts +153 -0
  114. package/src/tools/executors/timer/timer-session-updates.ts +200 -0
  115. package/src/tools/executors/timer/timer-sessions-executor.ts +91 -0
  116. package/src/tools/executors/timer/timer-stats-executor.ts +157 -0
  117. package/src/tools/executors/timer.ts +22 -0
  118. package/src/tools/executors/user.ts +60 -0
  119. package/src/tools/executors/workspace.ts +135 -0
  120. package/src/tools/json-render-catalog.ts +875 -0
  121. package/src/tools/mira-tool-definitions.ts +55 -0
  122. package/src/tools/mira-tool-dispatcher.ts +265 -0
  123. package/src/tools/mira-tool-metadata.ts +164 -0
  124. package/src/tools/mira-tool-names.ts +95 -0
  125. package/src/tools/mira-tool-render-ui.ts +54 -0
  126. package/src/tools/mira-tool-types.ts +17 -0
  127. package/src/tools/mira-tools.ts +167 -0
  128. package/src/tools/normalize-render-ui-input.ts +321 -0
  129. package/src/tools/workspace-context.ts +233 -0
  130. package/src/types.ts +38 -0
@@ -0,0 +1,687 @@
1
+ import { google } from '@ai-sdk/google';
2
+ import { createAdminClient } from '@tuturuuu/supabase/next/server';
3
+ import {
4
+ getPermissions,
5
+ normalizeWorkspaceId,
6
+ verifyWorkspaceMembershipType,
7
+ } from '@tuturuuu/utils/workspace-helper';
8
+ import { consumeStream, gateway, stepCountIs, streamText } from 'ai';
9
+ import { type NextRequest, NextResponse } from 'next/server';
10
+ import { z } from 'zod';
11
+ import type { CreditSource as SharedCreditSource } from '../chat/credit-source';
12
+ import { mapToUIMessages } from '../chat/google/chat-request-schema';
13
+ import {
14
+ type AiRouteAuthResult,
15
+ resolveAiRouteAuth,
16
+ } from '../chat/google/route-auth';
17
+ import { performCreditPreflight } from '../chat/google/route-credits';
18
+ import { prepareProcessedMessages } from '../chat/google/route-message-preparation';
19
+ import { deductAiCredits } from '../credits/check-credits';
20
+ import {
21
+ isGoogleModelId,
22
+ normalizeStableModelId,
23
+ toBareModelName,
24
+ } from '../credits/model-mapping';
25
+ import {
26
+ PlanModelResolutionError,
27
+ resolvePlanModel,
28
+ } from '../credits/resolve-plan-model';
29
+ import { withAiMemory } from '../memory';
30
+ import { createMiraStreamTools } from '../tools/mira-tools';
31
+ import { createMindStreamTools, type MindToolCallbacks } from './tools';
32
+
33
+ type AuthOk = Extract<AiRouteAuthResult, { ok: true }>;
34
+
35
+ const MindChatBodySchema = z.object({
36
+ boardId: z.guid().nullable().optional(),
37
+ clientRunId: z.string().trim().min(1).max(120).optional(),
38
+ creditSource: z.enum(['personal', 'workspace']).optional(),
39
+ creditWsId: z.string().optional(),
40
+ messages: z.array(z.unknown()).optional(),
41
+ model: z.string().optional(),
42
+ thinkingMode: z.enum(['fast', 'thinking']).optional(),
43
+ threadId: z.guid().optional(),
44
+ timezone: z.string().optional(),
45
+ writeMode: z.enum(['direct', 'review']).optional(),
46
+ wsId: z.string().min(1),
47
+ });
48
+
49
+ export type MindRouteCallbacks = MindToolCallbacks & {
50
+ ensureThread(input: {
51
+ boardId?: string | null;
52
+ model?: string | null;
53
+ threadId?: string | null;
54
+ userId: string;
55
+ writeMode: 'direct' | 'review';
56
+ wsId: string;
57
+ }): Promise<string>;
58
+ persistMessage(input: {
59
+ boardId?: string | null;
60
+ content: string;
61
+ metadata?: Record<string, unknown>;
62
+ model?: string | null;
63
+ role: 'assistant' | 'system' | 'tool' | 'user';
64
+ threadId: string;
65
+ toolCalls?: unknown[];
66
+ toolResults?: unknown[];
67
+ usage?: Record<string, unknown>;
68
+ userId: string;
69
+ wsId: string;
70
+ }): Promise<void>;
71
+ resolveAccess(input: {
72
+ auth: AuthOk;
73
+ request: NextRequest;
74
+ wsId: string;
75
+ }): Promise<{ ok: true; wsId: string } | { ok: false; response: Response }>;
76
+ resolveAuth?: (
77
+ request: NextRequest
78
+ ) => Promise<AuthOk | { ok: false; response: Response }>;
79
+ };
80
+
81
+ function collectTextFromMessages(messages: ReturnType<typeof mapToUIMessages>) {
82
+ const latest = [...messages]
83
+ .reverse()
84
+ .find((message) => message.role === 'user');
85
+ if (!latest) return '';
86
+
87
+ return latest.parts
88
+ .map((part) => (part.type === 'text' ? part.text : ''))
89
+ .filter(Boolean)
90
+ .join('\n\n');
91
+ }
92
+
93
+ function buildMindSystemPrompt({
94
+ boardId,
95
+ compactContext,
96
+ timezone,
97
+ writeMode,
98
+ }: {
99
+ boardId?: string | null;
100
+ compactContext: string;
101
+ timezone?: string;
102
+ writeMode: 'direct' | 'review';
103
+ }) {
104
+ return `You are Mind, Tuturuuu's internal planning copilot for mindboards and long-horizon knowledge graphs.
105
+
106
+ Current selected board: ${boardId ?? 'none'}.
107
+ Current timezone: ${timezone ?? 'UTC'}.
108
+ Current write mode: ${writeMode}.
109
+
110
+ Compact Mind context:
111
+ ${compactContext}
112
+
113
+ Use Mind tools to inspect boards, load snapshots or chunks, search nodes, render visual planning UI, and propose structured graph patches. Use convert_file_to_markdown when attached binary files need conversion before analysis. Prefer small coherent patches over huge rewrites. Treat review mode as Draft mode: create applyable draft patches with propose_mind_patch whenever a useful graph change is implied, and do not claim they were applied. Treat direct mode as Implement mode: you may call apply_mind_patch after proposing a patch when the user's intent is clearly to change the board.
114
+
115
+ Be autonomous when it helps: if the user asks for a roadmap, plan, breakdown, structure, refinement, consolidation, timeline, risk pass, or elaboration, inspect/search the board, brainstorm the structure internally, then propose one applyable draft patch. Do not devise a non-applyable plan and a separate duplicate draft. Use render_mind_ui only when it adds a compact, nonduplicative preview of the same pending draft; the applyable propose_mind_patch output is the source of truth. Do not end with "would you like me to draft this" when drafting is clearly useful; draft it.
116
+
117
+ Plan completeness standard: proposals should be concrete enough to apply. When the user asks for a structure, generate the major planning clusters, the important child nodes in each cluster, and the relationships that make the graph useful. Do not stop after one partial cluster when the request clearly spans multiple areas. Prefer 8-24 well-linked operations for normal plans, and more when the board context warrants it. Use parentNodeId plus explicit "contains" edges to form supportive clusters, then add "sequence", "depends_on", "supports", "blocks", or "relates_to" edges between clusters and critical child nodes so the graph is not isolated. Existing graph is authoritative: reuse, update, link, or extend relevant existing nodes by ID before creating replacements. If the existing board already has top-level nodes, enrich all relevant nodes consistently rather than expanding only the first one.
118
+
119
+ Graph health standard: orphaned nodes are nodes with zero inbound and zero outbound relationships. Treat relevant orphaned nodes from inspect_mind_structure or the compact context as repair candidates, not background noise. When the user asks to expand, refine, consolidate, improve, or connect a board, link, reparent, merge, or explicitly account for relevant orphaned nodes before creating more content. New patch nodes should not be left orphaned: every new node should have at least one explicit edge to a parent, sibling, child, or existing anchor whenever another node is available. parentNodeId is useful hierarchy metadata, but parentNodeId alone is not enough for graph connectivity; include a "contains" edge to the parent when possible. New roots/clusters should connect to an existing anchor or to another new cluster unless the user explicitly asks for unrelated alternatives.
120
+
121
+ Stream work visibly through tools. For multi-step graph work, first call the smallest inspection/search/neighborhood tool that proves context, then call propose_mind_patch for useful graph changes. A render_mind_ui call is optional and should be limited to a compact preview of the same draft, never a separate plan artifact with duplicated content. Do not silently think through all work and only answer at the end. If a tool returns ok:false, correct the shape once; do not retry the same invalid patch repeatedly. If render_mind_ui rejects a nested object, retry once with the loose outline shape: {"root":"Title","elements":[{"title":"Section","children":[{"title":"Item"}]}]}.
122
+
123
+ Node statuses are first-class planning state. Use them deliberately:
124
+ - backlog: captured but not yet committed
125
+ - planned: intended and ordered
126
+ - in_progress: actively being executed
127
+ - in_review: waiting for validation, walkthrough, or decision
128
+ - blocked: cannot progress until dependency/risk is resolved
129
+ - completed: done and no longer active
130
+ - deferred: intentionally postponed
131
+ - cancelled: intentionally removed from the plan
132
+
133
+ Large-board navigation strategy: never assume full context is available forever. Start with inspect_mind_structure, search_mind_nodes for the user's topic, then load_mind_neighborhood around relevant nodes. Use load_mind_chunk with offset/limit for broad audits. Keep your own working map of board regions, unresolved questions, duplicates, and chunk cursors while iterating.
134
+
135
+ When showing standalone comparisons, phase maps, or structured planning summaries that do not change the graph, call render_mind_ui instead of pasting large JSON/code blocks. When graph changes are useful, do not make render_mind_ui the main deliverable; use propose_mind_patch as the applyable draft and keep any visual preview compact. Never paste raw patch JSON in the assistant text; the draft patch tool output is rendered by the client with Apply controls.
136
+
137
+ Graph structure rules:
138
+ - Parent/child structure is represented by node.parentNodeId plus a "contains" edge whenever possible. Parent nodes should be higher-level goals, plans, systems, or milestones. Child nodes should be concrete milestones, actions, risks, questions, or resources under that parent.
139
+ - Same-level chronological order should use "sequence" edges. Real prerequisites should use "depends_on"; blockers should use "blocks"; enabling or reinforcing relationships should use "supports"; loose associations should use "relates_to".
140
+ - Always label non-obvious edges with a short relationship phrase such as "requires", "unblocks", "enables", "feeds", or "validates".
141
+ - Build on the current board instead of drafting a detached replacement. If a node already represents the user's topic, use its ID as the parent/anchor; if a related cluster already exists, add missing child nodes and edges into that cluster instead of creating a duplicate root.
142
+ - For structure-generation requests, every new child node should have parentNodeId plus a "contains" edge to its parent whenever possible, and every top-level cluster should have at least one meaningful relationship to the root goal or to another cluster. Avoid creating disconnected islands unless the user explicitly asks for unrelated alternatives.
143
+ - Before proposing a patch, check the isolated/orphaned node list returned by inspect_mind_structure and the compact context. If any isolated node is relevant to the user request, include relationship or parent updates for it in the same draft. Do not leave existing relevant orphaned nodes disconnected while adding new parallel content.
144
+ - When refining relationships, propose update_node parentNodeId changes, create_edge/update_edge operations, and delete_edge operations as needed. Do not solve relationship questions only by adding more isolated nodes.
145
+ - For new nodes and edges, include stable short ids whenever possible. If you create edges to nodes in the same patch, reference the created node id or the create_node operation id consistently. Put all create_node operations before any create_edge, update_node, or update_edge operation that depends on those new nodes.
146
+ - For update_node and update_edge operations, put editable fields at the top level of the operation. Do not nest update fields under "node" or "edge". For create_node and create_edge, use the nested "node" or "edge" object.
147
+ - Use only valid node types: decision, goal, idea, milestone, plan, question, resource, risk, system. Use "idea" for tasks/actions. Use only valid edge types: blocks, contains, contradicts, custom, depends_on, reference, relates_to, sequence, supports. Use "supports" for validates/enables/informs unless it is a real prerequisite.
148
+
149
+ Valid render_mind_ui examples:
150
+ - Full json-render spec:
151
+ {"root":"roadmap","elements":{"roadmap":{"type":"Card","props":{"title":"LMS roadmap"},"children":["phase_1","phase_2"]},"phase_1":{"type":"ListItem","props":{"title":"Phase 1: Foundations","subtitle":"Users, roles, courses, enrollment"},"children":[]},"phase_2":{"type":"ListItem","props":{"title":"Phase 2: Core learning","subtitle":"Quizzes, grading, progress tracking"},"children":[]}}}
152
+ - Loose outline, also accepted:
153
+ {"root":"LMS roadmap","elements":[{"title":"Phase 1: Foundations","children":[{"title":"User and role management"},{"title":"Course creation"}]},{"title":"Phase 2: Core learning","children":[{"title":"Quizzes"},{"title":"Progress dashboards"}]}]}
154
+ - Loose keyed outline, also accepted when root is a display title:
155
+ {"root":"LMS roadmap","elements":{"phase1":{"title":"Phase 1: Foundations","children":[{"title":"User and role management"},{"title":"Course creation"}]},"phase2":{"title":"Phase 2: Core learning","children":[{"title":"Quizzes"},{"title":"Progress dashboards"}]}}}
156
+
157
+ Valid propose_mind_patch example:
158
+ {"boardId":"current","patch":{"summary":"Initialize LMS roadmap","operations":[{"id":"op1","kind":"create_node","node":{"id":"phase1","title":"Phase 1: Foundations","body":"Users, roles, course shell, enrollment.","nodeType":"milestone","horizon":"quarter","status":"planned","positionX":0,"positionY":0}},{"id":"op2","kind":"create_node","node":{"id":"phase2","title":"Phase 2: Core learning","body":"Quizzes, assignments, grading, progress dashboards.","nodeType":"milestone","horizon":"quarter","status":"planned","positionX":320,"positionY":0}},{"id":"op3","kind":"create_edge","edge":{"id":"edge_phase1_phase2","sourceNodeId":"phase1","targetNodeId":"phase2","edgeType":"sequence","label":"then"}}]}}
159
+
160
+ Valid relationship refinement patch example:
161
+ {"boardId":"current","patch":{"summary":"Clarify compliance baseline relationships","operations":[{"id":"rename_baseline","kind":"update_node","nodeId":"5279e3f1-bf4c-4e95-ac64-0d519e10db83","title":"Compliance Baseline & Data Privacy","body":"Define privacy-by-design requirements, PII handling, encryption, and data classification for MVP launch."},{"id":"relabel_mvp_dependency","kind":"update_edge","edgeId":"9da91129-e15f-4774-9a89-ad40199e2b51","edgeType":"blocks","label":"blocks MVP release until satisfied"},{"id":"add_classification","kind":"create_node","node":{"id":"data_classification_framework","title":"Data Classification Framework","body":"Define PII classes, retention expectations, and handling rules.","nodeType":"idea","horizon":"month","status":"planned","parentNodeId":"5279e3f1-bf4c-4e95-ac64-0d519e10db83","positionX":0,"positionY":260}},{"id":"link_baseline_classification","kind":"create_edge","edge":{"id":"edge_baseline_classification","sourceNodeId":"5279e3f1-bf4c-4e95-ac64-0d519e10db83","targetNodeId":"data_classification_framework","edgeType":"contains","label":"contains"}}]}}`;
162
+ }
163
+
164
+ function truncateValue(value: string, maxLength = 160) {
165
+ return value.length <= maxLength ? value : `${value.slice(0, maxLength)}...`;
166
+ }
167
+
168
+ async function buildCompactMindContext({
169
+ boardId,
170
+ callbacks,
171
+ wsId,
172
+ }: {
173
+ boardId?: string | null;
174
+ callbacks: MindRouteCallbacks;
175
+ wsId: string;
176
+ }) {
177
+ const boards = await callbacks.listBoards(wsId);
178
+ const snapshot = boardId ? await callbacks.getSnapshot(wsId, boardId) : null;
179
+ const boardLines = boards
180
+ .slice(0, 12)
181
+ .map((board) =>
182
+ [
183
+ board.title,
184
+ `${board.nodeCount} nodes`,
185
+ `${board.edgeCount} edges`,
186
+ `${board.tagCount} tags`,
187
+ board.defaultHorizon,
188
+ ].join(' | ')
189
+ );
190
+
191
+ if (!snapshot) {
192
+ return [
193
+ `Workspace boards: ${boards.length}`,
194
+ boardLines.length ? boardLines.join('\n') : 'No boards yet.',
195
+ boardId ? 'Selected board snapshot was not found.' : '',
196
+ ]
197
+ .filter(Boolean)
198
+ .join('\n');
199
+ }
200
+
201
+ const degree = new Map<string, number>();
202
+ for (const edge of snapshot.edges) {
203
+ degree.set(edge.sourceNodeId, (degree.get(edge.sourceNodeId) ?? 0) + 1);
204
+ degree.set(edge.targetNodeId, (degree.get(edge.targetNodeId) ?? 0) + 1);
205
+ }
206
+ const nodeById = new Map(snapshot.nodes.map((node) => [node.id, node]));
207
+ const highDegreeNodes = [...snapshot.nodes]
208
+ .sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0))
209
+ .slice(0, 10)
210
+ .map(
211
+ (node) =>
212
+ `${node.title} [${node.id}] (${degree.get(node.id) ?? 0}, ${node.status}, ${node.horizon})`
213
+ );
214
+ const recentNodes = [...snapshot.nodes]
215
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
216
+ .slice(0, 12)
217
+ .map(
218
+ (node) =>
219
+ `${node.title} [${node.id}] | ${node.horizon} | ${node.status} | ${node.nodeType}`
220
+ );
221
+ const statusCounts = snapshot.nodes.reduce<Record<string, number>>(
222
+ (acc, node) => {
223
+ acc[node.status] = (acc[node.status] ?? 0) + 1;
224
+ return acc;
225
+ },
226
+ {}
227
+ );
228
+ const isolatedNodes = snapshot.nodes
229
+ .filter((node) => (degree.get(node.id) ?? 0) === 0)
230
+ .slice(0, 20)
231
+ .map(
232
+ (node) =>
233
+ `${node.title} [${node.id}] | ${node.nodeType} | ${node.status} | ${node.horizon}`
234
+ );
235
+ const relationshipLines = snapshot.edges.slice(0, 30).map((edge) => {
236
+ const source = nodeById.get(edge.sourceNodeId);
237
+ const target = nodeById.get(edge.targetNodeId);
238
+ const label = edge.label ? ` "${edge.label}"` : '';
239
+ return `${edge.edgeType}${label}: ${source?.title ?? edge.sourceNodeId} -> ${
240
+ target?.title ?? edge.targetNodeId
241
+ }`;
242
+ });
243
+
244
+ return [
245
+ `Workspace boards: ${boards.length}`,
246
+ boardLines.length ? `Boards:\n${boardLines.join('\n')}` : 'No boards yet.',
247
+ `Selected board: ${snapshot.board.title}`,
248
+ `Selected board counts: ${snapshot.nodes.length} nodes, ${snapshot.edges.length} edges, ${snapshot.tags.length} tags, ${snapshot.groups.length} groups`,
249
+ snapshot.tags.length
250
+ ? `Tags: ${snapshot.tags
251
+ .slice(0, 30)
252
+ .map((tag) => tag.name)
253
+ .join(', ')}`
254
+ : 'Tags: none',
255
+ snapshot.groups.length
256
+ ? `Groups: ${snapshot.groups
257
+ .slice(0, 20)
258
+ .map((group) => group.name)
259
+ .join(', ')}`
260
+ : 'Groups: none',
261
+ highDegreeNodes.length
262
+ ? `High-degree nodes: ${truncateValue(highDegreeNodes.join(', '), 800)}`
263
+ : 'High-degree nodes: none',
264
+ `Graph health: ${isolatedNodes.length} orphaned nodes in context sample; ${
265
+ snapshot.nodes.filter((node) => (degree.get(node.id) ?? 0) === 0).length
266
+ } total nodes have zero inbound and zero outbound relationships. Treat relevant orphaned nodes as repair candidates in expansion/refinement drafts.`,
267
+ isolatedNodes.length
268
+ ? `Orphaned nodes: ${truncateValue(isolatedNodes.join('; '), 1000)}`
269
+ : 'Orphaned nodes: none',
270
+ `Status counts: ${JSON.stringify(statusCounts)}`,
271
+ relationshipLines.length
272
+ ? `Relationship sample: ${truncateValue(relationshipLines.join('; '), 1200)}`
273
+ : 'Relationship sample: none',
274
+ recentNodes.length
275
+ ? `Recent nodes: ${truncateValue(recentNodes.join('; '), 1000)}`
276
+ : 'Recent nodes: none',
277
+ 'Large-board strategy: use inspect_mind_structure first, then search_mind_nodes, load_mind_neighborhood for relevant nodes, and load_mind_chunk with offset/limit only for broad audits.',
278
+ ].join('\n');
279
+ }
280
+
281
+ function getMindStreamErrorMessage(error: unknown) {
282
+ if (error instanceof Error && error.message.trim()) {
283
+ return `Mind AI could not finish: ${error.message}`;
284
+ }
285
+
286
+ return 'Mind AI could not finish this request.';
287
+ }
288
+
289
+ export function createPOST(callbacks: MindRouteCallbacks) {
290
+ return async function POST(request: NextRequest): Promise<Response> {
291
+ let parsedBody: z.infer<typeof MindChatBodySchema>;
292
+ try {
293
+ parsedBody = MindChatBodySchema.parse(await request.json());
294
+ } catch (error) {
295
+ return NextResponse.json(
296
+ {
297
+ error: 'Invalid Mind AI payload',
298
+ issues: error instanceof z.ZodError ? error.issues : undefined,
299
+ },
300
+ { status: 400 }
301
+ );
302
+ }
303
+
304
+ try {
305
+ const auth = callbacks.resolveAuth
306
+ ? await callbacks.resolveAuth(request)
307
+ : await resolveAiRouteAuth(request);
308
+ if (!auth.ok) return auth.response;
309
+
310
+ const access = await callbacks.resolveAccess({
311
+ auth,
312
+ request,
313
+ wsId: parsedBody.wsId,
314
+ });
315
+ if (!access.ok) return access.response;
316
+
317
+ const sbAdmin = await createAdminClient();
318
+ const requestedCreditSource: SharedCreditSource =
319
+ parsedBody.creditSource ?? 'workspace';
320
+ let requestedCreditWsId: string | undefined;
321
+ try {
322
+ requestedCreditWsId = parsedBody.creditWsId
323
+ ? await normalizeWorkspaceId(
324
+ parsedBody.creditWsId,
325
+ auth.supabase,
326
+ request
327
+ )
328
+ : undefined;
329
+ } catch {
330
+ return NextResponse.json(
331
+ { error: 'Invalid credit workspace identifier' },
332
+ { status: 422 }
333
+ );
334
+ }
335
+
336
+ let billingWsId: string | null = access.wsId;
337
+ if (requestedCreditSource === 'personal') {
338
+ const { data: personalWorkspace, error: personalWorkspaceError } =
339
+ await sbAdmin
340
+ .from('workspaces')
341
+ .select('id, workspace_members!inner(user_id)')
342
+ .eq('personal', true)
343
+ .eq('workspace_members.user_id', auth.user.id)
344
+ .maybeSingle();
345
+
346
+ if (personalWorkspaceError) {
347
+ return NextResponse.json(
348
+ { error: 'Internal error resolving personal workspace' },
349
+ { status: 500 }
350
+ );
351
+ }
352
+
353
+ if (!personalWorkspace?.id) {
354
+ return NextResponse.json(
355
+ {
356
+ code: 'PERSONAL_WORKSPACE_NOT_FOUND',
357
+ error: 'Personal workspace not found.',
358
+ },
359
+ { status: 403 }
360
+ );
361
+ }
362
+
363
+ if (
364
+ requestedCreditWsId &&
365
+ requestedCreditWsId !== personalWorkspace.id
366
+ ) {
367
+ return NextResponse.json(
368
+ {
369
+ code: 'INVALID_CREDIT_SOURCE',
370
+ error: 'Invalid credit workspace for personal credit source.',
371
+ },
372
+ { status: 403 }
373
+ );
374
+ }
375
+
376
+ billingWsId = personalWorkspace.id;
377
+ } else if (requestedCreditWsId) {
378
+ if (requestedCreditWsId !== access.wsId) {
379
+ const membership = await verifyWorkspaceMembershipType({
380
+ requiredType: 'MEMBER',
381
+ supabase: sbAdmin,
382
+ userId: auth.user.id,
383
+ wsId: requestedCreditWsId,
384
+ });
385
+
386
+ if (membership.error === 'membership_lookup_failed') {
387
+ return NextResponse.json(
388
+ { error: 'Internal server error' },
389
+ { status: 500 }
390
+ );
391
+ }
392
+
393
+ if (!membership.ok) {
394
+ return NextResponse.json(
395
+ {
396
+ code: 'INVALID_CREDIT_SOURCE',
397
+ error: 'Workspace access denied for selected credit workspace.',
398
+ },
399
+ { status: 403 }
400
+ );
401
+ }
402
+ }
403
+
404
+ billingWsId = requestedCreditWsId;
405
+ }
406
+
407
+ const writeMode = parsedBody.writeMode ?? 'review';
408
+ const model = normalizeStableModelId(
409
+ parsedBody.model ?? 'google/gemini-2.5-flash'
410
+ );
411
+ let resolvedModelId: string;
412
+ try {
413
+ const resolvedPlanModel = await resolvePlanModel({
414
+ capability: 'language',
415
+ requestedModel: model,
416
+ wsId: billingWsId,
417
+ });
418
+ resolvedModelId = normalizeStableModelId(resolvedPlanModel.modelId);
419
+ } catch (error) {
420
+ if (error instanceof PlanModelResolutionError) {
421
+ return NextResponse.json(
422
+ { code: error.code, error: error.message },
423
+ { status: error.code === 'NO_ALLOCATION' ? 503 : 500 }
424
+ );
425
+ }
426
+
427
+ return NextResponse.json(
428
+ { error: 'Failed to resolve Mind AI model' },
429
+ { status: 500 }
430
+ );
431
+ }
432
+
433
+ const threadId = await callbacks.ensureThread({
434
+ boardId: parsedBody.boardId ?? null,
435
+ model: resolvedModelId,
436
+ threadId: parsedBody.threadId ?? null,
437
+ userId: auth.user.id,
438
+ writeMode,
439
+ wsId: access.wsId,
440
+ });
441
+
442
+ const messages = mapToUIMessages(parsedBody.messages as never);
443
+ const latestUserText = collectTextFromMessages(messages);
444
+ if (latestUserText) {
445
+ await callbacks.persistMessage({
446
+ boardId: parsedBody.boardId ?? null,
447
+ content: latestUserText,
448
+ model: resolvedModelId,
449
+ role: 'user',
450
+ threadId,
451
+ userId: auth.user.id,
452
+ wsId: access.wsId,
453
+ });
454
+ }
455
+
456
+ const creditPreflight = await performCreditPreflight({
457
+ model: resolvedModelId,
458
+ sbAdmin,
459
+ userId: auth.user.id,
460
+ wsId: billingWsId ?? access.wsId,
461
+ });
462
+ if ('error' in creditPreflight) return creditPreflight.error;
463
+
464
+ const supportsThinking =
465
+ resolvedModelId.includes('gemini-2.5') ||
466
+ resolvedModelId.includes('gemini-3');
467
+ const thinkingConfig = supportsThinking
468
+ ? parsedBody.thinkingMode === 'thinking'
469
+ ? { thinkingConfig: { includeThoughts: true } }
470
+ : { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } }
471
+ : {};
472
+ const compactContext = await buildCompactMindContext({
473
+ boardId: parsedBody.boardId ?? null,
474
+ callbacks,
475
+ wsId: access.wsId,
476
+ });
477
+ const mindTools = createMindStreamTools(
478
+ {
479
+ boardId: parsedBody.boardId ?? null,
480
+ threadId,
481
+ userId: auth.user.id,
482
+ writeMode,
483
+ wsId: access.wsId,
484
+ },
485
+ callbacks
486
+ );
487
+ const permissions = await getPermissions({
488
+ user: auth.user,
489
+ wsId: access.wsId,
490
+ });
491
+ const userGroupStorageAccessCache = new Map<string, boolean>();
492
+ const miraFileTools = createMiraStreamTools({
493
+ chatId: threadId,
494
+ creditWsId: billingWsId ?? access.wsId,
495
+ canReadUserGroupStorage: async ({ groupId, storagePath, wsId }) => {
496
+ if (wsId !== access.wsId || storagePath.includes('..')) {
497
+ return false;
498
+ }
499
+
500
+ if (
501
+ !permissions ||
502
+ permissions.withoutPermission('view_user_groups')
503
+ ) {
504
+ return false;
505
+ }
506
+
507
+ const expectedPrefix = `${access.wsId}/user-groups/${groupId}/`;
508
+ if (!storagePath.startsWith(expectedPrefix)) {
509
+ return false;
510
+ }
511
+
512
+ const cachedAccess = userGroupStorageAccessCache.get(groupId);
513
+ if (cachedAccess !== undefined) {
514
+ return cachedAccess;
515
+ }
516
+
517
+ const { data, error } = await sbAdmin
518
+ .from('workspace_user_groups')
519
+ .select('id')
520
+ .eq('ws_id', access.wsId)
521
+ .eq('id', groupId)
522
+ .maybeSingle();
523
+ const allowed = !error && Boolean(data);
524
+ userGroupStorageAccessCache.set(groupId, allowed);
525
+ return allowed;
526
+ },
527
+ supabase: sbAdmin as never,
528
+ timezone: parsedBody.timezone,
529
+ userId: auth.user.id,
530
+ wsId: access.wsId,
531
+ });
532
+ const streamTools = {
533
+ ...mindTools,
534
+ ...(miraFileTools.convert_file_to_markdown
535
+ ? { convert_file_to_markdown: miraFileTools.convert_file_to_markdown }
536
+ : {}),
537
+ };
538
+ type PrepareStep = NonNullable<
539
+ NonNullable<Parameters<typeof streamText>[0]>['prepareStep']
540
+ >;
541
+ const prepareStep: PrepareStep = ({ stepNumber }) => {
542
+ if (stepNumber === 0) {
543
+ return {
544
+ activeTools: [
545
+ 'list_mindboards',
546
+ 'inspect_mind_structure',
547
+ 'get_mindboard_snapshot',
548
+ 'load_mind_neighborhood',
549
+ 'search_mind_nodes',
550
+ 'render_mind_ui',
551
+ 'convert_file_to_markdown',
552
+ ],
553
+ };
554
+ }
555
+
556
+ return {
557
+ activeTools: [
558
+ 'list_mindboards',
559
+ 'inspect_mind_structure',
560
+ 'get_mindboard_snapshot',
561
+ 'load_mind_chunk',
562
+ 'load_mind_neighborhood',
563
+ 'search_mind_nodes',
564
+ 'propose_mind_patch',
565
+ 'render_mind_ui',
566
+ 'convert_file_to_markdown',
567
+ ...(writeMode === 'direct' ? ['apply_mind_patch'] : []),
568
+ ],
569
+ };
570
+ };
571
+ const useGoogleNativeModel = isGoogleModelId(resolvedModelId);
572
+ const preparedMessages = await prepareProcessedMessages(
573
+ messages,
574
+ access.wsId,
575
+ threadId,
576
+ request,
577
+ { attachYoutubeVideoInput: useGoogleNativeModel }
578
+ );
579
+ if ('error' in preparedMessages) return preparedMessages.error;
580
+
581
+ const modelWithMemory = await withAiMemory({
582
+ customId: threadId,
583
+ model: useGoogleNativeModel
584
+ ? google(toBareModelName(resolvedModelId))
585
+ : gateway(resolvedModelId),
586
+ product: 'mind',
587
+ source: 'mind_chat',
588
+ surface: 'mind_chat',
589
+ userId: auth.user.id,
590
+ wsId: access.wsId,
591
+ });
592
+
593
+ const result = streamText({
594
+ abortSignal: request.signal,
595
+ experimental_telemetry: {
596
+ functionId: 'mind.ai.stream',
597
+ isEnabled: true,
598
+ metadata: {
599
+ boardId: parsedBody.boardId ?? 'none',
600
+ clientRunId: parsedBody.clientRunId ?? 'none',
601
+ creditSource: requestedCreditSource,
602
+ creditWsId: billingWsId ?? access.wsId,
603
+ model: resolvedModelId,
604
+ threadId,
605
+ thinkingMode: parsedBody.thinkingMode ?? 'fast',
606
+ writeMode,
607
+ wsId: access.wsId,
608
+ },
609
+ },
610
+ maxRetries: 0,
611
+ maxOutputTokens: creditPreflight.cappedMaxOutput ?? undefined,
612
+ messages: preparedMessages.processedMessages,
613
+ model: modelWithMemory,
614
+ ...(useGoogleNativeModel
615
+ ? { providerOptions: { google: thinkingConfig } }
616
+ : {}),
617
+ stopWhen: stepCountIs(8),
618
+ system: buildMindSystemPrompt({
619
+ boardId: parsedBody.boardId,
620
+ compactContext,
621
+ timezone: parsedBody.timezone,
622
+ writeMode,
623
+ }),
624
+ prepareStep,
625
+ timeout: {
626
+ chunkMs: 20_000,
627
+ stepMs: 45_000,
628
+ totalMs: 120_000,
629
+ },
630
+ toolChoice: 'auto',
631
+ tools: streamTools,
632
+ onFinish: async (response) => {
633
+ const toolCalls = response.steps?.flatMap(
634
+ (step) => step.toolCalls ?? []
635
+ );
636
+ const toolResults = response.steps?.flatMap(
637
+ (step) => step.toolResults ?? []
638
+ );
639
+ const usage = response.totalUsage ?? response.usage ?? {};
640
+ await callbacks.persistMessage({
641
+ boardId: parsedBody.boardId ?? null,
642
+ content: response.text || '',
643
+ metadata: {
644
+ clientRunId: parsedBody.clientRunId ?? null,
645
+ finishReason: response.finishReason,
646
+ threadId,
647
+ },
648
+ model: resolvedModelId,
649
+ role: 'assistant',
650
+ threadId,
651
+ toolCalls,
652
+ toolResults,
653
+ usage: {
654
+ inputTokens: usage.inputTokens ?? 0,
655
+ outputTokens: usage.outputTokens ?? 0,
656
+ reasoningTokens: usage.reasoningTokens ?? 0,
657
+ totalTokens: usage.totalTokens ?? 0,
658
+ },
659
+ userId: auth.user.id,
660
+ wsId: access.wsId,
661
+ });
662
+ await deductAiCredits({
663
+ feature: 'chat',
664
+ inputTokens: usage.inputTokens ?? 0,
665
+ modelId: resolvedModelId,
666
+ outputTokens: usage.outputTokens ?? 0,
667
+ reasoningTokens: usage.reasoningTokens ?? 0,
668
+ userId: auth.user.id,
669
+ wsId: billingWsId ?? access.wsId,
670
+ });
671
+ },
672
+ });
673
+
674
+ return result.toUIMessageStreamResponse({
675
+ consumeSseStream: consumeStream,
676
+ onError: getMindStreamErrorMessage,
677
+ sendReasoning: true,
678
+ sendSources: true,
679
+ });
680
+ } catch {
681
+ return NextResponse.json(
682
+ { error: 'Mind AI could not start this request.' },
683
+ { status: 500 }
684
+ );
685
+ }
686
+ };
687
+ }