From bac3672deb1e2d03af22dad060c30d0f802e0733 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 13 May 2026 12:59:53 -0700 Subject: [PATCH 1/2] Improve mship fexecute --- .../lib/copilot/generated/tool-catalog-v1.ts | 8 +- .../lib/copilot/generated/tool-schemas-v1.ts | 10 ++- .../copilot/tool-executor/executor.test.ts | 79 +++++++++++++++++++ .../sim/lib/copilot/tool-executor/executor.ts | 20 ++++- apps/sim/tools/function/types.ts | 5 ++ 5 files changed, 116 insertions(+), 6 deletions(-) diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index 616244da295..1f247315667 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -1040,6 +1040,11 @@ export const FunctionExecute: ToolCatalogEntry = { description: 'Table ID to overwrite with the code\'s return value. Code MUST return an array of objects where keys match column names. All existing rows are replaced. Example: "tbl_abc123"', }, + timeout: { + type: 'number', + description: + 'Optional maximum execution time in seconds. If omitted, Copilot sends 10 seconds by default. Override when needed; capped at the default execution limit.', + }, }, required: ['code'], }, @@ -2964,8 +2969,7 @@ export const UserTable: ToolCatalogEntry = { }, rowIds: { type: 'array', - description: - 'Array of row IDs. Used by batch_delete_rows (rows to delete) and run_column (optional row scope — when omitted, runs across the whole table; when provided, only these rows are candidates and the server eligibility predicate still applies).', + description: 'Array of row IDs to delete (for batch_delete_rows)', items: { type: 'string' }, }, rows: { diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index bb329500012..543afe5d584 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -4,7 +4,7 @@ export type JsonSchema = unknown -interface ToolRuntimeSchemaEntry { +export interface ToolRuntimeSchemaEntry { parameters?: JsonSchema resultSchema?: JsonSchema } @@ -863,6 +863,11 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { description: 'Table ID to overwrite with the code\'s return value. Code MUST return an array of objects where keys match column names. All existing rows are replaced. Example: "tbl_abc123"', }, + timeout: { + type: 'number', + description: + 'Optional maximum execution time in seconds. If omitted, Copilot sends 10 seconds by default. Override when needed; capped at the default execution limit.', + }, }, required: ['code'], }, @@ -2794,8 +2799,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, rowIds: { type: 'array', - description: - 'Array of row IDs. Used by batch_delete_rows (rows to delete) and run_column (optional row scope — when omitted, runs across the whole table; when provided, only these rows are candidates and the server eligibility predicate still applies).', + description: 'Array of row IDs to delete (for batch_delete_rows)', items: { type: 'string', }, diff --git a/apps/sim/lib/copilot/tool-executor/executor.test.ts b/apps/sim/lib/copilot/tool-executor/executor.test.ts index 4ae97dd45c7..72d47deac3b 100644 --- a/apps/sim/lib/copilot/tool-executor/executor.test.ts +++ b/apps/sim/lib/copilot/tool-executor/executor.test.ts @@ -3,6 +3,7 @@ */ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants' const { isKnownTool, isSimExecuted } = vi.hoisted(() => ({ isKnownTool: vi.fn(), @@ -58,4 +59,82 @@ describe('copilot tool executor fallback', () => { ) expect(result).toEqual({ success: true, output: { emails: [] } }) }) + + it('converts function_execute timeout from seconds to milliseconds for copilot calls', async () => { + isKnownTool.mockReturnValue(false) + isSimExecuted.mockReturnValue(false) + executeAppTool.mockResolvedValue({ success: true, output: { result: 'ok' } }) + + await executeTool( + 'function_execute', + { code: 'return 1', timeout: 7 }, + { + userId: 'user-1', + workflowId: 'workflow-1', + workspaceId: 'ws-1', + copilotToolExecution: true, + } + ) + + expect(executeAppTool).toHaveBeenCalledWith( + 'function_execute', + expect.objectContaining({ + timeout: 7000, + _context: expect.objectContaining({ + copilotToolExecution: true, + }), + }), + false + ) + }) + + it('defaults copilot function_execute timeout to 10 seconds when omitted', async () => { + isKnownTool.mockReturnValue(false) + isSimExecuted.mockReturnValue(false) + executeAppTool.mockResolvedValue({ success: true, output: { result: 'ok' } }) + + await executeTool( + 'function_execute', + { code: 'return 1' }, + { + userId: 'user-1', + workflowId: 'workflow-1', + workspaceId: 'ws-1', + copilotToolExecution: true, + } + ) + + expect(executeAppTool).toHaveBeenCalledWith( + 'function_execute', + expect.objectContaining({ + timeout: 10_000, + }), + false + ) + }) + + it('does not let copilot function_execute timeout exceed the default execution limit', async () => { + isKnownTool.mockReturnValue(false) + isSimExecuted.mockReturnValue(false) + executeAppTool.mockResolvedValue({ success: true, output: { result: 'ok' } }) + + await executeTool( + 'function_execute', + { code: 'return 1', timeout: 10_000 }, + { + userId: 'user-1', + workflowId: 'workflow-1', + workspaceId: 'ws-1', + copilotToolExecution: true, + } + ) + + expect(executeAppTool).toHaveBeenCalledWith( + 'function_execute', + expect.objectContaining({ + timeout: DEFAULT_EXECUTION_TIMEOUT_MS, + }), + false + ) + }) }) diff --git a/apps/sim/lib/copilot/tool-executor/executor.ts b/apps/sim/lib/copilot/tool-executor/executor.ts index 7658a25e70e..082252d7e69 100644 --- a/apps/sim/lib/copilot/tool-executor/executor.ts +++ b/apps/sim/lib/copilot/tool-executor/executor.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants' import { executeTool as executeAppTool } from '@/tools' import { isKnownTool, isSimExecuted } from './router' import type { @@ -10,6 +11,9 @@ import type { } from './types' const logger = createLogger('ToolExecutor') +const FUNCTION_EXECUTE_TOOL_ID = 'function_execute' +const DEFAULT_FUNCTION_EXECUTE_TIMEOUT_SECONDS = 10 +const MILLISECONDS_PER_SECOND = 1000 const handlerRegistry = new Map() @@ -38,7 +42,7 @@ export async function executeTool( ): Promise { const canUseRegisteredHandler = isKnownTool(toolId) && isSimExecuted(toolId) if (!canUseRegisteredHandler) { - const appParams = buildAppToolParams(params, context) + const appParams = buildAppToolParams(toolId, params, context) return executeAppTool(toolId, appParams, false) } @@ -95,11 +99,25 @@ async function executeToolBatch( } function buildAppToolParams( + toolId: string, params: Record, context: ToolExecutionContext ): Record { const result = { ...params } + if (toolId === FUNCTION_EXECUTE_TOOL_ID && context.copilotToolExecution) { + const timeoutSeconds = + result.timeout === undefined || result.timeout === null + ? DEFAULT_FUNCTION_EXECUTE_TIMEOUT_SECONDS + : Number(result.timeout) + if (Number.isFinite(timeoutSeconds) && timeoutSeconds > 0) { + result.timeout = Math.min( + Math.ceil(timeoutSeconds * MILLISECONDS_PER_SECOND), + DEFAULT_EXECUTION_TIMEOUT_MS + ) + } + } + if (result.credentialId && !result.credential && !result.oauthCredential) { result.credential = result.credentialId } diff --git a/apps/sim/tools/function/types.ts b/apps/sim/tools/function/types.ts index d44d509174e..decb3978871 100644 --- a/apps/sim/tools/function/types.ts +++ b/apps/sim/tools/function/types.ts @@ -7,6 +7,10 @@ export interface CodeExecutionInput { sourceCode?: string language?: CodeLanguage useLocalVM?: boolean + /** + * Workflow Function blocks pass milliseconds. Copilot/Mothership tool calls pass seconds + * and are converted at the request boundary. + */ timeout?: number memoryLimit?: number outputPath?: string @@ -28,6 +32,7 @@ export interface CodeExecutionInput { allowLargeValueWorkflowScope?: boolean userId?: string workspaceId?: string + copilotToolExecution?: boolean } isCustomTool?: boolean _sandboxFiles?: Array<{ path: string; content: string }> From 4125a3505e6408e26fde2109cae1fe366827af9a Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 13 May 2026 14:19:52 -0700 Subject: [PATCH 2/2] Fix --- .../lib/copilot/generated/tool-catalog-v1.ts | 3 ++- .../lib/copilot/generated/tool-schemas-v1.ts | 3 ++- .../copilot/tool-executor/executor.test.ts | 25 +++++++++++++++++++ .../sim/lib/copilot/tool-executor/executor.ts | 16 ++++++------ 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index 1f247315667..fd373749aa3 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -2969,7 +2969,8 @@ export const UserTable: ToolCatalogEntry = { }, rowIds: { type: 'array', - description: 'Array of row IDs to delete (for batch_delete_rows)', + description: + 'Array of row IDs. Used by batch_delete_rows (rows to delete) and run_column (optional row scope — when omitted, runs across the whole table; when provided, only these rows are candidates and the server eligibility predicate still applies).', items: { type: 'string' }, }, rows: { diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index 543afe5d584..9cd31211322 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -2799,7 +2799,8 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, rowIds: { type: 'array', - description: 'Array of row IDs to delete (for batch_delete_rows)', + description: + 'Array of row IDs. Used by batch_delete_rows (rows to delete) and run_column (optional row scope — when omitted, runs across the whole table; when provided, only these rows are candidates and the server eligibility predicate still applies).', items: { type: 'string', }, diff --git a/apps/sim/lib/copilot/tool-executor/executor.test.ts b/apps/sim/lib/copilot/tool-executor/executor.test.ts index 72d47deac3b..a0cd2eec358 100644 --- a/apps/sim/lib/copilot/tool-executor/executor.test.ts +++ b/apps/sim/lib/copilot/tool-executor/executor.test.ts @@ -113,6 +113,31 @@ describe('copilot tool executor fallback', () => { ) }) + it('defaults copilot function_execute timeout to 10 seconds when invalid', async () => { + isKnownTool.mockReturnValue(false) + isSimExecuted.mockReturnValue(false) + executeAppTool.mockResolvedValue({ success: true, output: { result: 'ok' } }) + + await executeTool( + 'function_execute', + { code: 'return 1', timeout: 0 }, + { + userId: 'user-1', + workflowId: 'workflow-1', + workspaceId: 'ws-1', + copilotToolExecution: true, + } + ) + + expect(executeAppTool).toHaveBeenCalledWith( + 'function_execute', + expect.objectContaining({ + timeout: 10_000, + }), + false + ) + }) + it('does not let copilot function_execute timeout exceed the default execution limit', async () => { isKnownTool.mockReturnValue(false) isSimExecuted.mockReturnValue(false) diff --git a/apps/sim/lib/copilot/tool-executor/executor.ts b/apps/sim/lib/copilot/tool-executor/executor.ts index 082252d7e69..869228970bf 100644 --- a/apps/sim/lib/copilot/tool-executor/executor.ts +++ b/apps/sim/lib/copilot/tool-executor/executor.ts @@ -106,16 +106,18 @@ function buildAppToolParams( const result = { ...params } if (toolId === FUNCTION_EXECUTE_TOOL_ID && context.copilotToolExecution) { - const timeoutSeconds = + const rawTimeoutSeconds = result.timeout === undefined || result.timeout === null ? DEFAULT_FUNCTION_EXECUTE_TIMEOUT_SECONDS : Number(result.timeout) - if (Number.isFinite(timeoutSeconds) && timeoutSeconds > 0) { - result.timeout = Math.min( - Math.ceil(timeoutSeconds * MILLISECONDS_PER_SECOND), - DEFAULT_EXECUTION_TIMEOUT_MS - ) - } + const timeoutSeconds = + Number.isFinite(rawTimeoutSeconds) && rawTimeoutSeconds > 0 + ? rawTimeoutSeconds + : DEFAULT_FUNCTION_EXECUTE_TIMEOUT_SECONDS + result.timeout = Math.min( + Math.ceil(timeoutSeconds * MILLISECONDS_PER_SECOND), + DEFAULT_EXECUTION_TIMEOUT_MS + ) } if (result.credentialId && !result.credential && !result.oauthCredential) {