diff --git a/apps/trigger/package.json b/apps/trigger/package.json index 89c7cea25..09ccafedc 100644 --- a/apps/trigger/package.json +++ b/apps/trigger/package.json @@ -34,7 +34,7 @@ "@buster/web-tools": "workspace:*", "@duckdb/node-api": "1.3.2-alpha.26", "@duckdb/node-bindings": "1.3.2-alpha.26", - "@trigger.dev/sdk": "4.0.2", + "@trigger.dev/sdk": "4.0.4", "@types/js-yaml": "catalog:", "ai": "catalog:", "braintrust": "catalog:", @@ -44,6 +44,6 @@ "zod": "catalog:" }, "devDependencies": { - "@trigger.dev/build": "4.0.2" + "@trigger.dev/build": "4.0.4" } } diff --git a/packages/ai/src/tools/communication-tools/done-tool/done-tool-start.ts b/packages/ai/src/tools/communication-tools/done-tool/done-tool-start.ts index 331462c42..4672e66a1 100644 --- a/packages/ai/src/tools/communication-tools/done-tool/done-tool-start.ts +++ b/packages/ai/src/tools/communication-tools/done-tool/done-tool-start.ts @@ -68,27 +68,30 @@ export function createDoneToolStart(context: DoneToolContext, doneToolState: Don } } - // Update the chat with the most recent file - simply use the first file from extractedFiles - // No filtering or sorting - just use whatever was selected for response messages - if (context.chatId && extractedFiles.length > 0) { - const mostRecentFile = extractedFiles[0]; - - console.info('[done-tool-start] Updating chat with most recent file', { - chatId: context.chatId, - fileId: mostRecentFile.id, - fileType: mostRecentFile.fileType, - fileName: mostRecentFile.fileName, - versionNumber: mostRecentFile.versionNumber, - }); - - try { - await updateChat(context.chatId, { - mostRecentFileId: mostRecentFile.id, - mostRecentFileType: mostRecentFile.fileType, - mostRecentVersionNumber: mostRecentFile.versionNumber || 1, + // Update the chat with the most recent file using the full set that includes reports + // Do not rely on the filtered response list since it excludes report files + if (context.chatId && allFilesForChatUpdate.length > 0) { + const mostRecentFile = + allFilesForChatUpdate.find((f) => f.fileType === 'report_file') || + allFilesForChatUpdate[0]; + if (mostRecentFile) { + console.info('[done-tool-start] Updating chat with most recent file', { + chatId: context.chatId, + fileId: mostRecentFile.id, + fileType: mostRecentFile.fileType, + fileName: mostRecentFile.fileName, + versionNumber: mostRecentFile.versionNumber, }); - } catch (error) { - console.error('[done-tool] Failed to update chat with most recent file:', error); + + try { + await updateChat(context.chatId, { + mostRecentFileId: mostRecentFile.id, + mostRecentFileType: mostRecentFile.fileType, + mostRecentVersionNumber: mostRecentFile.versionNumber || 1, + }); + } catch (error) { + console.error('[done-tool] Failed to update chat with most recent file:', error); + } } } } diff --git a/packages/ai/src/tools/communication-tools/done-tool/done-tool-streaming.test.ts b/packages/ai/src/tools/communication-tools/done-tool/done-tool-streaming.test.ts index bc1665740..ce203556b 100644 --- a/packages/ai/src/tools/communication-tools/done-tool/done-tool-streaming.test.ts +++ b/packages/ai/src/tools/communication-tools/done-tool/done-tool-streaming.test.ts @@ -1,5 +1,8 @@ import type { ModelMessage, ToolCallOptions } from 'ai'; import { describe, expect, test, vi } from 'vitest'; +import { CREATE_DASHBOARDS_TOOL_NAME } from '../../visualization-tools/dashboards/create-dashboards-tool/create-dashboards-tool'; +import { CREATE_METRICS_TOOL_NAME } from '../../visualization-tools/metrics/create-metrics-tool/create-metrics-tool'; +import { CREATE_REPORTS_TOOL_NAME } from '../../visualization-tools/reports/create-reports-tool/create-reports-tool'; import type { DoneToolContext, DoneToolInput, DoneToolState } from './done-tool'; import { createDoneToolDelta } from './done-tool-delta'; import { createDoneToolFinish } from './done-tool-finish'; @@ -132,6 +135,256 @@ describe('Done Tool Streaming Tests', () => { await expect(startHandler(options)).resolves.not.toThrow(); expect(state.toolCallId).toBe('tool-call-789'); }); + + test('should prefer report_file for mostRecent and not create report file responses', async () => { + vi.clearAllMocks(); + + const state: DoneToolState = { + toolCallId: undefined, + args: undefined, + finalResponse: undefined, + }; + + const startHandler = createDoneToolStart(mockContext, state); + + const reportId = 'report-1'; + const messages: ModelMessage[] = [ + { + role: 'assistant', + content: [ + { + type: 'tool-call' as const, + toolCallId: 'tc-report', + toolName: CREATE_REPORTS_TOOL_NAME, + input: { files: [{ content: 'report content' }] }, + }, + ], + }, + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: 'tc-report', + toolName: CREATE_REPORTS_TOOL_NAME, + output: { + type: 'json', + value: JSON.stringify({ + files: [ + { + id: reportId, + name: 'Quarterly Report', + version_number: 1, + }, + ], + }), + }, + }, + ], + }, + ]; + + await startHandler({ toolCallId: 'call-1', messages }); + + const queries = await import('@buster/database/queries'); + + // mostRecent should be set to the report + expect(queries.updateChat).toHaveBeenCalled(); + const updateArgs = ((queries.updateChat as unknown as { mock: { calls: unknown[][] } }).mock + .calls?.[0]?.[1] || {}) as Record; + expect(updateArgs).toMatchObject({ + mostRecentFileId: reportId, + mostRecentFileType: 'report_file', + mostRecentVersionNumber: 1, + }); + + // No file response messages should be created for report-only case + const fileResponseCallWithFiles = ( + queries.updateMessageEntries as unknown as { mock: { calls: [Record][] } } + ).mock.calls.find((c) => + Array.isArray((c[0] as { responseMessages?: unknown[] }).responseMessages) && + ((c[0] as { responseMessages?: { type?: string }[] }).responseMessages || []).some( + (m) => m?.type === 'file' + ) + ); + expect(fileResponseCallWithFiles).toBeUndefined(); + }); + + test('should create non-report file responses but set mostRecent to report when both exist', async () => { + vi.clearAllMocks(); + + const state: DoneToolState = { + toolCallId: undefined, + args: undefined, + finalResponse: undefined, + }; + + const startHandler = createDoneToolStart(mockContext, state); + + const reportId = 'report-2'; + const metricId = 'metric-1'; + + const messages: ModelMessage[] = [ + { + role: 'assistant', + content: [ + { + type: 'tool-call' as const, + toolCallId: 'tc-report', + toolName: CREATE_REPORTS_TOOL_NAME, + input: { files: [{ content: 'report content' }] }, + }, + ], + }, + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: 'tc-report', + toolName: CREATE_REPORTS_TOOL_NAME, + output: { + type: 'json', + value: JSON.stringify({ + files: [ + { + id: reportId, + name: 'Key Metrics Report', + version_number: 1, + }, + ], + }), + }, + }, + ], + }, + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: 'tc-metric', + toolName: CREATE_METRICS_TOOL_NAME, + output: { + type: 'json', + value: JSON.stringify({ + files: [ + { + id: metricId, + name: 'Revenue', + version_number: 1, + }, + ], + }), + }, + }, + ], + }, + ]; + + await startHandler({ toolCallId: 'call-2', messages }); + + const queries = await import('@buster/database/queries'); + + // mostRecent should prefer the report + const updateArgs = ((queries.updateChat as unknown as { mock: { calls: unknown[][] } }).mock + .calls?.[0]?.[1] || {}) as Record; + expect(updateArgs).toMatchObject({ + mostRecentFileId: reportId, + mostRecentFileType: 'report_file', + }); + + // Response messages should include the metric file + const fileResponseCall = ( + queries.updateMessageEntries as unknown as { mock: { calls: [Record][] } } + ).mock.calls.find((c) => + Array.isArray((c[0] as { responseMessages?: unknown[] }).responseMessages) && + ((c[0] as { responseMessages?: { type?: string }[] }).responseMessages || []).some( + (m) => m?.type === 'file' + ) + ); + + expect(fileResponseCall).toBeDefined(); + const responseMessages = (fileResponseCall?.[0] as { responseMessages?: Record[] }) + ?.responseMessages as Record[]; + const metricResponse = responseMessages?.find((m) => m.id === metricId); + expect(metricResponse).toBeDefined(); + expect(metricResponse?.file_type).toBe('metric_file'); + }); + + test('should fall back to first non-report file when no report exists', async () => { + vi.clearAllMocks(); + + const state: DoneToolState = { + toolCallId: undefined, + args: undefined, + finalResponse: undefined, + }; + + const startHandler = createDoneToolStart(mockContext, state); + + const dashboardId = 'dash-1'; + const metricId = 'metric-2'; + + const messages: ModelMessage[] = [ + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: 'tc-dash', + toolName: CREATE_DASHBOARDS_TOOL_NAME, + output: { + type: 'json', + value: JSON.stringify({ + files: [ + { + id: dashboardId, + name: 'Sales Dashboard', + version_number: 1, + }, + ], + }), + }, + }, + ], + }, + { + role: 'tool', + content: [ + { + type: 'tool-result', + toolCallId: 'tc-metric2', + toolName: CREATE_METRICS_TOOL_NAME, + output: { + type: 'json', + value: JSON.stringify({ + files: [ + { + id: metricId, + name: 'Margin', + version_number: 1, + }, + ], + }), + }, + }, + ], + }, + ]; + + await startHandler({ toolCallId: 'call-3', messages }); + + const queries = await import('@buster/database/queries'); + const updateArgs = ((queries.updateChat as unknown as { mock: { calls: unknown[][] } }).mock + .calls[0]?.[1] || {}) as Record; + + // Should fall back to the first available (dashboard here) + expect(updateArgs).toMatchObject({ + mostRecentFileId: dashboardId, + mostRecentFileType: 'dashboard_file', + }); + }); }); describe('createDoneToolDelta', () => { diff --git a/packages/ai/src/utils/with-agent-retry.ts b/packages/ai/src/utils/with-agent-retry.ts index 220184c62..197ae9556 100644 --- a/packages/ai/src/utils/with-agent-retry.ts +++ b/packages/ai/src/utils/with-agent-retry.ts @@ -71,7 +71,7 @@ export const isOverloadedError = (error: unknown): boolean => { if ('message' in error && typeof error.message === 'string') { const lowerMessage = error.message.toLowerCase(); return ( - lowerMessage.includes('overloaded') || + lowerMessage.includes('overloaded') || lowerMessage.includes('overloaded_error') || lowerMessage.includes('terminated') ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3aad8620..7f10ec96b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -368,8 +368,8 @@ importers: specifier: 1.3.2-alpha.26 version: 1.3.2-alpha.26 '@trigger.dev/sdk': - specifier: 4.0.2 - version: 4.0.2(ai@5.0.44(zod@3.25.76))(zod@3.25.76) + specifier: 4.0.4 + version: 4.0.4(ai@5.0.44(zod@3.25.76))(zod@3.25.76) '@types/js-yaml': specifier: 'catalog:' version: 4.0.9 @@ -393,8 +393,8 @@ importers: version: 3.25.76 devDependencies: '@trigger.dev/build': - specifier: 4.0.2 - version: 4.0.2(typescript@5.9.2) + specifier: 4.0.4 + version: 4.0.4(typescript@5.9.2) apps/web: dependencies: @@ -6245,14 +6245,18 @@ packages: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} - '@trigger.dev/build@4.0.2': - resolution: {integrity: sha512-GOqjGIUXWEEIfWqY2o+xr//4KTHYoRQIML8cCoP/L8x1wPb45qFJWTwNaECgYp9i+9vPMI4/G3Jm/jvWp9xznQ==} + '@trigger.dev/build@4.0.4': + resolution: {integrity: sha512-W3mP+RBkcYOrNYTTmQ/WdU6LB+2Tk1S6r3OjEWqXEPsXLEEw6BzHTHZBirHYX4lWRBL9jVkL+/H74ycyNfzRjg==} engines: {node: '>=18.20.0'} '@trigger.dev/core@4.0.2': resolution: {integrity: sha512-hc/alfT7iVdJNZ5YSMbGR9FirLjURqdZ7tCBX4btKas0GDg6M5onwcQsJ3oom5TDp/Nrt+dHaviNMhFxhKCu3g==} engines: {node: '>=18.20.0'} + '@trigger.dev/core@4.0.4': + resolution: {integrity: sha512-c5myttkNhqaqvLlEz3ttE1qEsULlD6ILBge5FAfEtMv9HVS/pNlgvMKrdFMefaGO/bE4HoxrNGdJsY683Kq32w==} + engines: {node: '>=18.20.0'} + '@trigger.dev/sdk@4.0.2': resolution: {integrity: sha512-ulhWJRSHPXOHz0bMvkhAKThkW63x7lnjAb87LPi6dUps1YwwoOL8Nkr15xLXa73UrldPFT+9Y/GvQ9qpzU478w==} engines: {node: '>=18.20.0'} @@ -6263,6 +6267,16 @@ packages: ai: optional: true + '@trigger.dev/sdk@4.0.4': + resolution: {integrity: sha512-54krRw9SN1CGm5u17JBzu0hNzRf1u37jKbSFFngPJjUOltOgi/owey5+KNu1rGthabhOBK2VKzvKEd4sn08RCA==} + engines: {node: '>=18.20.0'} + peerDependencies: + ai: ^4.2.0 || ^5.0.0 + zod: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + ai: + optional: true + '@turbopuffer/turbopuffer@1.0.0': resolution: {integrity: sha512-flPN00Zy9KgOHzPampOZPkNWNHLrkpcTiTel+VQ7sznOZO79LErGSDwzoyD7ykEj85scY7T8jVsD+qwQ9OPjyQ==} @@ -19263,9 +19277,9 @@ snapshots: '@tootallnate/once@2.0.0': {} - '@trigger.dev/build@4.0.2(typescript@5.9.2)': + '@trigger.dev/build@4.0.4(typescript@5.9.2)': dependencies: - '@trigger.dev/core': 4.0.2 + '@trigger.dev/core': 4.0.4 pkg-types: 1.3.1 tinyglobby: 0.2.14 tsconfck: 3.1.3(typescript@5.9.2) @@ -19314,6 +19328,45 @@ snapshots: - supports-color - utf-8-validate + '@trigger.dev/core@4.0.4': + dependencies: + '@bugsnag/cuid': 3.2.1 + '@electric-sql/client': 1.0.0-beta.1 + '@google-cloud/precise-date': 4.0.0 + '@jsonhero/path': 1.0.21 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + dequal: 2.0.3 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + execa: 8.0.1 + humanize-duration: 3.33.0 + jose: 5.10.0 + nanoid: 3.3.8 + prom-client: 15.1.3 + socket.io: 4.7.4 + socket.io-client: 4.7.5 + std-env: 3.9.0 + superjson: 2.2.2 + tinyexec: 0.3.2 + uncrypto: 0.1.3 + zod: 3.25.76 + zod-error: 1.5.0 + zod-validation-error: 1.5.0(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@trigger.dev/sdk@4.0.2(ai@5.0.44(zod@3.25.76))(zod@3.25.76)': dependencies: '@opentelemetry/api': 1.9.0 @@ -19336,6 +19389,28 @@ snapshots: - supports-color - utf-8-validate + '@trigger.dev/sdk@4.0.4(ai@5.0.44(zod@3.25.76))(zod@3.25.76)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.36.0 + '@trigger.dev/core': 4.0.4 + chalk: 5.6.0 + cronstrue: 2.59.0 + debug: 4.4.1 + evt: 2.5.9 + slug: 6.1.0 + ulid: 2.4.0 + uncrypto: 0.1.3 + uuid: 9.0.1 + ws: 8.18.3 + zod: 3.25.76 + optionalDependencies: + ai: 5.0.44(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@turbopuffer/turbopuffer@1.0.0': dependencies: pako: 2.1.0