@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.
- 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/google-search-usage.ts +62 -0
- package/src/chat/google/mira-step-preparation.ts +37 -16
- 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 -71
- 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 +8 -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 +4 -1
- package/src/meetings/gemini.ts +72 -20
- 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
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyWorkspaceCalendarSchedule,
|
|
3
|
+
createWorkspaceCalendarEvent,
|
|
4
|
+
deleteWorkspaceCalendarEvent,
|
|
5
|
+
getGoogleCalendarAuthUrl,
|
|
6
|
+
getWorkspaceCalendarSyncStatus,
|
|
7
|
+
listCalendarConnections,
|
|
8
|
+
listWorkspaceSchedulableTasks,
|
|
9
|
+
previewWorkspaceCalendarSchedule,
|
|
10
|
+
syncWorkspaceCalendar,
|
|
11
|
+
updateWorkspaceCalendarEvent,
|
|
12
|
+
type WorkspaceCalendarEventCreatePayload,
|
|
13
|
+
type WorkspaceCalendarEventUpdatePayload,
|
|
14
|
+
} from '@tuturuuu/internal-api/calendar';
|
|
15
|
+
import { withForwardedInternalApiAuth } from '@tuturuuu/internal-api/client';
|
|
16
|
+
import { resolveInternalAppUrl } from '@tuturuuu/utils/app-url';
|
|
17
|
+
import { Effect } from '@tuturuuu/utils/effect';
|
|
18
|
+
import type { MiraToolContext } from '../mira-tool-types';
|
|
19
|
+
import { getWorkspaceContextWorkspaceId } from '../workspace-context';
|
|
20
|
+
|
|
21
|
+
type SchedulePreview = {
|
|
22
|
+
success: boolean;
|
|
23
|
+
preview?: { summary?: unknown; warnings?: string[]; events?: unknown[] };
|
|
24
|
+
tasks?: unknown;
|
|
25
|
+
habits?: unknown;
|
|
26
|
+
lockedEvents?: unknown[];
|
|
27
|
+
};
|
|
28
|
+
const previews = new WeakMap<object, Set<string>>();
|
|
29
|
+
|
|
30
|
+
export async function executeCalendarAutomation(
|
|
31
|
+
name: string,
|
|
32
|
+
args: Record<string, unknown>,
|
|
33
|
+
ctx: MiraToolContext
|
|
34
|
+
) {
|
|
35
|
+
if (!ctx.requestHeaders)
|
|
36
|
+
return {
|
|
37
|
+
success: false,
|
|
38
|
+
error:
|
|
39
|
+
'Calendar automation is unavailable in this session. Open Calendar settings to manage connections.',
|
|
40
|
+
};
|
|
41
|
+
const wsId = getWorkspaceContextWorkspaceId(ctx);
|
|
42
|
+
const state = ctx.executionState ?? ctx;
|
|
43
|
+
const baseUrl = resolveInternalAppUrl({
|
|
44
|
+
appName: 'calendar',
|
|
45
|
+
candidates: [
|
|
46
|
+
process.env.CALENDAR_APP_URL,
|
|
47
|
+
process.env.NEXT_PUBLIC_CALENDAR_APP_URL,
|
|
48
|
+
],
|
|
49
|
+
fallback:
|
|
50
|
+
process.env.NODE_ENV === 'production'
|
|
51
|
+
? 'https://calendar.tuturuuu.com'
|
|
52
|
+
: 'https://calendar.tuturuuu.localhost',
|
|
53
|
+
});
|
|
54
|
+
const target = new URL(baseUrl);
|
|
55
|
+
const localDevelopment =
|
|
56
|
+
process.env.NODE_ENV !== 'production' &&
|
|
57
|
+
(['localhost', '127.0.0.1', '[::1]'].includes(target.hostname) ||
|
|
58
|
+
target.hostname.endsWith('.localhost'));
|
|
59
|
+
if (target.protocol !== 'https:' && !localDevelopment)
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
error: 'Calendar requires a secure HTTPS connection.',
|
|
63
|
+
};
|
|
64
|
+
const options = withForwardedInternalApiAuth(ctx.requestHeaders, { baseUrl });
|
|
65
|
+
const windowDays = typeof args.windowDays === 'number' ? args.windowDays : 7;
|
|
66
|
+
const previewKey = `${wsId}:${windowDays}:${ctx.timezone ?? ''}`;
|
|
67
|
+
return Effect.runPromise(
|
|
68
|
+
Effect.tryPromise({
|
|
69
|
+
try: async () => {
|
|
70
|
+
switch (name) {
|
|
71
|
+
case 'create_event': {
|
|
72
|
+
const event = await createWorkspaceCalendarEvent(
|
|
73
|
+
wsId,
|
|
74
|
+
{
|
|
75
|
+
title: args.title as string,
|
|
76
|
+
start_at: new Date(args.startAt as string).toISOString(),
|
|
77
|
+
end_at: new Date(args.endAt as string).toISOString(),
|
|
78
|
+
description: args.description as string | null | undefined,
|
|
79
|
+
location: args.location as string | null | undefined,
|
|
80
|
+
color: args.color as string | undefined,
|
|
81
|
+
locked: args.locked as boolean | undefined,
|
|
82
|
+
source:
|
|
83
|
+
args.source as WorkspaceCalendarEventCreatePayload['source'],
|
|
84
|
+
},
|
|
85
|
+
options
|
|
86
|
+
);
|
|
87
|
+
return { success: true, event, workspaceId: wsId };
|
|
88
|
+
}
|
|
89
|
+
case 'update_event': {
|
|
90
|
+
const payload: WorkspaceCalendarEventUpdatePayload = {};
|
|
91
|
+
for (const [input, output] of [
|
|
92
|
+
['title', 'title'],
|
|
93
|
+
['startAt', 'start_at'],
|
|
94
|
+
['endAt', 'end_at'],
|
|
95
|
+
['description', 'description'],
|
|
96
|
+
['location', 'location'],
|
|
97
|
+
['color', 'color'],
|
|
98
|
+
['locked', 'locked'],
|
|
99
|
+
['source', 'source'],
|
|
100
|
+
] as const) {
|
|
101
|
+
if (args[input] !== undefined)
|
|
102
|
+
Object.assign(payload, {
|
|
103
|
+
[output]:
|
|
104
|
+
input === 'startAt' || input === 'endAt'
|
|
105
|
+
? new Date(args[input] as string).toISOString()
|
|
106
|
+
: args[input],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const event = await updateWorkspaceCalendarEvent(
|
|
110
|
+
wsId,
|
|
111
|
+
args.eventId as string,
|
|
112
|
+
payload,
|
|
113
|
+
options
|
|
114
|
+
);
|
|
115
|
+
return { success: true, event, workspaceId: wsId };
|
|
116
|
+
}
|
|
117
|
+
case 'delete_event':
|
|
118
|
+
return {
|
|
119
|
+
success: true,
|
|
120
|
+
...(await deleteWorkspaceCalendarEvent(
|
|
121
|
+
wsId,
|
|
122
|
+
args.eventId as string,
|
|
123
|
+
options
|
|
124
|
+
)),
|
|
125
|
+
};
|
|
126
|
+
case 'get_calendar_connections': {
|
|
127
|
+
const [connections, sync] = await Promise.all([
|
|
128
|
+
listCalendarConnections(wsId, options),
|
|
129
|
+
getWorkspaceCalendarSyncStatus(wsId, options),
|
|
130
|
+
]);
|
|
131
|
+
return { connections, sync };
|
|
132
|
+
}
|
|
133
|
+
case 'connect_google_calendar':
|
|
134
|
+
return getGoogleCalendarAuthUrl(wsId, options);
|
|
135
|
+
case 'sync_calendar':
|
|
136
|
+
return syncWorkspaceCalendar(wsId, options, {
|
|
137
|
+
direction: args.direction as 'inbound' | 'outbound' | 'both',
|
|
138
|
+
});
|
|
139
|
+
case 'set_event_locked':
|
|
140
|
+
return updateWorkspaceCalendarEvent(
|
|
141
|
+
wsId,
|
|
142
|
+
args.eventId as string,
|
|
143
|
+
{ locked: args.locked as boolean },
|
|
144
|
+
options
|
|
145
|
+
);
|
|
146
|
+
case 'get_schedulable_tasks':
|
|
147
|
+
return listWorkspaceSchedulableTasks(
|
|
148
|
+
wsId,
|
|
149
|
+
{ q: args.query as string | undefined },
|
|
150
|
+
options
|
|
151
|
+
);
|
|
152
|
+
case 'preview_calendar_schedule': {
|
|
153
|
+
const preview =
|
|
154
|
+
await previewWorkspaceCalendarSchedule<SchedulePreview>(
|
|
155
|
+
wsId,
|
|
156
|
+
{ windowDays, clientTimezone: ctx.timezone },
|
|
157
|
+
options
|
|
158
|
+
);
|
|
159
|
+
if (!preview.success)
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
error: 'Scheduling preview failed. Nothing was applied.',
|
|
163
|
+
};
|
|
164
|
+
const keys = previews.get(state) ?? new Set<string>();
|
|
165
|
+
keys.add(previewKey);
|
|
166
|
+
previews.set(state, keys);
|
|
167
|
+
return {
|
|
168
|
+
success: true,
|
|
169
|
+
summary: preview.preview?.summary,
|
|
170
|
+
warnings: preview.preview?.warnings,
|
|
171
|
+
events: preview.preview?.events?.slice(0, 50),
|
|
172
|
+
totalEvents: preview.preview?.events?.length ?? 0,
|
|
173
|
+
lockedEventCount: preview.lockedEvents?.length ?? 0,
|
|
174
|
+
tasks: preview.tasks,
|
|
175
|
+
habits: preview.habits,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
case 'apply_calendar_schedule': {
|
|
179
|
+
if (!previews.get(state)?.has(previewKey))
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
error: 'Preview this workspace and scheduling window first.',
|
|
183
|
+
};
|
|
184
|
+
previews.get(state)?.delete(previewKey);
|
|
185
|
+
return applyWorkspaceCalendarSchedule(
|
|
186
|
+
wsId,
|
|
187
|
+
{
|
|
188
|
+
windowDays,
|
|
189
|
+
clientTimezone: ctx.timezone,
|
|
190
|
+
mode: 'safe-apply',
|
|
191
|
+
scope: 'impacted-only',
|
|
192
|
+
forceReschedule: false,
|
|
193
|
+
},
|
|
194
|
+
options
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
default:
|
|
198
|
+
return { success: false, error: 'Unknown calendar operation' };
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
catch: (error) =>
|
|
202
|
+
error instanceof Error ? error.message : 'Calendar operation failed',
|
|
203
|
+
}).pipe(
|
|
204
|
+
Effect.catchAll((error) => Effect.succeed({ success: false, error }))
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -4,6 +4,7 @@ import timezone from 'dayjs/plugin/timezone';
|
|
|
4
4
|
import utc from 'dayjs/plugin/utc';
|
|
5
5
|
import type { MiraToolContext } from '../mira-tools';
|
|
6
6
|
import { getWorkspaceContextWorkspaceId } from '../workspace-context';
|
|
7
|
+
import { executeCalendarAutomation } from './calendar-automation';
|
|
7
8
|
import {
|
|
8
9
|
decryptEventsForTools,
|
|
9
10
|
encryptEventFieldsForTools,
|
|
@@ -42,7 +43,9 @@ export async function executeGetUpcomingEvents(
|
|
|
42
43
|
|
|
43
44
|
const { data: events, error } = await ctx.supabase
|
|
44
45
|
.from('workspace_calendar_events')
|
|
45
|
-
.select(
|
|
46
|
+
.select(
|
|
47
|
+
'id, title, description, start_at, end_at, location, is_encrypted, locked'
|
|
48
|
+
)
|
|
46
49
|
.eq('ws_id', workspaceId)
|
|
47
50
|
.gte('start_at', rangeStart.toISOString())
|
|
48
51
|
.lte('start_at', rangeEnd.toISOString())
|
|
@@ -75,6 +78,7 @@ export async function executeGetUpcomingEvents(
|
|
|
75
78
|
start_at: string;
|
|
76
79
|
end_at: string;
|
|
77
80
|
location: string | null;
|
|
81
|
+
locked?: boolean | null;
|
|
78
82
|
}) => ({
|
|
79
83
|
id: e.id,
|
|
80
84
|
title: e.title,
|
|
@@ -82,6 +86,7 @@ export async function executeGetUpcomingEvents(
|
|
|
82
86
|
start: formatInTz(e.start_at),
|
|
83
87
|
end: formatInTz(e.end_at),
|
|
84
88
|
location: e.location,
|
|
89
|
+
locked: e.locked === true,
|
|
85
90
|
})
|
|
86
91
|
),
|
|
87
92
|
};
|
|
@@ -91,6 +96,17 @@ export async function executeCreateEvent(
|
|
|
91
96
|
args: Record<string, unknown>,
|
|
92
97
|
ctx: MiraToolContext
|
|
93
98
|
) {
|
|
99
|
+
if (ctx.requestHeaders)
|
|
100
|
+
return executeCalendarAutomation('create_event', args, ctx);
|
|
101
|
+
if (
|
|
102
|
+
args.source !== undefined ||
|
|
103
|
+
args.locked !== undefined ||
|
|
104
|
+
args.color !== undefined
|
|
105
|
+
)
|
|
106
|
+
return {
|
|
107
|
+
error:
|
|
108
|
+
'Provider destinations, colors and locks require an authenticated Chat or Calendar session. No event changes were saved.',
|
|
109
|
+
};
|
|
94
110
|
const workspaceId = getWorkspaceContextWorkspaceId(ctx);
|
|
95
111
|
const title = args.title as string;
|
|
96
112
|
const description = (args.description as string) ?? '';
|
|
@@ -161,6 +177,17 @@ export async function executeUpdateEvent(
|
|
|
161
177
|
args: Record<string, unknown>,
|
|
162
178
|
ctx: MiraToolContext
|
|
163
179
|
) {
|
|
180
|
+
if (ctx.requestHeaders)
|
|
181
|
+
return executeCalendarAutomation('update_event', args, ctx);
|
|
182
|
+
if (
|
|
183
|
+
args.source !== undefined ||
|
|
184
|
+
args.locked !== undefined ||
|
|
185
|
+
args.color !== undefined
|
|
186
|
+
)
|
|
187
|
+
return {
|
|
188
|
+
error:
|
|
189
|
+
'Provider destinations, colors and locks require an authenticated Chat or Calendar session. No event changes were saved.',
|
|
190
|
+
};
|
|
164
191
|
const workspaceId = getWorkspaceContextWorkspaceId(ctx);
|
|
165
192
|
const eventId = args.eventId as string;
|
|
166
193
|
const title = args.title as string | undefined;
|
|
@@ -257,6 +284,8 @@ export async function executeDeleteEvent(
|
|
|
257
284
|
args: Record<string, unknown>,
|
|
258
285
|
ctx: MiraToolContext
|
|
259
286
|
) {
|
|
287
|
+
if (ctx.requestHeaders)
|
|
288
|
+
return executeCalendarAutomation('delete_event', args, ctx);
|
|
260
289
|
const workspaceId = getWorkspaceContextWorkspaceId(ctx);
|
|
261
290
|
const eventId = args.eventId as string;
|
|
262
291
|
|
|
@@ -17,22 +17,22 @@ type CalendarEventRow = {
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Retrieve the workspace encryption key (read-only).
|
|
20
|
-
*
|
|
20
|
+
* Strict mode throws on key/configuration failures; null means no workspace key.
|
|
21
21
|
*/
|
|
22
22
|
export async function getWorkspaceKeyForTools(
|
|
23
|
-
wsId: string
|
|
23
|
+
wsId: string,
|
|
24
|
+
strict = false
|
|
24
25
|
): Promise<Buffer | null> {
|
|
25
26
|
try {
|
|
26
27
|
const { isEncryptionEnabled, getMasterKey, decryptWorkspaceKey } =
|
|
27
28
|
await import('@tuturuuu/utils/encryption');
|
|
28
29
|
|
|
29
|
-
if (!isEncryptionEnabled()) return null;
|
|
30
|
+
if (!strict && !isEncryptionEnabled()) return null;
|
|
30
31
|
|
|
31
32
|
const { createAdminClient } = await import(
|
|
32
33
|
'@tuturuuu/supabase/next/server'
|
|
33
34
|
);
|
|
34
35
|
const sbAdmin = await createAdminClient();
|
|
35
|
-
const masterKey = getMasterKey();
|
|
36
36
|
|
|
37
37
|
const { data, error } = await sbAdmin
|
|
38
38
|
.from('workspace_encryption_keys')
|
|
@@ -40,13 +40,15 @@ export async function getWorkspaceKeyForTools(
|
|
|
40
40
|
.eq('ws_id', wsId)
|
|
41
41
|
.maybeSingle();
|
|
42
42
|
|
|
43
|
-
if (error
|
|
43
|
+
if (error) throw error;
|
|
44
|
+
if (!data) return null;
|
|
44
45
|
|
|
45
46
|
return await decryptWorkspaceKey(
|
|
46
47
|
(data as { encrypted_key: string }).encrypted_key,
|
|
47
|
-
|
|
48
|
+
getMasterKey()
|
|
48
49
|
);
|
|
49
|
-
} catch {
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (strict) throw error;
|
|
50
52
|
return null;
|
|
51
53
|
}
|
|
52
54
|
}
|
|
@@ -74,14 +76,15 @@ export async function decryptEventsForTools<T extends CalendarEventRow>(
|
|
|
74
76
|
*/
|
|
75
77
|
export async function encryptEventFieldsForTools(
|
|
76
78
|
fields: { title: string; description: string; location: string | null },
|
|
77
|
-
wsId: string
|
|
79
|
+
wsId: string,
|
|
80
|
+
strict = false
|
|
78
81
|
): Promise<{
|
|
79
82
|
title: string;
|
|
80
83
|
description: string;
|
|
81
84
|
location: string | null;
|
|
82
85
|
is_encrypted: boolean;
|
|
83
86
|
}> {
|
|
84
|
-
const key = await getWorkspaceKeyForTools(wsId);
|
|
87
|
+
const key = await getWorkspaceKeyForTools(wsId, strict);
|
|
85
88
|
if (!key) {
|
|
86
89
|
return { ...fields, is_encrypted: false };
|
|
87
90
|
}
|
|
@@ -2,10 +2,11 @@ import { google } from '@ai-sdk/google';
|
|
|
2
2
|
import { generateText, stepCountIs } from 'ai';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { withAiMemory } from '../../memory';
|
|
5
|
+
import { isGoogleSearchToolName } from '../google-search-events';
|
|
5
6
|
import { createGoogleSearchToolSet } from '../google-search-tool';
|
|
6
7
|
import type { MiraToolContext } from '../mira-tools';
|
|
7
8
|
|
|
8
|
-
const SEARCH_WRAPPER_MODEL = 'gemini-3.
|
|
9
|
+
const SEARCH_WRAPPER_MODEL = 'gemini-3.5-flash-lite';
|
|
9
10
|
|
|
10
11
|
type SearchSource = {
|
|
11
12
|
sourceId?: string;
|
|
@@ -34,16 +35,35 @@ function hasGoogleSearchCallInSteps(steps: unknown): boolean {
|
|
|
34
35
|
return steps.some((step) => {
|
|
35
36
|
if (!step || typeof step !== 'object') return false;
|
|
36
37
|
const typedStep = step as ToolStepLike;
|
|
37
|
-
const called = (typedStep.toolCalls ?? []).some(
|
|
38
|
-
(toolCall
|
|
38
|
+
const called = (typedStep.toolCalls ?? []).some((toolCall) =>
|
|
39
|
+
isGoogleSearchToolName(toolCall.toolName)
|
|
39
40
|
);
|
|
40
|
-
const hasResult = (typedStep.toolResults ?? []).some(
|
|
41
|
-
(toolResult
|
|
41
|
+
const hasResult = (typedStep.toolResults ?? []).some((toolResult) =>
|
|
42
|
+
isGoogleSearchToolName(toolResult.toolName)
|
|
42
43
|
);
|
|
43
44
|
return called || hasResult;
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
function hasGroundingQueries(value: unknown): boolean {
|
|
49
|
+
if (!value || typeof value !== 'object') return false;
|
|
50
|
+
const result = value as {
|
|
51
|
+
providerMetadata?: {
|
|
52
|
+
google?: { groundingMetadata?: { webSearchQueries?: unknown } };
|
|
53
|
+
};
|
|
54
|
+
steps?: unknown[];
|
|
55
|
+
};
|
|
56
|
+
const queries =
|
|
57
|
+
result.providerMetadata?.google?.groundingMetadata?.webSearchQueries;
|
|
58
|
+
return (
|
|
59
|
+
(Array.isArray(queries) &&
|
|
60
|
+
queries.some(
|
|
61
|
+
(query) => typeof query === 'string' && query.trim().length > 0
|
|
62
|
+
)) ||
|
|
63
|
+
(result.steps?.some(hasGroundingQueries) ?? false)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
function normalizeSources(value: unknown): SearchSource[] {
|
|
48
68
|
if (!Array.isArray(value)) return [];
|
|
49
69
|
|
|
@@ -88,8 +108,11 @@ async function runGoogleSearchWrapper(
|
|
|
88
108
|
}),
|
|
89
109
|
tools: createGoogleSearchToolSet(),
|
|
90
110
|
prompt,
|
|
91
|
-
stopWhen: stepCountIs(
|
|
92
|
-
|
|
111
|
+
stopWhen: stepCountIs(1),
|
|
112
|
+
maxRetries: 0,
|
|
113
|
+
maxOutputTokens: 2048,
|
|
114
|
+
abortSignal: AbortSignal.timeout(30_000),
|
|
115
|
+
toolChoice: 'auto',
|
|
93
116
|
});
|
|
94
117
|
}
|
|
95
118
|
|
|
@@ -114,7 +137,7 @@ export async function executeGoogleSearch(
|
|
|
114
137
|
(result as { steps?: unknown }).steps
|
|
115
138
|
);
|
|
116
139
|
|
|
117
|
-
if (!wasToolCalled) {
|
|
140
|
+
if (!wasToolCalled && !hasGroundingQueries(result)) {
|
|
118
141
|
result = await runGoogleSearchWrapper(query, true, ctx);
|
|
119
142
|
sources = normalizeSources((result as { sources?: unknown }).sources);
|
|
120
143
|
wasToolCalled = hasGoogleSearchCallInSteps(
|
|
@@ -122,7 +145,7 @@ export async function executeGoogleSearch(
|
|
|
122
145
|
);
|
|
123
146
|
}
|
|
124
147
|
|
|
125
|
-
if (!wasToolCalled &&
|
|
148
|
+
if (!wasToolCalled && !hasGroundingQueries(result)) {
|
|
126
149
|
return {
|
|
127
150
|
ok: false,
|
|
128
151
|
query,
|
|
@@ -130,6 +153,14 @@ export async function executeGoogleSearch(
|
|
|
130
153
|
};
|
|
131
154
|
}
|
|
132
155
|
|
|
156
|
+
if (!result.text.trim())
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
query,
|
|
160
|
+
error:
|
|
161
|
+
'Search completed without a final answer. Try a more specific query.',
|
|
162
|
+
};
|
|
163
|
+
|
|
133
164
|
return {
|
|
134
165
|
ok: true,
|
|
135
166
|
query,
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { Enums } from '@tuturuuu/types';
|
|
2
|
+
import { parseTaskDateToUTCISO } from '@tuturuuu/utils/task-date-timezone';
|
|
3
|
+
import type { MiraToolContext } from '../mira-tool-types';
|
|
4
|
+
import { getWorkspaceContextWorkspaceId } from '../workspace-context';
|
|
5
|
+
|
|
6
|
+
export async function executeCreateTask(
|
|
7
|
+
args: Record<string, unknown>,
|
|
8
|
+
ctx: MiraToolContext
|
|
9
|
+
) {
|
|
10
|
+
const { supabase } = ctx;
|
|
11
|
+
const wsId = getWorkspaceContextWorkspaceId(ctx);
|
|
12
|
+
const boardIdArg = args.boardId as string | undefined;
|
|
13
|
+
const listIdArg = args.listId as string | undefined;
|
|
14
|
+
|
|
15
|
+
// If listId is provided directly, validate it exists and use it
|
|
16
|
+
if (listIdArg) {
|
|
17
|
+
const { data: targetList, error: listError } = await supabase
|
|
18
|
+
.from('task_lists')
|
|
19
|
+
.select('id, board_id, workspace_boards!inner(ws_id)')
|
|
20
|
+
.eq('id', listIdArg)
|
|
21
|
+
.not('status', 'in', '(done,closed)')
|
|
22
|
+
.is('workspace_boards.archived_at', null)
|
|
23
|
+
.is('workspace_boards.deleted_at', null)
|
|
24
|
+
.eq('archived', false)
|
|
25
|
+
.eq('deleted', false)
|
|
26
|
+
.maybeSingle();
|
|
27
|
+
if (listError) return { created: false, error: listError.message };
|
|
28
|
+
|
|
29
|
+
if (!targetList) {
|
|
30
|
+
return {
|
|
31
|
+
created: false,
|
|
32
|
+
error: `Task list "${listIdArg}" not found or archived. Use list_task_lists to discover valid lists.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const relation = targetList.workspace_boards as unknown as
|
|
37
|
+
| { ws_id: string }
|
|
38
|
+
| { ws_id: string }[];
|
|
39
|
+
const listWsId = (Array.isArray(relation) ? relation[0] : relation)?.ws_id;
|
|
40
|
+
if (listWsId !== wsId) {
|
|
41
|
+
return {
|
|
42
|
+
created: false,
|
|
43
|
+
error: `Task list does not belong to the current workspace context. Switch workspace first with set_workspace_context.`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (boardIdArg && targetList.board_id !== boardIdArg) {
|
|
48
|
+
return {
|
|
49
|
+
created: false,
|
|
50
|
+
error: 'The list does not belong to the specified board.',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return persistTask(args, ctx, listIdArg);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Resolve board: use provided boardId or fall back to first workspace board
|
|
57
|
+
let board: { id: string } | null = null;
|
|
58
|
+
|
|
59
|
+
if (boardIdArg) {
|
|
60
|
+
const { data: targetBoard, error: boardError } = await supabase
|
|
61
|
+
.from('workspace_boards')
|
|
62
|
+
.select('id')
|
|
63
|
+
.eq('id', boardIdArg)
|
|
64
|
+
.eq('ws_id', wsId)
|
|
65
|
+
.is('deleted_at', null)
|
|
66
|
+
.is('archived_at', null)
|
|
67
|
+
.maybeSingle();
|
|
68
|
+
if (boardError) return { created: false, error: boardError.message };
|
|
69
|
+
|
|
70
|
+
if (!targetBoard) {
|
|
71
|
+
return {
|
|
72
|
+
created: false,
|
|
73
|
+
error: `Board "${boardIdArg}" not found in this workspace. Use list_boards to discover valid boards.`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
board = targetBoard;
|
|
77
|
+
} else {
|
|
78
|
+
const { data: firstBoard, error: lookupError } = await supabase
|
|
79
|
+
.from('workspace_boards')
|
|
80
|
+
.select('id')
|
|
81
|
+
.eq('ws_id', wsId)
|
|
82
|
+
.is('deleted_at', null)
|
|
83
|
+
.is('archived_at', null)
|
|
84
|
+
.order('created_at', { ascending: true })
|
|
85
|
+
.limit(1)
|
|
86
|
+
.maybeSingle();
|
|
87
|
+
if (lookupError) return { created: false, error: lookupError.message };
|
|
88
|
+
board = firstBoard;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!board) {
|
|
92
|
+
const { data: newBoard, error: boardErr } = await supabase
|
|
93
|
+
.from('workspace_boards')
|
|
94
|
+
.insert({ name: 'Tasks', ws_id: wsId })
|
|
95
|
+
.select('id')
|
|
96
|
+
.single();
|
|
97
|
+
if (boardErr || !newBoard)
|
|
98
|
+
return {
|
|
99
|
+
created: false,
|
|
100
|
+
writeUncertain: true,
|
|
101
|
+
error: `Failed to create board: ${boardErr?.message ?? 'Unknown error'}`,
|
|
102
|
+
};
|
|
103
|
+
board = newBoard;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { data: existingList, error: lookupError } = await supabase
|
|
107
|
+
.from('task_lists')
|
|
108
|
+
.select('id')
|
|
109
|
+
.eq('board_id', board.id)
|
|
110
|
+
.not('status', 'in', '(done,closed)')
|
|
111
|
+
.eq('archived', false)
|
|
112
|
+
.eq('deleted', false)
|
|
113
|
+
.order('created_at', { ascending: true })
|
|
114
|
+
.limit(1)
|
|
115
|
+
.maybeSingle();
|
|
116
|
+
if (lookupError) return { created: false, error: lookupError.message };
|
|
117
|
+
|
|
118
|
+
let list = existingList;
|
|
119
|
+
if (!list) {
|
|
120
|
+
const { data: newList, error: listErr } = await supabase
|
|
121
|
+
.from('task_lists')
|
|
122
|
+
.insert({ name: 'To Do', board_id: board.id })
|
|
123
|
+
.select('id')
|
|
124
|
+
.single();
|
|
125
|
+
if (listErr || !newList)
|
|
126
|
+
return {
|
|
127
|
+
created: false,
|
|
128
|
+
writeUncertain: true,
|
|
129
|
+
error: `Failed to create list: ${listErr?.message ?? 'Unknown error'}`,
|
|
130
|
+
};
|
|
131
|
+
list = newList;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return persistTask(args, ctx, list.id);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function persistTask(
|
|
138
|
+
args: Record<string, unknown>,
|
|
139
|
+
ctx: MiraToolContext,
|
|
140
|
+
listId: string
|
|
141
|
+
) {
|
|
142
|
+
const { userId, supabase } = ctx;
|
|
143
|
+
const name = args.name as string;
|
|
144
|
+
const description = args.description as string | null;
|
|
145
|
+
const priority = (args.priority as Enums<'task_priority'> | null) ?? null;
|
|
146
|
+
const assignToSelf = args.assignToSelf !== false;
|
|
147
|
+
const date = (value: unknown, end: boolean) =>
|
|
148
|
+
typeof value === 'string'
|
|
149
|
+
? ctx.timezone
|
|
150
|
+
? parseTaskDateToUTCISO(value, ctx.timezone, end)
|
|
151
|
+
: value
|
|
152
|
+
: null;
|
|
153
|
+
const { data: task, error } = await supabase
|
|
154
|
+
.from('tasks')
|
|
155
|
+
.insert({
|
|
156
|
+
creator_id: userId,
|
|
157
|
+
name,
|
|
158
|
+
description: description
|
|
159
|
+
? JSON.stringify({
|
|
160
|
+
type: 'doc',
|
|
161
|
+
content: [
|
|
162
|
+
{
|
|
163
|
+
type: 'paragraph',
|
|
164
|
+
content: [{ type: 'text', text: description }],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
})
|
|
168
|
+
: null,
|
|
169
|
+
list_id: listId,
|
|
170
|
+
priority,
|
|
171
|
+
start_date: date(args.startDate, false),
|
|
172
|
+
end_date: date(args.endDate ?? args.dueDate, true),
|
|
173
|
+
estimation_points: (args.estimationPoints as number | null) ?? null,
|
|
174
|
+
completed: false,
|
|
175
|
+
})
|
|
176
|
+
.select('id')
|
|
177
|
+
.single();
|
|
178
|
+
if (error || !task) {
|
|
179
|
+
// PostgreSQL data/constraint/permission errors reject the insert statement.
|
|
180
|
+
// Unknown or transport errors can lose the response after a committed write.
|
|
181
|
+
const rejected =
|
|
182
|
+
/^(22|23)/.test(error?.code ?? '') || error?.code === '42501';
|
|
183
|
+
return {
|
|
184
|
+
success: false,
|
|
185
|
+
...(rejected ? { created: false } : { writeUncertain: true }),
|
|
186
|
+
error: error?.message ?? 'Task creation returned no saved task.',
|
|
187
|
+
instruction: rejected
|
|
188
|
+
? 'The database rejected the insert. Correct the reported problem before retrying.'
|
|
189
|
+
: 'The insert could not be verified. Check existing tasks before attempting creation again.',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let assignmentError: string | undefined;
|
|
194
|
+
if (assignToSelf) {
|
|
195
|
+
const { error } = await supabase
|
|
196
|
+
.from('task_assignees')
|
|
197
|
+
.insert({ task_id: task.id, user_id: userId });
|
|
198
|
+
assignmentError = error?.message;
|
|
199
|
+
}
|
|
200
|
+
// Read the saved row and its location before claiming completion. Never retry
|
|
201
|
+
// a write when this read fails: the ID is returned for recovery instead.
|
|
202
|
+
const { data: saved, error: readError } = await supabase
|
|
203
|
+
.from('tasks')
|
|
204
|
+
.select(
|
|
205
|
+
'id, name, priority, start_date, end_date, estimation_points, created_at, task_lists!inner(id, name, workspace_boards!inner(id, name, ws_id))'
|
|
206
|
+
)
|
|
207
|
+
.eq('id', task.id)
|
|
208
|
+
.single();
|
|
209
|
+
if (readError || !saved)
|
|
210
|
+
return {
|
|
211
|
+
success: false,
|
|
212
|
+
created: true,
|
|
213
|
+
taskId: task.id,
|
|
214
|
+
error:
|
|
215
|
+
'Task was inserted but its saved details could not be verified. Do not create it again; retrieve this task ID.',
|
|
216
|
+
...(assignmentError ? { assignmentError } : {}),
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
success: !assignmentError,
|
|
220
|
+
created: true,
|
|
221
|
+
task: saved,
|
|
222
|
+
workspaceId: getWorkspaceContextWorkspaceId(ctx),
|
|
223
|
+
assignedToSelf: assignToSelf && !assignmentError,
|
|
224
|
+
...(assignmentError
|
|
225
|
+
? {
|
|
226
|
+
error: `Task was created, but assignment failed: ${assignmentError}. Retry assignment only, never creation.`,
|
|
227
|
+
}
|
|
228
|
+
: {}),
|
|
229
|
+
message:
|
|
230
|
+
'Use the saved task ID, location and due date in your confirmation. Do not claim any fields absent from this result.',
|
|
231
|
+
};
|
|
232
|
+
}
|