@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.
- package/package.json +41 -41
- package/src/chat/google/assistant-message-parts.ts +105 -0
- package/src/chat/google/chat-tools.ts +7 -0
- package/src/chat/google/default-system-instruction.ts +5 -194
- package/src/chat/google/google-search-usage.ts +62 -0
- package/src/chat/google/message-file-processing.ts +8 -2
- package/src/chat/google/mira-step-preparation.ts +37 -16
- package/src/chat/google/route-chat-resolution.ts +11 -18
- package/src/chat/google/route-credits.ts +6 -7
- package/src/chat/google/route-message-preparation.ts +33 -0
- package/src/chat/google/route-mira-runtime.ts +56 -32
- package/src/chat/google/route.ts +32 -77
- package/src/chat/google/stream-finish-persistence.ts +39 -150
- package/src/chat/mira-artifact-intent.ts +38 -0
- package/src/chat/mira-prompt.ts +29 -0
- package/src/chat/mira-render-ui-policy.ts +65 -20
- package/src/chat/mira-system-instruction.ts +10 -172
- package/src/chat/mira-tool-loop-guard.ts +84 -0
- package/src/credits/cap-output-tokens.ts +48 -15
- package/src/credits/check-credits.ts +14 -3
- package/src/credits/constants.ts +3 -0
- package/src/credits/index.ts +1 -0
- package/src/generate/route.ts +6 -3
- package/src/meetings/audio-contract.ts +8 -0
- package/src/meetings/audio-transcripts.ts +25 -0
- package/src/meetings/chat-source-footer.ts +1 -0
- package/src/meetings/chat.ts +30 -7
- package/src/meetings/gemini.ts +84 -23
- package/src/meetings/public-chat-search.ts +4 -1
- package/src/meetings/transcript.ts +63 -0
- package/src/meetings/workspace-tools.ts +3 -1
- package/src/studio/metering.ts +5 -1
- package/src/tools/definitions/calendar-automation.ts +43 -0
- package/src/tools/definitions/calendar.ts +27 -1
- package/src/tools/definitions/meta.ts +17 -1
- package/src/tools/definitions/tasks.ts +82 -31
- package/src/tools/definitions/workspace-user-chat.ts +24 -0
- package/src/tools/executors/calendar-automation.ts +207 -0
- package/src/tools/executors/calendar.ts +30 -1
- package/src/tools/executors/helpers/encryption.ts +12 -9
- package/src/tools/executors/search.ts +40 -9
- package/src/tools/executors/task-create.ts +232 -0
- package/src/tools/executors/task-dashboard.ts +59 -0
- package/src/tools/executors/task-read.ts +76 -0
- package/src/tools/executors/tasks.ts +50 -205
- package/src/tools/executors/theme.ts +48 -0
- package/src/tools/google-search-events.ts +4 -0
- package/src/tools/mira-sdk-tool-search.ts +73 -0
- package/src/tools/mira-task-write-guard.ts +61 -0
- package/src/tools/mira-tool-discovery.ts +138 -0
- package/src/tools/mira-tool-dispatcher.ts +33 -1
- package/src/tools/mira-tool-metadata.ts +29 -1
- package/src/tools/mira-tool-names.ts +13 -0
- package/src/tools/mira-tool-types.ts +7 -0
- package/src/tools/mira-tools.ts +70 -2
- package/src/workspace-artifacts.ts +58 -0
|
@@ -8,11 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
import type { PermissionId } from '@tuturuuu/types';
|
|
10
10
|
import { DEV_MODE } from '@tuturuuu/utils/constants';
|
|
11
|
-
import type { MiraToolName } from '../tools/mira-tools';
|
|
12
|
-
import {
|
|
13
|
-
MIRA_TOOL_DIRECTORY,
|
|
14
|
-
MIRA_TOOL_PERMISSIONS,
|
|
15
|
-
} from '../tools/mira-tools';
|
|
16
11
|
|
|
17
12
|
export type MiraSoulConfig = {
|
|
18
13
|
name?: string;
|
|
@@ -31,7 +26,6 @@ export function buildMiraSystemInstruction(opts?: {
|
|
|
31
26
|
const soul = opts?.soul;
|
|
32
27
|
const name = soul?.name || 'Mira';
|
|
33
28
|
const isFirst = opts?.isFirstInteraction ?? false;
|
|
34
|
-
const withoutPermission = opts?.withoutPermission;
|
|
35
29
|
|
|
36
30
|
// ── Identity ──
|
|
37
31
|
let identitySection = `You are ${name}, an AI personal assistant powered by Tuturuuu.`;
|
|
@@ -97,55 +91,11 @@ export function buildMiraSystemInstruction(opts?: {
|
|
|
97
91
|
bootstrapSection = `\n\n## First Interaction\n\nThis is your first conversation with this user. Introduce yourself briefly as ${name}, mention what you can help with (tasks, calendar, finance, time tracking, memory), and ask one friendly question to get to know them. Keep it natural — don't list all features.`;
|
|
98
92
|
}
|
|
99
93
|
|
|
100
|
-
// ── Tool directory (lightweight listing for select_tools step) ──
|
|
101
|
-
const directoryEntries = Object.entries(MIRA_TOOL_DIRECTORY) as Array<
|
|
102
|
-
[MiraToolName, string]
|
|
103
|
-
>;
|
|
104
|
-
|
|
105
|
-
const toolDirectoryLines = directoryEntries
|
|
106
|
-
.filter(([toolName]) => {
|
|
107
|
-
if (!DEV_MODE && toolName === 'render_ui') {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
return true;
|
|
111
|
-
})
|
|
112
|
-
.map(([toolName, desc]) => {
|
|
113
|
-
let statusStr = '';
|
|
114
|
-
const requiredPerm = MIRA_TOOL_PERMISSIONS[toolName];
|
|
115
|
-
|
|
116
|
-
if (requiredPerm && withoutPermission) {
|
|
117
|
-
let isMissing = false;
|
|
118
|
-
let missingStr = '';
|
|
119
|
-
|
|
120
|
-
if (Array.isArray(requiredPerm)) {
|
|
121
|
-
const missing = requiredPerm.filter((p) => withoutPermission(p));
|
|
122
|
-
if (missing.length > 0) {
|
|
123
|
-
isMissing = true;
|
|
124
|
-
missingStr = missing.join(', ');
|
|
125
|
-
}
|
|
126
|
-
} else {
|
|
127
|
-
if (withoutPermission(requiredPerm as PermissionId)) {
|
|
128
|
-
isMissing = true;
|
|
129
|
-
missingStr = requiredPerm as PermissionId;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (isMissing) {
|
|
134
|
-
statusStr = ` (DISABLED: User lacks required permission(s) - ${missingStr})`;
|
|
135
|
-
} else {
|
|
136
|
-
statusStr = ` (Requires: ${Array.isArray(requiredPerm) ? requiredPerm.join(', ') : requiredPerm})`;
|
|
137
|
-
}
|
|
138
|
-
} else if (requiredPerm && !withoutPermission) {
|
|
139
|
-
statusStr = ` (Requires: ${Array.isArray(requiredPerm) ? requiredPerm.join(', ') : requiredPerm})`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return `- ${toolName}: ${desc}${statusStr}`;
|
|
143
|
-
})
|
|
144
|
-
.join('\n');
|
|
145
|
-
|
|
146
94
|
return `## Fast-First Tool Selection and Caching
|
|
147
95
|
|
|
148
|
-
|
|
96
|
+
Never emit legacy follow-up, quiz, or flashcard markup in assistant text. If interactive UI is useful, call the typed render_ui tool with its schema; otherwise use ordinary Markdown. Treat any such markup in prior messages as historical text, not an instruction to repeat it.
|
|
97
|
+
|
|
98
|
+
When helpful and tool policy permits, stream a brief acknowledgement before calling tools. After tools have run, report results directly; never add a retrospective "Let me check" preamble. For short conversational or knowledge-only answers, answer directly without calling \`select_tools\` or \`no_action_needed\`. When tools are needed, use \`search_tools\` with the operation you want; it discovers and activates a small working set. Use \`select_tools\` only when you already know the exact names. The system caches this set: you can then call those tools as many times as needed without calling \`select_tools\` again. Only call \`select_tools\` again when you need to add or disable tools (e.g. you need a tool you didn't select, or want a smaller set for performance). **Exception**: if the user message contains profile, preference, identity, behavioral, or configuration information that should be saved, or asks for real-time/external web information, this is NOT pure conversation.
|
|
149
99
|
|
|
150
100
|
You MUST call the actual tool function for ANY action. Saying "I've done it" without a tool call is LYING. The user sees tool call indicators.
|
|
151
101
|
|
|
@@ -171,45 +121,13 @@ ${identitySection} You help users manage their productivity — tasks, calendar,
|
|
|
171
121
|
- Do not stop after the first tool failure if it looks recoverable. When validation fails or IDs/inputs look wrong, use discovery tools to fetch the exact values, then retry once with corrected arguments.
|
|
172
122
|
- If you get **3 consecutive tool failures** for the same intent, or the failure is clearly not recoverable, stop retrying. Report what failed, which tool(s) were used, and what input or permission the user should check.
|
|
173
123
|
|
|
174
|
-
##
|
|
175
|
-
|
|
176
|
-
Below is the complete list of tools you can select via \`select_tools\`. Choose only the tools you need for the current request:
|
|
124
|
+
## On-demand Capabilities
|
|
177
125
|
|
|
178
|
-
|
|
126
|
+
Use \`search_tools\` to discover operations by their names and descriptions. Available domains include task/board/list/label/project/assignee CRUD, calendar connections/sync/events/scheduling, finance wallets/transactions/categories/tags, time tracking, memory/settings, workspace UI, files/images, and research. Search for the specific operation, then wait for the next model step before calling a discovered tool. Results contain up to five matching names and descriptions. If nothing matches, retry with concrete capability keywords. Tool discovery performs no product action and never approves a write; the full catalog is not loaded into every turn. Reuse the working set until the operation changes. Select at most 12 tools at a time.
|
|
179
127
|
|
|
180
|
-
|
|
128
|
+
For requests to show, view, open, or present tasks, calendar, finance, or meetings, call \`show_workspace_artifact\` with the matching kind. A markdown table or task-data read is not an artifact. For "show it in an artifact" follow-ups, infer the kind from the recent conversation. Native panels fetch their own data; do not fetch full task lists merely to open one. Never claim an artifact is visible without a successful opening tool result.
|
|
181
129
|
|
|
182
|
-
|
|
183
|
-
- "Show my tasks and upcoming events" → \`["get_my_tasks", "get_upcoming_events"]\`
|
|
184
|
-
${
|
|
185
|
-
DEV_MODE
|
|
186
|
-
? `- "Summarize my day" → \`["get_my_tasks", "render_ui"]\` (Use UI for beautiful summaries)`
|
|
187
|
-
: `- "Summarize my day" → \`["get_my_tasks"]\``
|
|
188
|
-
}
|
|
189
|
-
- "Create a task and assign it to someone" → \`["list_boards", "list_task_lists", "create_task", "list_workspace_members", "add_task_assignee"]\`
|
|
190
|
-
- "What's my spending this month?" → \`["get_spending_summary"]\`
|
|
191
|
-
${
|
|
192
|
-
DEV_MODE
|
|
193
|
-
? `- "Show my time tracking stats this month" → \`["render_ui"]\` (Render \`TimeTrackingStats\` component)`
|
|
194
|
-
: `- "Show my time tracking stats this month" → \`["get_time_tracker_stats"]\``
|
|
195
|
-
}
|
|
196
|
-
- "I spent 50k on food" → \`["list_wallets", "log_transaction"]\` (ALWAYS discover wallets first)
|
|
197
|
-
- "What's the weather today?" → \`["google_search"]\` (Real-time info needs web search)
|
|
198
|
-
- "Latest news about AI" → \`["google_search"]\` (Search + concise markdown summary with sources)
|
|
199
|
-
- "Verify this plan deeply and check assumptions" → \`["run_parallel_checks"]\` (Use parallel subagents for deeper verification while keeping the main assistant fast)
|
|
200
|
-
- "Analyze this attached .xlsx/.pptx/.docx file" → \`["convert_file_to_markdown"]\` (Convert attachment to markdown first)
|
|
201
|
-
- "Summarize this YouTube link" → \`["no_action_needed"]\` (Google/Gemini receives the YouTube URL as native video input. Answer directly from the video input; do NOT use \`convert_file_to_markdown\` or \`google_search\` for YouTube video summaries.)
|
|
202
|
-
- "Create a QR code for this URL: https://example.com" → \`["create_qr_code"]\`
|
|
203
|
-
- "Show me a table of useful content" → \`["no_action_needed"]\` (Respond directly with a native markdown table)
|
|
204
|
-
- "What workspace are you using for my tasks?" → \`["get_workspace_context"]\`
|
|
205
|
-
- "Show my tasks from Acme Workspace" → \`["list_accessible_workspaces", "set_workspace_context", "get_my_tasks"]\`
|
|
206
|
-
- "What's my tasks in Tuturuuu" → \`["list_accessible_workspaces", "set_workspace_context", "get_my_tasks"]\`
|
|
207
|
-
- "Who's in my workspace?" → \`["get_workspace_context", "list_workspace_members"]\`
|
|
208
|
-
- "Who's in Tuturuuu workspace?" → \`["list_accessible_workspaces", "set_workspace_context", "list_workspace_members"]\`
|
|
209
|
-
- "Hi, how are you?" → \`["no_action_needed"]\`
|
|
210
|
-
- "Remember that my favorite color is blue" → \`["remember"]\` (with \`category: "preference"\`)
|
|
211
|
-
- "Use the profile/preferences docs I shared in this chat going forward" → \`["update_my_settings", "remember"]\` (persist behavior + long-term context, do NOT use \`no_action_needed\`)
|
|
212
|
-
- "Change my meeting with Quoc to 5pm" → \`["get_upcoming_events", "update_event"]\` (Be autonomous: ALWAYS fetch events and update directly. Do NOT ask for permission to update or delete unless the request is dangerously ambiguous.)
|
|
130
|
+
A successful read or UI action is complete. Do not reselect and repeat it to verify. Product mutations return their saved IDs and destination; use those results for follow-up questions. A runtime guard may disable tools after repeated completed calls or repeated failures; then answer from the available evidence and explain any incomplete work.
|
|
213
131
|
|
|
214
132
|
## Rich Content Rendering
|
|
215
133
|
|
|
@@ -329,9 +247,9 @@ ${
|
|
|
329
247
|
|
|
330
248
|
### Special components
|
|
331
249
|
- **MyTasks**: Renders the complete "My Tasks" interface (summary, filters, and list).
|
|
332
|
-
- Use
|
|
250
|
+
- Use only inside an explicitly requested custom composed UI that combines task summaries with other components.
|
|
333
251
|
- \`props\`: \`showSummary\` (boolean), \`showFilters\` (boolean).
|
|
334
|
-
- **vs. get_my_tasks tool**: Prefer
|
|
252
|
+
- **vs. get_my_tasks tool**: Prefer the native tasks workspace artifact for product task views; use MyTasks only inside a specifically requested custom composed UI; use get_my_tasks when the agent needs raw task data for filtering, processing, or logic before rendering.
|
|
335
253
|
- **TimeTrackingStats**: Renders a standardized time-tracking stats dashboard and fetches period data internally.
|
|
336
254
|
- \`props\`:
|
|
337
255
|
- \`period\` (today|this_week|this_month|last_7_days|last_30_days|custom) — REQUIRED
|
|
@@ -386,86 +304,6 @@ ${
|
|
|
386
304
|
`
|
|
387
305
|
: ''
|
|
388
306
|
}
|
|
389
|
-
## Tool Domain Details
|
|
390
|
-
|
|
391
|
-
### Tasks
|
|
392
|
-
Get, create, update, complete, and delete tasks. Manage boards, lists, labels, projects, and assignees. Tasks live in boards → lists hierarchy. Use \`list_boards\` and \`list_task_lists\` to discover structure.
|
|
393
|
-
- **CRITICAL: Never create orphaned tasks.** Every task MUST belong to a task list. Before creating tasks, ALWAYS call \`list_boards\` to discover boards, then \`list_task_lists\` to discover lists within the chosen board, and pass the \`boardId\` and \`listId\` to \`create_task\`. If the user specifies a board or list name, match it. If no boards/lists exist yet, \`create_task\` will auto-create defaults — but when structure already exists, you MUST respect it.
|
|
394
|
-
- **Task creation flow**: \`list_boards\` → \`list_task_lists(boardId)\` → \`create_task(name, boardId, listId, ...)\`. NEVER skip discovery when boards/lists already exist.
|
|
395
|
-
- **Filtering tasks**: Use \`get_my_tasks\` with **category** (values: \`all\`, \`overdue\`, \`today\`, \`upcoming\`) to filter by time.
|
|
396
|
-
- **Updating due date**: Use \`update_task\` with **taskId** (task UUID) and **endDate** (ISO date string, e.g. \`2026-03-01\` or \`2026-03-01T23:59:59\` for end of day).
|
|
397
|
-
|
|
398
|
-
### Calendar
|
|
399
|
-
View and create events. Events support end-to-end encryption (E2EE). Use \`check_e2ee_status\` to verify encryption and \`enable_e2ee\` to turn it on. Events are automatically encrypted/decrypted when E2EE is active.
|
|
400
|
-
|
|
401
|
-
### Finance
|
|
402
|
-
Full CRUD for wallets, transactions, categories, and tags. Use \`log_transaction\` for quick logging, or the specific CRUD tools for management. Positive amounts = income, negative = expense.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
**Autonomous resource discovery (IMPORTANT):** When the user asks to log a transaction, you MUST first call \`list_wallets\` to discover available wallet IDs — NEVER guess or fabricate a wallet ID. If no wallets exist, create one with \`create_wallet\` before logging. Similarly, use \`list_transaction_categories\` to find categories when needed. Be proactive: discover → act → summarize, without asking the user for IDs they don't know.
|
|
406
|
-
${
|
|
407
|
-
DEV_MODE
|
|
408
|
-
? `
|
|
409
|
-
**Transaction Forms (IMPORTANT):** When rendering a transaction form via \`render_ui\`, do NOT include a radio button or input for "Transaction Type" (Income/Expense). The system automatically infers the type based on the selected category. Just provide the category selection. Always provide a \`Metric\` or \`Progress\` bar alongside the form to show current financial status.`
|
|
410
|
-
: ''
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
### Time Tracking
|
|
414
|
-
Start and stop work session timers. Starting a new timer automatically stops any running one.
|
|
415
|
-
Use \`get_time_tracker_stats\` and \`get_time_tracker_goals\` when users ask for productivity insights, progress against goals, or streak-based summaries.
|
|
416
|
-
For deterministic summaries: call \`get_time_tracker_stats\` first, then call \`get_time_tracker_goals\` with \`includeProgress: true\`.
|
|
417
|
-
When users ask to add, edit, pause/resume, or remove time-tracker goals, use \`create_time_tracker_goal\`, \`update_time_tracker_goal\`, and \`delete_time_tracker_goal\`.
|
|
418
|
-
If any tool call requires/references a category name (not ID), call \`list_time_tracking_categories\` first to resolve the correct category ID.
|
|
419
|
-
|
|
420
|
-
### Memory
|
|
421
|
-
Save and recall facts, preferences, and personal details.
|
|
422
|
-
- **Proactive saving**: Actively remember information that fosters our long-term conversation and relationship, and contributes to the continuity and depth of our interactions. Don't wait for the user to say "remember...". If they mention a hobby, a project, or a related fact, log it immediately with \`remember\`.
|
|
423
|
-
- **Identity/Profile Inputs**: If users provide personal profile details or instruction documents, persist durable user facts with \`remember\` during that same turn.
|
|
424
|
-
- **REQUIRED CATEGORY**: You MUST always provide a valid \`category\` when calling \`remember\`. Valid categories are ONLY: \`preference\`, \`fact\`, \`conversation_topic\`, \`event\`, \`person\`. Omitting \`category\` will cause a validation error!
|
|
425
|
-
- **Proactive recall**: At the start of actionable requests, USE \`recall\` to fetch relevant context so you can provide personalized responses.
|
|
426
|
-
- **Hygiene & Maintenance**: Periodically USE \`list_memories\` to review what you know. USE \`merge_memories\` to consolidate duplicates. USE \`delete_memory\` to remove outdated entries.
|
|
427
|
-
- **Context Limit**: You only see the **last 10 messages** of the chat to save tokens. You MUST rely on your long-term memory to maintain context. If you forget something, \`recall\` it.
|
|
428
|
-
- **Store rich values**: Don't split related facts. One entry per person with all details.
|
|
429
|
-
- **Recall efficiently**: For "everything you know about me", use \`query: null, maxResults: 50\`.
|
|
430
|
-
|
|
431
|
-
### Images
|
|
432
|
-
Generate images from text descriptions via \`create_image\`. Only for visual/artistic content — NOT for equations, code, charts.
|
|
433
|
-
|
|
434
|
-
### QR Codes
|
|
435
|
-
Generate QR codes from any text via \`create_qr_code\`. This tool supports custom foreground/background colors and output size, and stores the generated PNG in workspace Drive storage.
|
|
436
|
-
Only call \`create_qr_code\` when the current user message contains the exact text or URL to encode, or clearly refers to one immediately preceding value. If the user only says "create QR", "make a QR code", or similar without a value, ask for the text or URL. Never concatenate unrelated earlier chat text into the QR payload.
|
|
437
|
-
|
|
438
|
-
### File Conversion (MarkItDown)
|
|
439
|
-
- Use \`convert_file_to_markdown\` when the user asks to read/analyze attached binary documents such as Excel, Word, PowerPoint, PDF, etc.
|
|
440
|
-
- If the file is already attached in the current chat, prefer passing \`fileName\` (or omit arguments to convert the latest attachment).
|
|
441
|
-
- Use this tool only when file conversion is actually needed for the user's request.
|
|
442
|
-
- Do NOT use MarkItDown for YouTube links. Google/Gemini models receive public or unlisted YouTube URLs as native video input and can summarize or answer from that video directly. If video access fails, explain the model could not access the video.
|
|
443
|
-
|
|
444
|
-
### Self-Configuration
|
|
445
|
-
Update YOUR personality via \`update_my_settings\`. The \`name\` field is YOUR name (the assistant). If the user says "call me X", use \`remember\` (and \`update_user_name\` if they want their account display name changed). Proactively use \`update_my_settings\` when users describe assistant behavior preferences ("be more casual", "keep it short") or provide identity/config documents.
|
|
446
|
-
|
|
447
|
-
### Appearance
|
|
448
|
-
Use \`set_theme\` to switch the UI between dark mode, light mode, or system default. Use \`set_immersive_mode\` to enter or exit immersive fullscreen mode for the chat. Act immediately when the user asks — no confirmation needed.
|
|
449
|
-
|
|
450
|
-
### Web Search (\`google_search\`)
|
|
451
|
-
\`google_search\` lets you search the web for current, real-time information. Use it whenever the user asks about:
|
|
452
|
-
- Current events, news, weather, sports scores
|
|
453
|
-
- Product prices, availability, business hours
|
|
454
|
-
- Facts that may have changed since your training data
|
|
455
|
-
- Any question where up-to-date information would improve your answer
|
|
456
|
-
|
|
457
|
-
**IMPORTANT**: \`google_search\` is always available in Mira mode. Call it directly whenever web grounding is needed. You may include it in \`select_tools\` for planning clarity.
|
|
458
|
-
|
|
459
|
-
**Tool name rule**: Use the tool name \`google_search\` for web lookup. Do NOT call a generic \`search\` tool name in assistant text or planning.
|
|
460
|
-
|
|
461
|
-
**Usage**: If the user asks for latest/current information (news, pricing, weather, trending updates), invoke \`google_search\` before answering.
|
|
462
|
-
|
|
463
|
-
### User
|
|
464
|
-
Use \`update_user_name\` to update the user's display name or full name when they ask you to change how they are addressed. You MUST provide at least one field (\`displayName\` or \`fullName\`).
|
|
465
|
-
|
|
466
|
-
### Workspace
|
|
467
|
-
Use \`list_workspace_members\` to see who is in the current workspace context and to find user IDs for task assignment. If the user names a different workspace, resolve it with \`list_accessible_workspaces\` and \`set_workspace_context\` first.
|
|
468
|
-
|
|
469
307
|
## Boundaries
|
|
470
308
|
|
|
471
309
|
- You can write and display code, but you cannot execute it.
|
|
@@ -476,7 +314,7 @@ Use \`list_workspace_members\` to see who is in the current workspace context an
|
|
|
476
314
|
|
|
477
315
|
## FINAL REMINDER — Cache Tools, Re-select Only When Needed
|
|
478
316
|
|
|
479
|
-
Per user message: (1) stream brief text first when helpful, (2)
|
|
317
|
+
Per user message: (1) stream brief text first when helpful, (2) discover needed capabilities with \`search_tools\` or activate known names with \`select_tools\`, (3) use those tools as needed (reuse the cache — no need to call \`select_tools\` before each tool call), (4) call \`select_tools\` again only to add/disable tools, (5) summarize results in natural language.
|
|
480
318
|
`;
|
|
481
319
|
}
|
|
482
320
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { isGoogleSearchToolName } from '../tools/google-search-events';
|
|
2
|
+
|
|
3
|
+
type Call = {
|
|
4
|
+
toolName?: string;
|
|
5
|
+
toolCallId?: string;
|
|
6
|
+
input?: unknown;
|
|
7
|
+
args?: unknown;
|
|
8
|
+
output?: unknown;
|
|
9
|
+
};
|
|
10
|
+
type Step = { toolCalls?: Call[]; toolResults?: Call[] };
|
|
11
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
12
|
+
!!value && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
|
|
14
|
+
function stableValue(value: unknown): unknown {
|
|
15
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
16
|
+
if (!isObject(value)) return value;
|
|
17
|
+
return Object.fromEntries(
|
|
18
|
+
Object.keys(value)
|
|
19
|
+
.sort()
|
|
20
|
+
.map((key) => [key, stableValue(value[key])])
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isReadOrUi(name: string) {
|
|
25
|
+
return (
|
|
26
|
+
isGoogleSearchToolName(name) ||
|
|
27
|
+
/^(get_|list_|search_|recall$|check_|preview_)/.test(name) ||
|
|
28
|
+
[
|
|
29
|
+
'select_tools',
|
|
30
|
+
'show_workspace_artifact',
|
|
31
|
+
'manage_workspace',
|
|
32
|
+
'set_sidebar',
|
|
33
|
+
'set_theme',
|
|
34
|
+
'set_immersive_mode',
|
|
35
|
+
'run_parallel_checks',
|
|
36
|
+
'no_action_needed',
|
|
37
|
+
].includes(name)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Stop repeated discovery/read/UI cycles, while writes invalidate cached reads. */
|
|
42
|
+
export function getMiraToolLoopReason(steps: unknown[]): string | null {
|
|
43
|
+
const successes = new Map<string, number>();
|
|
44
|
+
const failures = new Map<string, number>();
|
|
45
|
+
for (const value of steps) {
|
|
46
|
+
if (!isObject(value)) continue;
|
|
47
|
+
const step = value as Step;
|
|
48
|
+
for (const result of step.toolResults ?? []) {
|
|
49
|
+
const name = result.toolName;
|
|
50
|
+
if (!name) continue;
|
|
51
|
+
const call = step.toolCalls?.find((candidate) =>
|
|
52
|
+
result.toolCallId
|
|
53
|
+
? candidate.toolCallId === result.toolCallId
|
|
54
|
+
: candidate.toolName === name
|
|
55
|
+
);
|
|
56
|
+
const input = result.input ?? call?.input ?? call?.args ?? {};
|
|
57
|
+
const signature = `${name}:${JSON.stringify(stableValue(input))}`;
|
|
58
|
+
const output = result.output;
|
|
59
|
+
if (isObject(output) && output.reusedResult === true)
|
|
60
|
+
return `Repeated mutation prevented: ${name}`;
|
|
61
|
+
const failed =
|
|
62
|
+
isObject(output) &&
|
|
63
|
+
(output.error ||
|
|
64
|
+
output.ok === false ||
|
|
65
|
+
output.success === false ||
|
|
66
|
+
output.partialFailure === true);
|
|
67
|
+
if (failed) {
|
|
68
|
+
const count = (failures.get(signature) ?? 0) + 1;
|
|
69
|
+
failures.set(signature, count);
|
|
70
|
+
if (count >= 3) return `Repeated failure: ${name}`;
|
|
71
|
+
} else if (isReadOrUi(name)) {
|
|
72
|
+
failures.delete(signature);
|
|
73
|
+
const count = (successes.get(signature) ?? 0) + 1;
|
|
74
|
+
successes.set(signature, count);
|
|
75
|
+
if (count >= 2) return `Repeated completed action: ${name}`;
|
|
76
|
+
} else {
|
|
77
|
+
// A successful product mutation can make previous reads stale.
|
|
78
|
+
successes.clear();
|
|
79
|
+
failures.clear();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { SupabaseClient } from '@tuturuuu/supabase';
|
|
2
|
+
import { AI_REQUEST_MAX_OUTPUT_TOKENS } from './constants';
|
|
2
3
|
import { resolveGatewayModelId } from './model-mapping';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Query the gateway model's output price per token from the database.
|
|
6
7
|
* Returns the flat output_price_per_token. For tiered pricing, returns
|
|
7
|
-
* the
|
|
8
|
+
* the highest valid tier cost as a conservative upper bound.
|
|
8
9
|
*/
|
|
9
10
|
async function getOutputPricePerToken(
|
|
10
11
|
sbAdmin: SupabaseClient,
|
|
@@ -23,16 +24,21 @@ async function getOutputPricePerToken(
|
|
|
23
24
|
|
|
24
25
|
if (error || !data) return null;
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
+
// Without input-tier context, budget against the most expensive output tier.
|
|
27
28
|
if (
|
|
28
29
|
data.output_tiers &&
|
|
29
30
|
Array.isArray(data.output_tiers) &&
|
|
30
31
|
data.output_tiers.length > 0
|
|
31
32
|
) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
return
|
|
35
|
-
|
|
33
|
+
const costs = data.output_tiers.map((tier) => {
|
|
34
|
+
const cost = (tier as { cost?: unknown } | null)?.cost;
|
|
35
|
+
return typeof cost === 'number' ||
|
|
36
|
+
(typeof cost === 'string' && cost.trim() !== '')
|
|
37
|
+
? Number(cost)
|
|
38
|
+
: NaN;
|
|
39
|
+
});
|
|
40
|
+
if (costs.some((cost) => !Number.isFinite(cost) || cost <= 0)) return null;
|
|
41
|
+
return Math.max(...costs);
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
return data.output_price_per_token ?? null;
|
|
@@ -55,12 +61,27 @@ export async function capMaxOutputTokensByCredits(
|
|
|
55
61
|
remainingCredits: number,
|
|
56
62
|
markupMultiplier = 1.0
|
|
57
63
|
): Promise<number | null> {
|
|
58
|
-
if (
|
|
64
|
+
if (
|
|
65
|
+
!Number.isFinite(remainingCredits) ||
|
|
66
|
+
remainingCredits <= 0 ||
|
|
67
|
+
!Number.isFinite(markupMultiplier) ||
|
|
68
|
+
markupMultiplier < 1
|
|
69
|
+
)
|
|
70
|
+
return null;
|
|
71
|
+
if (
|
|
72
|
+
maxOutputTokens !== null &&
|
|
73
|
+
(!Number.isSafeInteger(maxOutputTokens) || maxOutputTokens <= 0)
|
|
74
|
+
)
|
|
75
|
+
return null;
|
|
59
76
|
|
|
60
77
|
const outputPricePerToken = await getOutputPricePerToken(sbAdmin, modelId);
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
if (
|
|
79
|
+
!outputPricePerToken ||
|
|
80
|
+
!Number.isFinite(outputPricePerToken) ||
|
|
81
|
+
outputPricePerToken <= 0
|
|
82
|
+
) {
|
|
83
|
+
// Missing or invalid prices must not authorize a provider expense.
|
|
84
|
+
return null;
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
// credits = (tokens * pricePerToken / 0.0001) * markup
|
|
@@ -69,9 +90,12 @@ export async function capMaxOutputTokensByCredits(
|
|
|
69
90
|
(remainingCredits * 0.0001) / markupMultiplier / outputPricePerToken
|
|
70
91
|
);
|
|
71
92
|
|
|
72
|
-
if (affordableTokens < 1) return null;
|
|
73
|
-
|
|
74
|
-
|
|
93
|
+
if (!Number.isFinite(affordableTokens) || affordableTokens < 1) return null;
|
|
94
|
+
return Math.min(
|
|
95
|
+
maxOutputTokens ?? AI_REQUEST_MAX_OUTPUT_TOKENS,
|
|
96
|
+
affordableTokens,
|
|
97
|
+
AI_REQUEST_MAX_OUTPUT_TOKENS
|
|
98
|
+
);
|
|
75
99
|
}
|
|
76
100
|
|
|
77
101
|
/**
|
|
@@ -83,8 +107,17 @@ export function computeAffordableTokens(
|
|
|
83
107
|
outputPricePerToken: number,
|
|
84
108
|
markupMultiplier = 1.0
|
|
85
109
|
): number {
|
|
86
|
-
if (
|
|
87
|
-
|
|
110
|
+
if (
|
|
111
|
+
![remainingCredits, outputPricePerToken, markupMultiplier].every(
|
|
112
|
+
Number.isFinite
|
|
113
|
+
) ||
|
|
114
|
+
remainingCredits <= 0 ||
|
|
115
|
+
outputPricePerToken <= 0 ||
|
|
116
|
+
markupMultiplier < 1
|
|
117
|
+
)
|
|
118
|
+
return 0;
|
|
119
|
+
const affordableTokens = Math.floor(
|
|
88
120
|
(remainingCredits * 0.0001) / markupMultiplier / outputPricePerToken
|
|
89
121
|
);
|
|
122
|
+
return Number.isFinite(affordableTokens) ? affordableTokens : 0;
|
|
90
123
|
}
|
|
@@ -73,7 +73,18 @@ export async function checkAiCredits(
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const row = Array.isArray(data) ? data[0] : data;
|
|
76
|
-
|
|
76
|
+
const rawBalance: unknown = row?.remaining_credits;
|
|
77
|
+
const balance =
|
|
78
|
+
typeof rawBalance === 'number' ||
|
|
79
|
+
(typeof rawBalance === 'string' && rawBalance.trim() !== '')
|
|
80
|
+
? Number(rawBalance)
|
|
81
|
+
: NaN;
|
|
82
|
+
if (
|
|
83
|
+
!row ||
|
|
84
|
+
typeof row.allowed !== 'boolean' ||
|
|
85
|
+
!Number.isFinite(balance) ||
|
|
86
|
+
balance < 0
|
|
87
|
+
) {
|
|
77
88
|
return {
|
|
78
89
|
allowed: false,
|
|
79
90
|
remainingCredits: 0,
|
|
@@ -85,8 +96,8 @@ export async function checkAiCredits(
|
|
|
85
96
|
}
|
|
86
97
|
|
|
87
98
|
return {
|
|
88
|
-
allowed: row.allowed
|
|
89
|
-
remainingCredits:
|
|
99
|
+
allowed: row.allowed === true && balance > 0,
|
|
100
|
+
remainingCredits: balance,
|
|
90
101
|
tier: row.tier ?? 'FREE',
|
|
91
102
|
maxOutputTokens: row.max_output_tokens ?? null,
|
|
92
103
|
errorCode: (row.error_code as CreditErrorCode) ?? null,
|
package/src/credits/constants.ts
CHANGED
|
@@ -29,3 +29,6 @@ export const CREDIT_ERROR_CODES = {
|
|
|
29
29
|
|
|
30
30
|
export type CreditErrorCode =
|
|
31
31
|
(typeof CREDIT_ERROR_CODES)[keyof typeof CREDIT_ERROR_CODES];
|
|
32
|
+
|
|
33
|
+
/** Technical ceiling, separate from the purchased credit allowance. */
|
|
34
|
+
export const AI_REQUEST_MAX_OUTPUT_TOKENS = 65_536;
|
package/src/credits/index.ts
CHANGED
package/src/generate/route.ts
CHANGED
|
@@ -232,10 +232,13 @@ export function createPOST(
|
|
|
232
232
|
creditCheck.maxOutputTokens,
|
|
233
233
|
creditCheck.remainingCredits
|
|
234
234
|
);
|
|
235
|
-
if (cappedMaxOutput === null
|
|
235
|
+
if (cappedMaxOutput === null) {
|
|
236
236
|
return NextResponse.json(
|
|
237
|
-
{
|
|
238
|
-
|
|
237
|
+
{
|
|
238
|
+
error: 'AI request budget could not be verified',
|
|
239
|
+
code: 'CREDIT_CHECK_FAILED',
|
|
240
|
+
},
|
|
241
|
+
{ status: 503 }
|
|
239
242
|
);
|
|
240
243
|
}
|
|
241
244
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Keep database reservations at one per ten-second upload window. */
|
|
2
|
+
export const MEET_AUDIO_BATCH_INTERVAL_MS = 10_000;
|
|
3
|
+
export const MEET_AUDIO_BATCH_MAX_CLIPS = 48;
|
|
4
|
+
// Base64 plus prompts stays below the provider's 20 MB inline-audio request bound.
|
|
5
|
+
export const MEET_AUDIO_BATCH_MAX_BYTES = 12_000_000;
|
|
6
|
+
export const MEET_AUDIO_REQUEST_MAX_BYTES = 12_100_000;
|
|
7
|
+
export const MEET_AUDIO_PENDING_MAX_BYTES = 48_000_000;
|
|
8
|
+
export const MEET_AUDIO_SESSION_MAX_BATCHES = 1080;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { MEET_AUDIO_BATCH_MAX_CLIPS } from './audio-contract';
|
|
3
|
+
|
|
4
|
+
export const audioTranscriptsSchema = z.object({
|
|
5
|
+
transcripts: z.array(
|
|
6
|
+
z.object({
|
|
7
|
+
index: z
|
|
8
|
+
.number()
|
|
9
|
+
.int()
|
|
10
|
+
.min(0)
|
|
11
|
+
.max(MEET_AUDIO_BATCH_MAX_CLIPS - 1),
|
|
12
|
+
text: z.string(),
|
|
13
|
+
})
|
|
14
|
+
),
|
|
15
|
+
});
|
|
16
|
+
export function orderedAudioTranscripts(output: unknown, count: number) {
|
|
17
|
+
const { transcripts } = audioTranscriptsSchema.parse(output);
|
|
18
|
+
if (
|
|
19
|
+
transcripts.length !== count ||
|
|
20
|
+
new Set(transcripts.map((part) => part.index)).size !== count ||
|
|
21
|
+
transcripts.some((part) => part.index >= count)
|
|
22
|
+
)
|
|
23
|
+
throw new Error('Incomplete audio source transcription');
|
|
24
|
+
return transcripts.sort((a, b) => a.index - b.index).map((part) => part.text);
|
|
25
|
+
}
|
|
@@ -46,6 +46,7 @@ export function formatMeetSourceAnswer(
|
|
|
46
46
|
text: string,
|
|
47
47
|
sources: MeetCitationSource[]
|
|
48
48
|
) {
|
|
49
|
+
if (!text.trim()) return '';
|
|
49
50
|
const selected = selectMeetSources(sources);
|
|
50
51
|
const footer = [
|
|
51
52
|
...new Map(selected.map((source) => [source.url, source])).values(),
|
package/src/meetings/chat.ts
CHANGED
|
@@ -17,6 +17,9 @@ export async function answerMeetChat(
|
|
|
17
17
|
model: MeetChatModel,
|
|
18
18
|
context: MeetAssistantContext,
|
|
19
19
|
options: {
|
|
20
|
+
provider?: { apiKey?: string; fetch?: typeof fetch };
|
|
21
|
+
scenarioInstructions?: string;
|
|
22
|
+
allowSearch?: boolean;
|
|
20
23
|
workspaceTools?: ToolSet;
|
|
21
24
|
messages?: ModelMessage[];
|
|
22
25
|
audience?: 'private';
|
|
@@ -27,7 +30,9 @@ export async function answerMeetChat(
|
|
|
27
30
|
Effect.either(
|
|
28
31
|
Effect.tryPromise({
|
|
29
32
|
try: async () => {
|
|
30
|
-
const apiKey =
|
|
33
|
+
const apiKey =
|
|
34
|
+
options.provider?.apiKey ??
|
|
35
|
+
process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
|
31
36
|
if (!apiKey) throw new Error('Meeting AI is not configured');
|
|
32
37
|
const initialMessages: ModelMessage[] = options.messages ?? [
|
|
33
38
|
{
|
|
@@ -55,12 +60,16 @@ export async function answerMeetChat(
|
|
|
55
60
|
([name]) =>
|
|
56
61
|
(options.audience !== 'private' ||
|
|
57
62
|
name !== 'get_meeting_context') &&
|
|
58
|
-
(!options.messages
|
|
63
|
+
((!options.messages &&
|
|
64
|
+
!options.scenarioInstructions &&
|
|
65
|
+
options.allowSearch !== false) ||
|
|
66
|
+
name !== 'google_search')
|
|
59
67
|
)
|
|
60
68
|
);
|
|
61
|
-
const languageModel = createGoogleGenerativeAI({
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
const languageModel = createGoogleGenerativeAI({
|
|
70
|
+
apiKey,
|
|
71
|
+
fetch: options.provider?.fetch,
|
|
72
|
+
})(model.providerModelId);
|
|
64
73
|
const signal = AbortSignal.timeout(45000);
|
|
65
74
|
const stepBudget = Math.max(1, Math.floor(maxOutputTokens / 4));
|
|
66
75
|
const search = publicMeetSearch(
|
|
@@ -76,7 +85,12 @@ export async function answerMeetChat(
|
|
|
76
85
|
],
|
|
77
86
|
options.beforeSearch
|
|
78
87
|
);
|
|
79
|
-
if (
|
|
88
|
+
if (
|
|
89
|
+
!options.messages &&
|
|
90
|
+
!options.scenarioInstructions &&
|
|
91
|
+
options.allowSearch !== false
|
|
92
|
+
)
|
|
93
|
+
publicTools.google_search = search.tool;
|
|
80
94
|
const hasWorkspaceTools =
|
|
81
95
|
Object.keys(options.workspaceTools ?? {}).length > 0;
|
|
82
96
|
const tools: ToolSet = {
|
|
@@ -113,6 +127,12 @@ export async function answerMeetChat(
|
|
|
113
127
|
maxRetries: 0,
|
|
114
128
|
abortSignal: signal,
|
|
115
129
|
system: [
|
|
130
|
+
...(options.scenarioInstructions
|
|
131
|
+
? [
|
|
132
|
+
options.scenarioInstructions,
|
|
133
|
+
'External web search is disabled for private simulation material.',
|
|
134
|
+
]
|
|
135
|
+
: []),
|
|
116
136
|
'You are Mira, Tuturuuu\u2019s meeting assistant. Answer the explicit question field, using recentChat as context even if it contains newer questions. You can answer general knowledge questions; you are not restricted to facts mentioned in chat. Respond in the question\u2019s language using concise Markdown. Use get_current_time for today, dates, weekdays and current time; ',
|
|
117
137
|
options.audience === 'private'
|
|
118
138
|
? 'live room details are unavailable in personal context;'
|
|
@@ -159,7 +179,10 @@ export async function answerMeetChat(
|
|
|
159
179
|
messages: [...initialMessages, ...result.responseMessages],
|
|
160
180
|
text: search.unavailable()
|
|
161
181
|
? 'Google did not return verified sources for this question. Please try again or ask a more specific public question.'
|
|
162
|
-
: formatMeetSourceAnswer(
|
|
182
|
+
: formatMeetSourceAnswer(
|
|
183
|
+
result.text.trim() || (!privateResult ? search.answer() : ''),
|
|
184
|
+
sources
|
|
185
|
+
),
|
|
163
186
|
...measureMeetGeneration(
|
|
164
187
|
[
|
|
165
188
|
...result.steps.map((step) => ({
|