@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
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
1
|
import { deductAiCredits } from '@tuturuuu/ai/credits/check-credits';
|
|
3
2
|
import type { CreditDeductionResult } from '../../credits/types';
|
|
3
|
+
import { collectAssistantMessageParts } from './assistant-message-parts';
|
|
4
|
+
import { countGoogleSearchQueries } from './google-search-usage';
|
|
4
5
|
|
|
5
6
|
type UsageLike = {
|
|
6
7
|
inputTokens?: number;
|
|
@@ -21,7 +22,6 @@ type UsageLike = {
|
|
|
21
22
|
};
|
|
22
23
|
reasoningTokens?: number;
|
|
23
24
|
};
|
|
24
|
-
|
|
25
25
|
type GroundingMetadataLike = {
|
|
26
26
|
webSearchQueries?: string[];
|
|
27
27
|
};
|
|
@@ -51,6 +51,7 @@ type SourceLike = {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
type StepLike = {
|
|
54
|
+
content?: readonly unknown[];
|
|
54
55
|
text?: string;
|
|
55
56
|
toolCalls?: ReadonlyArray<ToolCallLike>;
|
|
56
57
|
toolResults?: ReadonlyArray<ToolResultLike>;
|
|
@@ -248,105 +249,6 @@ function logGoogleSearchDebug(response: StreamFinishResponseLike): void {
|
|
|
248
249
|
}
|
|
249
250
|
}
|
|
250
251
|
|
|
251
|
-
function countGoogleSearchQueries(
|
|
252
|
-
response: StreamFinishResponseLike,
|
|
253
|
-
allToolCalls: ToolCallLike[]
|
|
254
|
-
): number {
|
|
255
|
-
const customGoogleSearchCalls = allToolCalls.filter(
|
|
256
|
-
(toolCall) => toolCall.toolName === 'google_search'
|
|
257
|
-
).length;
|
|
258
|
-
if (customGoogleSearchCalls > 0) return customGoogleSearchCalls;
|
|
259
|
-
|
|
260
|
-
const topQueries =
|
|
261
|
-
response.providerMetadata?.google?.groundingMetadata?.webSearchQueries;
|
|
262
|
-
if (topQueries?.length) return topQueries.length;
|
|
263
|
-
|
|
264
|
-
let perStepQueriesCount = 0;
|
|
265
|
-
for (const step of response.steps ?? []) {
|
|
266
|
-
const stepQueries =
|
|
267
|
-
step.providerMetadata?.google?.groundingMetadata?.webSearchQueries;
|
|
268
|
-
if (stepQueries?.length) {
|
|
269
|
-
perStepQueriesCount += stepQueries.length;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
if (perStepQueriesCount > 0) return perStepQueriesCount;
|
|
273
|
-
|
|
274
|
-
if (Array.isArray(response.sources) && response.sources.length > 0) {
|
|
275
|
-
return 1;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return 0;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function collectUiMessageParts({
|
|
282
|
-
allToolCalls,
|
|
283
|
-
allToolResults,
|
|
284
|
-
reasoningText,
|
|
285
|
-
response,
|
|
286
|
-
serializedSources,
|
|
287
|
-
}: {
|
|
288
|
-
allToolCalls: ToolCallLike[];
|
|
289
|
-
allToolResults: ToolResultLike[];
|
|
290
|
-
reasoningText: string;
|
|
291
|
-
response: StreamFinishResponseLike;
|
|
292
|
-
serializedSources: ReturnType<typeof collectSerializableSources>;
|
|
293
|
-
}) {
|
|
294
|
-
const parts: Record<string, unknown>[] = [];
|
|
295
|
-
|
|
296
|
-
if (response.text) {
|
|
297
|
-
parts.push({ type: 'text', text: response.text });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (reasoningText) {
|
|
301
|
-
parts.push({ type: 'reasoning', text: reasoningText });
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
for (const toolCall of allToolCalls) {
|
|
305
|
-
const toolCallId =
|
|
306
|
-
toolCall.toolCallId ?? readString(toolCall.toolCallId) ?? undefined;
|
|
307
|
-
const matchingResult = allToolResults.find(
|
|
308
|
-
(result) => toolCallId && readString(result.toolCallId) === toolCallId
|
|
309
|
-
);
|
|
310
|
-
parts.push({
|
|
311
|
-
type: 'dynamic-tool',
|
|
312
|
-
toolName: toolCall.toolName ?? matchingResult?.toolName ?? 'tool',
|
|
313
|
-
toolCallId: toolCallId ?? randomUUID(),
|
|
314
|
-
state: 'output-available',
|
|
315
|
-
input: extractToolInput(toolCall),
|
|
316
|
-
output: matchingResult ? extractToolOutput(matchingResult) : null,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
for (const toolResult of allToolResults) {
|
|
321
|
-
const hasCall = allToolCalls.some(
|
|
322
|
-
(toolCall) =>
|
|
323
|
-
readString(toolCall.toolCallId) &&
|
|
324
|
-
readString(toolCall.toolCallId) === readString(toolResult.toolCallId)
|
|
325
|
-
);
|
|
326
|
-
if (hasCall) continue;
|
|
327
|
-
|
|
328
|
-
parts.push({
|
|
329
|
-
type: 'dynamic-tool',
|
|
330
|
-
toolName: toolResult.toolName ?? 'tool',
|
|
331
|
-
toolCallId: toolResult.toolCallId ?? randomUUID(),
|
|
332
|
-
state: 'output-available',
|
|
333
|
-
output: extractToolOutput(toolResult),
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
for (const source of serializedSources) {
|
|
338
|
-
if (!source.url) continue;
|
|
339
|
-
parts.push({
|
|
340
|
-
type: 'source-url',
|
|
341
|
-
sourceId: source.sourceId ?? source.url,
|
|
342
|
-
title: source.title,
|
|
343
|
-
url: source.url,
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return parts;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
252
|
export function buildAbortedStreamFinishResponse(
|
|
351
253
|
steps: ReadonlyArray<StepLike>
|
|
352
254
|
): StreamFinishResponseLike {
|
|
@@ -386,14 +288,6 @@ function sumStepUsage(steps: ReadonlyArray<StepLike>): UsageLike {
|
|
|
386
288
|
);
|
|
387
289
|
}
|
|
388
290
|
|
|
389
|
-
function extractToolInput(toolCall: ToolCallLike) {
|
|
390
|
-
return toolCall.input ?? toolCall.args ?? toolCall.arguments ?? {};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function extractToolOutput(toolResult: ToolResultLike) {
|
|
394
|
-
return toolResult.output ?? toolResult.result ?? toolResult;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
291
|
function readString(value: unknown) {
|
|
398
292
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
399
293
|
}
|
|
@@ -419,7 +313,7 @@ function compactJsonValue(value: unknown, maxLength = 400): unknown {
|
|
|
419
313
|
}
|
|
420
314
|
}
|
|
421
315
|
|
|
422
|
-
function compactAiMessagePart(part: Record<string, unknown
|
|
316
|
+
function compactAiMessagePart(part: Record<string, unknown>, limit = 400) {
|
|
423
317
|
const type = readString(part.type) ?? 'unknown';
|
|
424
318
|
|
|
425
319
|
if (type === 'text' || type === 'reasoning') {
|
|
@@ -427,7 +321,7 @@ function compactAiMessagePart(part: Record<string, unknown>) {
|
|
|
427
321
|
...part,
|
|
428
322
|
text:
|
|
429
323
|
typeof part.text === 'string'
|
|
430
|
-
? truncateString(part.text,
|
|
324
|
+
? truncateString(part.text, limit)
|
|
431
325
|
: part.text,
|
|
432
326
|
};
|
|
433
327
|
}
|
|
@@ -438,11 +332,11 @@ function compactAiMessagePart(part: Record<string, unknown>) {
|
|
|
438
332
|
toolName: part.toolName,
|
|
439
333
|
toolCallId: part.toolCallId,
|
|
440
334
|
state: part.state,
|
|
441
|
-
input: compactJsonValue(part.input),
|
|
442
|
-
output: compactJsonValue(part.output),
|
|
335
|
+
input: compactJsonValue(part.input, limit),
|
|
336
|
+
output: compactJsonValue(part.output, limit),
|
|
443
337
|
errorText:
|
|
444
338
|
typeof part.errorText === 'string'
|
|
445
|
-
? truncateString(part.errorText,
|
|
339
|
+
? truncateString(part.errorText, limit)
|
|
446
340
|
: part.errorText,
|
|
447
341
|
};
|
|
448
342
|
}
|
|
@@ -528,6 +422,22 @@ function compactAssistantMessageMetadata(
|
|
|
528
422
|
metadata: ReturnType<typeof buildAssistantMessageMetadata>
|
|
529
423
|
) {
|
|
530
424
|
const ai = metadata.ai;
|
|
425
|
+
let textOffset = 0;
|
|
426
|
+
const compactParts = ai.parts.map((part) => {
|
|
427
|
+
if (part.type === 'text' && typeof part.text === 'string') {
|
|
428
|
+
const result = {
|
|
429
|
+
type: 'text',
|
|
430
|
+
textStart: textOffset,
|
|
431
|
+
textLength: part.text.length,
|
|
432
|
+
};
|
|
433
|
+
textOffset += part.text.length + 2;
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
return compactAiMessagePart(
|
|
437
|
+
part,
|
|
438
|
+
Math.max(20, Math.min(400, Math.floor(1500 / ai.parts.length)))
|
|
439
|
+
);
|
|
440
|
+
});
|
|
531
441
|
return {
|
|
532
442
|
source: metadata.source,
|
|
533
443
|
...(metadata.requestId ? { requestId: metadata.requestId } : {}),
|
|
@@ -535,27 +445,11 @@ function compactAssistantMessageMetadata(
|
|
|
535
445
|
finishReason: ai.finishReason,
|
|
536
446
|
metadataCompacted: true,
|
|
537
447
|
model: ai.model,
|
|
538
|
-
observability: {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
: [],
|
|
542
|
-
},
|
|
543
|
-
omittedPartCount: Math.max(0, ai.parts.length - 8),
|
|
544
|
-
parts: ai.parts
|
|
545
|
-
.slice(0, 8)
|
|
546
|
-
.map((part) =>
|
|
547
|
-
part && typeof part === 'object' && !Array.isArray(part)
|
|
548
|
-
? compactAiMessagePart(part as Record<string, unknown>)
|
|
549
|
-
: part
|
|
550
|
-
),
|
|
448
|
+
observability: { contextBreakdown: [] },
|
|
449
|
+
omittedPartCount: 0,
|
|
450
|
+
parts: compactParts,
|
|
551
451
|
usage: ai.usage,
|
|
552
452
|
},
|
|
553
|
-
...(typeof metadata.reasoning === 'string'
|
|
554
|
-
? { reasoning: truncateString(metadata.reasoning, 3000) }
|
|
555
|
-
: {}),
|
|
556
|
-
...(Array.isArray(metadata.sources)
|
|
557
|
-
? { sources: metadata.sources.slice(0, 20) }
|
|
558
|
-
: {}),
|
|
559
453
|
toolCallCount: Array.isArray(metadata.toolCalls)
|
|
560
454
|
? metadata.toolCalls.length
|
|
561
455
|
: 0,
|
|
@@ -579,15 +473,6 @@ export async function persistAssistantResponse({
|
|
|
579
473
|
const steps = response.steps ?? [];
|
|
580
474
|
const { allToolCalls, allToolResults } = collectToolData(steps);
|
|
581
475
|
|
|
582
|
-
if (
|
|
583
|
-
!response.text &&
|
|
584
|
-
allToolCalls.length === 0 &&
|
|
585
|
-
allToolResults.length === 0
|
|
586
|
-
) {
|
|
587
|
-
console.warn('onFinish: no text and no tool calls — skipping DB save');
|
|
588
|
-
return false;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
476
|
const reasoningText = collectReasoningText(response);
|
|
592
477
|
const {
|
|
593
478
|
cachedInputTokens,
|
|
@@ -597,13 +482,14 @@ export async function persistAssistantResponse({
|
|
|
597
482
|
reasoningTokens,
|
|
598
483
|
} = collectUsageTotals(response);
|
|
599
484
|
const serializedSources = collectSerializableSources(response);
|
|
600
|
-
const parts =
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
reasoningText,
|
|
604
|
-
response,
|
|
605
|
-
serializedSources,
|
|
485
|
+
const parts = collectAssistantMessageParts({
|
|
486
|
+
...response,
|
|
487
|
+
sources: serializedSources,
|
|
606
488
|
});
|
|
489
|
+
if (!parts.some((part) => part.type !== 'step-start')) {
|
|
490
|
+
console.warn('onFinish: no message parts — skipping DB save');
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
607
493
|
|
|
608
494
|
const metadata = buildAssistantMessageMetadata({
|
|
609
495
|
allToolCalls,
|
|
@@ -626,7 +512,10 @@ export async function persistAssistantResponse({
|
|
|
626
512
|
const insertPayload = {
|
|
627
513
|
chat_id: chatId,
|
|
628
514
|
creator_id: userId,
|
|
629
|
-
content:
|
|
515
|
+
content: parts
|
|
516
|
+
.filter((part) => part.type === 'text')
|
|
517
|
+
.map((part) => part.text)
|
|
518
|
+
.join('\n\n'),
|
|
630
519
|
role: 'ASSISTANT',
|
|
631
520
|
model: (model.includes('/')
|
|
632
521
|
? model.split('/').pop()!
|
|
@@ -666,7 +555,7 @@ export async function persistAssistantResponse({
|
|
|
666
555
|
console.log('AI Response saved to database');
|
|
667
556
|
logGoogleSearchDebug(response);
|
|
668
557
|
|
|
669
|
-
const searchCount = countGoogleSearchQueries(response, allToolCalls);
|
|
558
|
+
const searchCount = countGoogleSearchQueries(model, response, allToolCalls);
|
|
670
559
|
if (searchCount > 0) {
|
|
671
560
|
console.log(
|
|
672
561
|
`Google Search grounding detected: ${searchCount} search quer${searchCount === 1 ? 'y' : 'ies'}`
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ModelMessage } from 'ai';
|
|
2
|
+
|
|
3
|
+
/** Route explicit product presentation to native panels, including follow-ups. */
|
|
4
|
+
export function shouldPresentWorkspaceArtifact(
|
|
5
|
+
messages: ModelMessage[]
|
|
6
|
+
): boolean {
|
|
7
|
+
const userTexts = messages
|
|
8
|
+
.filter((message) => message.role === 'user')
|
|
9
|
+
.map((message) =>
|
|
10
|
+
(typeof message.content === 'string'
|
|
11
|
+
? message.content
|
|
12
|
+
: message.content
|
|
13
|
+
.filter((part) => part.type === 'text')
|
|
14
|
+
.map((part) => part.text)
|
|
15
|
+
.join(' ')
|
|
16
|
+
)
|
|
17
|
+
.normalize('NFD')
|
|
18
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
);
|
|
21
|
+
const latest = userTexts.at(-1) ?? '';
|
|
22
|
+
if (
|
|
23
|
+
/\b((?:don't|do not) (?:show|open|display|present|see|view)|(?:can't|cannot) (?:see|view)|without (?:(?:any|an?) )?(?:artifacts?|panels?)|(?:no|not)(?: (?:in|as))?(?: an?)? (?:artifacts?|panels?)|text[-\s]+only|chat[-\s]+only|how (do|can|to))\b|\bkhong (?:mo|hien thi|xem)\b/u.test(
|
|
24
|
+
latest
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
return false;
|
|
28
|
+
const product =
|
|
29
|
+
/\b(tasks?|to[-\s]?dos?|calendars?|schedules?|agendas?|finances?|wallets?|transactions?|meetings?|cong viec|lich|tai chinh|cuoc hop)\b/u;
|
|
30
|
+
const present =
|
|
31
|
+
/\b(show|display|present|open|view|see|hien thi|cho (toi|minh) xem|mo)\b/u;
|
|
32
|
+
if (present.test(latest) && product.test(latest)) return true;
|
|
33
|
+
return (
|
|
34
|
+
/\b(artifact|panel)\b/u.test(latest) &&
|
|
35
|
+
(present.test(latest) || /\b(should|instead)\b/u.test(latest)) &&
|
|
36
|
+
userTexts.slice(-4).some((text) => product.test(text))
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { buildMiraContext } from '../tools/context-builder';
|
|
2
|
+
import type { MiraWorkspaceContextState } from '../tools/workspace-context';
|
|
3
|
+
import { buildMiraSystemInstruction } from './mira-system-instruction';
|
|
4
|
+
|
|
5
|
+
/** The shared identity, memory, workspace and behavior prompt for Chat and Live. */
|
|
6
|
+
export async function buildMiraPrompt({
|
|
7
|
+
workspaceContext,
|
|
8
|
+
taskBoardInstruction,
|
|
9
|
+
...options
|
|
10
|
+
}: Parameters<typeof buildMiraContext>[0] & {
|
|
11
|
+
workspaceContext: MiraWorkspaceContextState;
|
|
12
|
+
taskBoardInstruction?: string | null;
|
|
13
|
+
}): Promise<string> {
|
|
14
|
+
const { contextString, soul, isFirstInteraction } =
|
|
15
|
+
await buildMiraContext(options);
|
|
16
|
+
const workspaceInstruction = `## Workspace Context\n\nCurrent task/calendar/finance workspace context: ${workspaceContext.name} (${workspaceContext.personal ? 'personal' : 'shared'} workspace).\nUse this workspace for "my tasks", "my calendar", and "my finance" requests. Only switch to another workspace when the user explicitly names a different workspace.`;
|
|
17
|
+
return [
|
|
18
|
+
contextString,
|
|
19
|
+
workspaceInstruction,
|
|
20
|
+
taskBoardInstruction,
|
|
21
|
+
buildMiraSystemInstruction({
|
|
22
|
+
soul,
|
|
23
|
+
isFirstInteraction,
|
|
24
|
+
withoutPermission: options.withoutPermission,
|
|
25
|
+
}),
|
|
26
|
+
]
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.join('\n\n');
|
|
29
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModelMessage } from 'ai';
|
|
2
|
+
import { isGoogleSearchToolName } from '../tools/google-search-events';
|
|
2
3
|
|
|
3
4
|
type ToolStepLike = {
|
|
4
5
|
toolCalls?: Array<{
|
|
@@ -229,6 +230,15 @@ function hasExplicitWorkspaceQualifier(text: string): boolean {
|
|
|
229
230
|
|
|
230
231
|
const normalizedCandidate = normalizeWorkspaceQualifierCandidate(candidate);
|
|
231
232
|
if (!normalizedCandidate) return false;
|
|
233
|
+
// Explicit workspace naming wins over presentation-destination heuristics.
|
|
234
|
+
if (/\bworkspace\b/u.test(normalizedCandidate)) return true;
|
|
235
|
+
// A presentation destination is not a named workspace.
|
|
236
|
+
if (
|
|
237
|
+
/^(?:(?:the|an?|my)\s+)?(?:(?:tasks?|calendar|finance|meetings?)\s+)?(?:panels?|artifacts?|chat|sidebar|views?|screen)\b/u.test(
|
|
238
|
+
normalizedCandidate
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
return false;
|
|
232
242
|
if (DISALLOWED_WORKSPACE_QUALIFIERS.has(normalizedCandidate)) return false;
|
|
233
243
|
|
|
234
244
|
return true;
|
|
@@ -386,6 +396,20 @@ export function shouldForceWorkspaceMembersForLatestUserMessage(
|
|
|
386
396
|
return false;
|
|
387
397
|
}
|
|
388
398
|
|
|
399
|
+
function selectedNames(output: Record<string, unknown> | undefined): string[] {
|
|
400
|
+
if (Array.isArray(output?.selectedTools)) {
|
|
401
|
+
return output.selectedTools.filter(
|
|
402
|
+
(name): name is string => typeof name === 'string'
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
if (Array.isArray(output?.tools)) {
|
|
406
|
+
return output.tools.flatMap((entry) =>
|
|
407
|
+
isRecord(entry) && typeof entry.name === 'string' ? [entry.name] : []
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
|
|
389
413
|
export function extractSelectedToolsFromSteps(steps: unknown[]): string[] {
|
|
390
414
|
for (let i = steps.length - 1; i >= 0; i--) {
|
|
391
415
|
const step = steps[i] as ToolStepLike | undefined;
|
|
@@ -397,15 +421,10 @@ export function extractSelectedToolsFromSteps(steps: unknown[]): string[] {
|
|
|
397
421
|
return tools.filter((tool): tool is string => typeof tool === 'string');
|
|
398
422
|
}
|
|
399
423
|
|
|
400
|
-
const selectResult =
|
|
401
|
-
(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (Array.isArray(selectedTools)) {
|
|
405
|
-
return selectedTools.filter(
|
|
406
|
-
(tool): tool is string => typeof tool === 'string'
|
|
407
|
-
);
|
|
408
|
-
}
|
|
424
|
+
const selectResult =
|
|
425
|
+
step?.toolResults?.find((result) => result.toolName === 'select_tools') ??
|
|
426
|
+
step?.toolResults?.find((result) => result.toolName === 'search_tools');
|
|
427
|
+
if (selectResult) return selectedNames(selectResult.output);
|
|
409
428
|
}
|
|
410
429
|
return [];
|
|
411
430
|
}
|
|
@@ -429,12 +448,12 @@ export function wasToolEverSelectedInSteps(
|
|
|
429
448
|
if (fromCalls) return true;
|
|
430
449
|
|
|
431
450
|
return (typedStep?.toolResults ?? []).some((toolResult) => {
|
|
432
|
-
if (
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
);
|
|
451
|
+
if (
|
|
452
|
+
toolResult.toolName !== 'select_tools' &&
|
|
453
|
+
toolResult.toolName !== 'search_tools'
|
|
454
|
+
)
|
|
455
|
+
return false;
|
|
456
|
+
return selectedNames(toolResult.output).includes(toolName);
|
|
438
457
|
});
|
|
439
458
|
});
|
|
440
459
|
}
|
|
@@ -443,13 +462,17 @@ export function hasToolCallInSteps(
|
|
|
443
462
|
steps: unknown[],
|
|
444
463
|
toolName: string
|
|
445
464
|
): boolean {
|
|
465
|
+
const matches = (name: string | undefined) =>
|
|
466
|
+
toolName === 'google_search'
|
|
467
|
+
? isGoogleSearchToolName(name)
|
|
468
|
+
: name === toolName;
|
|
446
469
|
return steps.some((step) => {
|
|
447
470
|
const typedStep = step as ToolStepLike | undefined;
|
|
448
|
-
const called = (typedStep?.toolCalls ?? []).some(
|
|
449
|
-
(toolCall
|
|
471
|
+
const called = (typedStep?.toolCalls ?? []).some((toolCall) =>
|
|
472
|
+
matches(toolCall.toolName)
|
|
450
473
|
);
|
|
451
|
-
const hasResult = (typedStep?.toolResults ?? []).some(
|
|
452
|
-
(toolResult
|
|
474
|
+
const hasResult = (typedStep?.toolResults ?? []).some((toolResult) =>
|
|
475
|
+
matches(toolResult.toolName)
|
|
453
476
|
);
|
|
454
477
|
return called || hasResult;
|
|
455
478
|
});
|
|
@@ -516,7 +539,8 @@ export function countRenderUiAttemptsInSteps(steps: unknown[]): number {
|
|
|
516
539
|
export function buildActiveToolsFromSelected(
|
|
517
540
|
selectedTools: string[]
|
|
518
541
|
): string[] {
|
|
519
|
-
if (selectedTools.length === 0)
|
|
542
|
+
if (selectedTools.length === 0)
|
|
543
|
+
return ['search_tools', 'select_tools', 'no_action_needed'];
|
|
520
544
|
|
|
521
545
|
const unique = Array.from(new Set(selectedTools));
|
|
522
546
|
const includesNoAction = unique.includes('no_action_needed');
|
|
@@ -526,6 +550,7 @@ export function buildActiveToolsFromSelected(
|
|
|
526
550
|
(toolName) =>
|
|
527
551
|
toolName !== 'select_tools' && toolName !== 'no_action_needed'
|
|
528
552
|
),
|
|
553
|
+
'search_tools',
|
|
529
554
|
'select_tools',
|
|
530
555
|
...(includesNoAction ? ['no_action_needed'] : []),
|
|
531
556
|
];
|
|
@@ -538,3 +563,23 @@ export function removeWorkspaceDiscoveryTools(selectedTools: string[]) {
|
|
|
538
563
|
(toolName) => !WORKSPACE_DISCOVERY_TOOL_NAMES.has(toolName)
|
|
539
564
|
);
|
|
540
565
|
}
|
|
566
|
+
|
|
567
|
+
/** A dispatched UI command is complete only after an acknowledged success. */
|
|
568
|
+
export function getWorkspaceArtifactProgress(steps: unknown[]) {
|
|
569
|
+
let attempts = 0;
|
|
570
|
+
let completed = false;
|
|
571
|
+
for (const step of steps) {
|
|
572
|
+
const value = step as ToolStepLike | undefined;
|
|
573
|
+
const calls = (value?.toolCalls ?? []).filter(
|
|
574
|
+
(call) => call.toolName === 'show_workspace_artifact'
|
|
575
|
+
);
|
|
576
|
+
const results = (value?.toolResults ?? []).filter(
|
|
577
|
+
(result) => result.toolName === 'show_workspace_artifact'
|
|
578
|
+
);
|
|
579
|
+
attempts += Math.max(calls.length, results.length);
|
|
580
|
+
completed ||= results.some(
|
|
581
|
+
(result) => result.output?.success === true && !result.output.error
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
return { attempts, completed };
|
|
585
|
+
}
|