From 78b5ae7b3dbadf2b090dfef46c7b02a57ed39fee Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 4 Jul 2025 13:48:17 -0700 Subject: [PATCH] v0.2.7: fix + feat (#615) * feat(logging): add additional logs for proxy routes * fix(blocks): workflow handler not working outside gui (#609) * fix: key to call api internally for workflow block * feat: use jwt for internal auth to avoid a static key * chore: formatter * fix(sidebar): added loop & parallel subblcoks to sidebar search * merged improvement/connection into staging (#604) * merged improvement/connection into staging * fix: merge conflicts and improved block path calculation * fix: removed migration * fix: removed duplicate call * fix: resolver and merge conflicts * fix: knowledge base folder * fix: settings modal * fix: typeform block * fix: parallel handler * fix: stores index * fix: tests * fix: tag-dropdown * improvement: start block input and tag dropdown * fix block id resolution + missing bracket * fix lint * fix test * works * fix * fix lint * Revert "fix lint" This reverts commit 433e2f9cfc383c80f110e76bcf202a4fb4feeb86. --------- Co-authored-by: Vikhyath Mondreti Co-authored-by: Vikhyath Mondreti * fix(autopan): migration missing (#614) * add autopan migration * fix lint * fix linter * fix tests --------- Co-authored-by: Vikhyath Mondreti --------- Co-authored-by: Waleed Latif Co-authored-by: Aditya Tripathi Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Co-authored-by: Vikhyath Mondreti Co-authored-by: Vikhyath Mondreti --- apps/docs/content/docs/blocks/workflow.mdx | 28 +- apps/sim/app/api/__test-utils__/utils.ts | 5 +- .../app/api/chat/[subdomain]/route.test.ts | 20 +- apps/sim/app/api/chat/[subdomain]/route.ts | 14 +- apps/sim/app/api/chat/utils.ts | 22 +- apps/sim/app/api/codegen/route.ts | 4 +- apps/sim/app/api/providers/route.ts | 32 +- apps/sim/app/api/proxy/route.ts | 79 +- apps/sim/app/api/user/settings/route.ts | 3 + .../api/workflows/[id]/deploy/route.test.ts | 159 +- .../app/api/workflows/[id]/deploy/route.ts | 2 +- .../api/workflows/[id]/execute/route.test.ts | 10 +- .../app/api/workflows/[id]/execute/route.ts | 25 +- apps/sim/app/api/workflows/[id]/route.ts | 65 +- apps/sim/app/chat/[subdomain]/chat-client.tsx | 2 +- .../components/panel/components/chat/chat.tsx | 16 +- .../output-select/output-select.tsx | 42 +- .../components/toolbar/toolbar.tsx | 12 +- .../connection-blocks/connection-blocks.tsx | 166 +- .../workflow-block/workflow-block.tsx | 5 +- .../hooks/use-block-connections.ts | 60 +- .../hooks/use-workflow-execution.ts | 31 +- .../components/general/general.tsx | 38 + apps/sim/blocks/blocks/agent.ts | 24 +- apps/sim/blocks/blocks/airtable.ts | 10 +- apps/sim/blocks/blocks/api.ts | 10 +- apps/sim/blocks/blocks/autoblocks.ts | 12 +- apps/sim/blocks/blocks/browser_use.ts | 12 +- apps/sim/blocks/blocks/clay.ts | 6 +- apps/sim/blocks/blocks/condition.ts | 12 +- apps/sim/blocks/blocks/confluence.ts | 14 +- apps/sim/blocks/blocks/discord.ts | 8 +- apps/sim/blocks/blocks/elevenlabs.ts | 6 +- apps/sim/blocks/blocks/evaluator.ts | 26 +- apps/sim/blocks/blocks/exa.ts | 18 +- apps/sim/blocks/blocks/file.ts | 8 +- apps/sim/blocks/blocks/firecrawl.ts | 18 +- apps/sim/blocks/blocks/function.ts | 8 +- apps/sim/blocks/blocks/github.ts | 8 +- apps/sim/blocks/blocks/gmail.ts | 8 +- apps/sim/blocks/blocks/google.ts | 8 +- apps/sim/blocks/blocks/google_calendar.ts | 8 +- apps/sim/blocks/blocks/google_docs.ts | 10 +- apps/sim/blocks/blocks/google_drive.ts | 8 +- apps/sim/blocks/blocks/google_sheets.ts | 18 +- apps/sim/blocks/blocks/guesty.ts | 20 +- apps/sim/blocks/blocks/huggingface.ts | 10 +- apps/sim/blocks/blocks/image_generator.ts | 10 +- apps/sim/blocks/blocks/jina.ts | 6 +- apps/sim/blocks/blocks/jira.ts | 20 +- apps/sim/blocks/blocks/knowledge.ts | 10 +- apps/sim/blocks/blocks/linear.ts | 8 +- apps/sim/blocks/blocks/linkup.ts | 8 +- apps/sim/blocks/blocks/mem0.ts | 10 +- apps/sim/blocks/blocks/memory.ts | 8 +- apps/sim/blocks/blocks/microsoft_excel.ts | 20 +- apps/sim/blocks/blocks/microsoft_teams.ts | 10 +- apps/sim/blocks/blocks/mistral_parse.ts | 8 +- apps/sim/blocks/blocks/notion.ts | 8 +- apps/sim/blocks/blocks/openai.ts | 10 +- apps/sim/blocks/blocks/outlook.ts | 8 +- apps/sim/blocks/blocks/perplexity.ts | 10 +- apps/sim/blocks/blocks/pinecone.ts | 16 +- apps/sim/blocks/blocks/reddit.ts | 12 +- apps/sim/blocks/blocks/response.ts | 10 +- apps/sim/blocks/blocks/router.ts | 14 +- apps/sim/blocks/blocks/s3.ts | 8 +- apps/sim/blocks/blocks/serper.ts | 6 +- apps/sim/blocks/blocks/slack.ts | 8 +- apps/sim/blocks/blocks/stagehand.ts | 6 +- apps/sim/blocks/blocks/stagehand_agent.ts | 8 +- apps/sim/blocks/blocks/starter.ts | 17 +- apps/sim/blocks/blocks/supabase.ts | 8 +- apps/sim/blocks/blocks/tavily.ts | 16 +- apps/sim/blocks/blocks/telegram.ts | 8 +- apps/sim/blocks/blocks/thinking.ts | 6 +- apps/sim/blocks/blocks/translate.ts | 10 +- apps/sim/blocks/blocks/twilio.ts | 12 +- apps/sim/blocks/blocks/typeform.ts | 21 +- apps/sim/blocks/blocks/vision.ts | 10 +- apps/sim/blocks/blocks/whatsapp.ts | 10 +- apps/sim/blocks/blocks/workflow.ts | 14 +- apps/sim/blocks/blocks/x.ts | 20 +- apps/sim/blocks/blocks/youtube.ts | 8 +- apps/sim/blocks/types.ts | 25 +- apps/sim/blocks/utils.ts | 45 +- apps/sim/components/ui/tag-dropdown.test.tsx | 106 +- apps/sim/components/ui/tag-dropdown.tsx | 698 ++- apps/sim/contexts/socket-context.tsx | 1 - .../migrations/0050_big_mattie_franklin.sql | 1 + .../sim/db/migrations/meta/0050_snapshot.json | 4468 +++++++++++++++++ apps/sim/db/migrations/meta/_journal.json | 7 + apps/sim/db/schema.ts | 1 + .../executor/__test-utils__/executor-mocks.ts | 42 +- .../executor/__test-utils__/test-executor.ts | 4 +- .../handlers/agent/agent-handler.test.ts | 105 +- .../executor/handlers/agent/agent-handler.ts | 24 +- .../executor/handlers/api/api-handler.test.ts | 4 +- apps/sim/executor/handlers/api/api-handler.ts | 7 +- .../condition/condition-handler.test.ts | 58 +- .../handlers/condition/condition-handler.ts | 16 +- .../evaluator/evaluator-handler.test.ts | 43 +- .../handlers/evaluator/evaluator-handler.ts | 26 +- .../function/function-handler.test.ts | 5 +- .../handlers/function/function-handler.ts | 5 +- .../handlers/generic/generic-handler.test.ts | 3 +- .../handlers/generic/generic-handler.ts | 5 +- .../handlers/loop/loop-handler.test.ts | 20 +- .../executor/handlers/loop/loop-handler.ts | 32 +- .../parallel/parallel-handler.test.ts | 55 +- .../handlers/parallel/parallel-handler.ts | 62 +- .../handlers/router/router-handler.test.ts | 35 +- .../handlers/router/router-handler.ts | 34 +- .../workflow/workflow-handler.test.ts | 28 +- .../handlers/workflow/workflow-handler.ts | 48 +- apps/sim/executor/index.test.ts | 105 +- apps/sim/executor/index.ts | 288 +- apps/sim/executor/loops.test.ts | 39 +- apps/sim/executor/loops.ts | 19 +- apps/sim/executor/parallels.test.ts | 2 +- apps/sim/executor/parallels.ts | 2 +- apps/sim/executor/path.test.ts | 60 +- apps/sim/executor/path.ts | 8 +- apps/sim/executor/resolver.test.ts | 667 ++- apps/sim/executor/resolver.ts | 217 +- apps/sim/executor/types.ts | 60 +- apps/sim/hooks/use-collaborative-workflow.ts | 2 +- apps/sim/lib/auth/internal.ts | 47 + apps/sim/lib/block-path-calculator.ts | 136 + apps/sim/lib/env.ts | 1 + apps/sim/lib/logs/execution-logger.ts | 211 +- apps/sim/lib/logs/trace-spans.ts | 28 +- apps/sim/lib/workflows/db-helpers.test.ts | 6 +- apps/sim/package.json | 1 + apps/sim/providers/anthropic/index.ts | 106 +- apps/sim/providers/azure-openai/index.ts | 108 +- apps/sim/providers/cerebras/index.ts | 106 +- apps/sim/providers/deepseek/index.ts | 106 +- apps/sim/providers/google/index.ts | 108 +- apps/sim/providers/groq/index.ts | 104 +- apps/sim/providers/ollama/index.ts | 4 - apps/sim/providers/openai/index.ts | 108 +- apps/sim/providers/xai/index.ts | 106 +- apps/sim/stores/execution/store.ts | 61 +- apps/sim/stores/execution/types.ts | 7 + apps/sim/stores/index.ts | 2 - apps/sim/stores/panel/console/store.test.ts | 34 +- apps/sim/stores/panel/console/store.ts | 14 +- apps/sim/stores/settings/general/store.ts | 8 + apps/sim/stores/settings/general/types.ts | 3 + apps/sim/stores/workflows/registry/store.ts | 2 - apps/sim/stores/workflows/workflow/store.ts | 2 +- .../stores/workflows/workflow/utils.test.ts | 4 +- apps/sim/tools/index.ts | 135 +- apps/sim/tsconfig.json | 4 +- apps/sim/vitest.setup.ts | 1 - bun.lock | 7 +- 157 files changed, 7708 insertions(+), 2876 deletions(-) create mode 100644 apps/sim/db/migrations/0050_big_mattie_franklin.sql create mode 100644 apps/sim/db/migrations/meta/0050_snapshot.json create mode 100644 apps/sim/lib/auth/internal.ts create mode 100644 apps/sim/lib/block-path-calculator.ts diff --git a/apps/docs/content/docs/blocks/workflow.mdx b/apps/docs/content/docs/blocks/workflow.mdx index f45e0ce41..3724cb7e4 100644 --- a/apps/docs/content/docs/blocks/workflow.mdx +++ b/apps/docs/content/docs/blocks/workflow.mdx @@ -66,17 +66,17 @@ Define the data to pass to the child workflow: - **Single Variable Input**: Select a variable or block output to pass to the child workflow - **Variable References**: Use `` to reference workflow variables -- **Block References**: Use `` to reference outputs from previous blocks -- **Automatic Mapping**: The selected data is automatically available as `start.response.input` in the child workflow +- **Block References**: Use `` to reference outputs from previous blocks +- **Automatic Mapping**: The selected data is automatically available as `start.input` in the child workflow - **Optional**: The input field is optional - child workflows can run without input data - **Type Preservation**: Variable types (strings, numbers, objects, etc.) are preserved when passed to the child workflow ### Examples of Input References - `` - Pass a workflow variable -- `` - Pass the result from a previous block -- `` - Pass the original workflow input -- `` - Pass a specific field from an API response +- `` - Pass the result from a previous block +- `` - Pass the original workflow input +- `` - Pass a specific field from an API response ### Execution Context @@ -109,7 +109,7 @@ To prevent infinite recursion and ensure system stability, the Workflow block in Workflow ID: The identifier of the workflow to execute
  • - Input Variable: Variable or block reference to pass to the child workflow (e.g., `` or ``) + Input Variable: Variable or block reference to pass to the child workflow (e.g., `` or ``)
  • @@ -150,23 +150,23 @@ blocks: - type: workflow name: "Setup Customer Account" workflowId: "account-setup-workflow" - input: "" + input: "" - type: workflow name: "Send Welcome Email" workflowId: "welcome-email-workflow" - input: "" + input: "" ``` ### Child Workflow: Customer Validation ```yaml # Reusable customer validation workflow -# Access the input data using: start.response.input +# Access the input data using: start.input blocks: - type: function name: "Validate Email" code: | - const customerData = start.response.input; + const customerData = start.input; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(customerData.email); @@ -174,7 +174,7 @@ blocks: name: "Check Credit Score" url: "https://api.creditcheck.com/score" method: "POST" - body: "" + body: "" ``` ### Variable Reference Examples @@ -184,13 +184,13 @@ blocks: input: "" # Using block outputs -input: "" + input: "" # Using nested object properties -input: "" + input: "" # Using array elements (if supported by the resolver) -input: "" + input: "" ``` ## Access Control and Permissions diff --git a/apps/sim/app/api/__test-utils__/utils.ts b/apps/sim/app/api/__test-utils__/utils.ts index 85f8bbcc0..a86421d61 100644 --- a/apps/sim/app/api/__test-utils__/utils.ts +++ b/apps/sim/app/api/__test-utils__/utils.ts @@ -93,7 +93,7 @@ export const sampleWorkflowState = { webhookPath: { id: 'webhookPath', type: 'short-input', value: '' }, }, outputs: { - response: { type: { input: 'any' } }, + input: 'any', }, enabled: true, horizontalHandles: true, @@ -111,7 +111,7 @@ export const sampleWorkflowState = { type: 'long-input', value: 'You are a helpful assistant', }, - context: { id: 'context', type: 'short-input', value: '' }, + context: { id: 'context', type: 'short-input', value: '' }, model: { id: 'model', type: 'dropdown', value: 'gpt-4o' }, apiKey: { id: 'apiKey', type: 'short-input', value: '{{OPENAI_API_KEY}}' }, }, @@ -138,6 +138,7 @@ export const sampleWorkflowState = { }, ], loops: {}, + parallels: {}, lastSaved: Date.now(), isDeployed: false, } diff --git a/apps/sim/app/api/chat/[subdomain]/route.test.ts b/apps/sim/app/api/chat/[subdomain]/route.test.ts index 6837b1a0a..538c28428 100644 --- a/apps/sim/app/api/chat/[subdomain]/route.test.ts +++ b/apps/sim/app/api/chat/[subdomain]/route.test.ts @@ -241,7 +241,7 @@ describe('Chat Subdomain API Route', () => { }) describe('POST endpoint', () => { - it('should handle authentication requests without messages', async () => { + it('should handle authentication requests without input', async () => { const req = createMockRequest('POST', { password: 'test-password' }) const params = Promise.resolve({ subdomain: 'password-protected-chat' }) @@ -257,7 +257,7 @@ describe('Chat Subdomain API Route', () => { expect(mockSetChatAuthCookie).toHaveBeenCalled() }) - it('should return 400 for requests without message', async () => { + it('should return 400 for requests without input', async () => { const req = createMockRequest('POST', {}) const params = Promise.resolve({ subdomain: 'test-chat' }) @@ -269,7 +269,7 @@ describe('Chat Subdomain API Route', () => { const data = await response.json() expect(data).toHaveProperty('error') - expect(data).toHaveProperty('message', 'No message provided') + expect(data).toHaveProperty('message', 'No input provided') }) it('should return 401 for unauthorized access', async () => { @@ -279,7 +279,7 @@ describe('Chat Subdomain API Route', () => { error: 'Authentication required', })) - const req = createMockRequest('POST', { message: 'Hello' }) + const req = createMockRequest('POST', { input: 'Hello' }) const params = Promise.resolve({ subdomain: 'protected-chat' }) const { POST } = await import('./route') @@ -342,7 +342,7 @@ describe('Chat Subdomain API Route', () => { } }) - const req = createMockRequest('POST', { message: 'Hello' }) + const req = createMockRequest('POST', { input: 'Hello' }) const params = Promise.resolve({ subdomain: 'test-chat' }) const { POST } = await import('./route') @@ -357,7 +357,7 @@ describe('Chat Subdomain API Route', () => { }) it('should return streaming response for valid chat messages', async () => { - const req = createMockRequest('POST', { message: 'Hello world', conversationId: 'conv-123' }) + const req = createMockRequest('POST', { input: 'Hello world', conversationId: 'conv-123' }) const params = Promise.resolve({ subdomain: 'test-chat' }) const { POST } = await import('./route') @@ -374,7 +374,7 @@ describe('Chat Subdomain API Route', () => { }) it('should handle streaming response body correctly', async () => { - const req = createMockRequest('POST', { message: 'Hello world' }) + const req = createMockRequest('POST', { input: 'Hello world' }) const params = Promise.resolve({ subdomain: 'test-chat' }) const { POST } = await import('./route') @@ -404,7 +404,7 @@ describe('Chat Subdomain API Route', () => { throw new Error('Execution failed') }) - const req = createMockRequest('POST', { message: 'Trigger error' }) + const req = createMockRequest('POST', { input: 'Trigger error' }) const params = Promise.resolve({ subdomain: 'test-chat' }) const { POST } = await import('./route') @@ -444,7 +444,7 @@ describe('Chat Subdomain API Route', () => { it('should pass conversationId to executeWorkflowForChat when provided', async () => { const req = createMockRequest('POST', { - message: 'Hello world', + input: 'Hello world', conversationId: 'test-conversation-123', }) const params = Promise.resolve({ subdomain: 'test-chat' }) @@ -461,7 +461,7 @@ describe('Chat Subdomain API Route', () => { }) it('should handle missing conversationId gracefully', async () => { - const req = createMockRequest('POST', { message: 'Hello world' }) + const req = createMockRequest('POST', { input: 'Hello world' }) const params = Promise.resolve({ subdomain: 'test-chat' }) const { POST } = await import('./route') diff --git a/apps/sim/app/api/chat/[subdomain]/route.ts b/apps/sim/app/api/chat/[subdomain]/route.ts index 80d00861e..325629420 100644 --- a/apps/sim/app/api/chat/[subdomain]/route.ts +++ b/apps/sim/app/api/chat/[subdomain]/route.ts @@ -72,11 +72,11 @@ export async function POST( } // Use the already parsed body - const { message, password, email, conversationId } = parsedBody + const { input, password, email, conversationId } = parsedBody - // If this is an authentication request (has password or email but no message), + // If this is an authentication request (has password or email but no input), // set auth cookie and return success - if ((password || email) && !message) { + if ((password || email) && !input) { const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request) // Set authentication cookie @@ -86,8 +86,8 @@ export async function POST( } // For chat messages, create regular response - if (!message) { - return addCorsHeaders(createErrorResponse('No message provided', 400), request) + if (!input) { + return addCorsHeaders(createErrorResponse('No input provided', 400), request) } // Get the workflow for this chat @@ -105,8 +105,8 @@ export async function POST( } try { - // Execute workflow with structured input (message + conversationId for context) - const result = await executeWorkflowForChat(deployment.id, message, conversationId) + // Execute workflow with structured input (input + conversationId for context) + const result = await executeWorkflowForChat(deployment.id, input, conversationId) // The result is always a ReadableStream that we can pipe to the client const streamResponse = new NextResponse(result, { diff --git a/apps/sim/app/api/chat/utils.ts b/apps/sim/app/api/chat/utils.ts index 038a3d794..dbf50d541 100644 --- a/apps/sim/app/api/chat/utils.ts +++ b/apps/sim/app/api/chat/utils.ts @@ -128,10 +128,10 @@ export async function validateChatAuth( return { authorized: false, error: 'Password is required' } } - const { password, message } = parsedBody + const { password, input } = parsedBody // If this is a chat message, not an auth attempt - if (message && !password) { + if (input && !password) { return { authorized: false, error: 'auth_required_password' } } @@ -170,10 +170,10 @@ export async function validateChatAuth( return { authorized: false, error: 'Email is required' } } - const { email, message } = parsedBody + const { email, input } = parsedBody // If this is a chat message, not an auth attempt - if (message && !email) { + if (input && !email) { return { authorized: false, error: 'auth_required_email' } } @@ -211,17 +211,17 @@ export async function validateChatAuth( /** * Executes a workflow for a chat request and returns the formatted output. * - * When workflows reference , they receive a structured JSON - * containing both the message and conversationId for maintaining chat context. + * When workflows reference , they receive the input directly. + * The conversationId is available at for maintaining chat context. * * @param chatId - Chat deployment identifier - * @param message - User's chat message + * @param input - User's chat input * @param conversationId - Optional ID for maintaining conversation context * @returns Workflow execution result formatted for the chat interface */ export async function executeWorkflowForChat( chatId: string, - message: string, + input: string, conversationId?: string ): Promise { const requestId = crypto.randomUUID().slice(0, 8) @@ -445,7 +445,7 @@ export async function executeWorkflowForChat( workflow: serializedWorkflow, currentBlockStates: processedBlockStates, envVarValues: decryptedEnvVars, - workflowInput: { input: message, conversationId }, + workflowInput: { input: input, conversationId }, workflowVariables, contextExtensions: { stream: true, @@ -463,8 +463,8 @@ export async function executeWorkflowForChat( if (result && 'success' in result) { result.logs?.forEach((log: BlockLog) => { if (streamedContent.has(log.blockId)) { - if (log.output?.response) { - log.output.response.content = streamedContent.get(log.blockId) + if (log.output) { + log.output.content = streamedContent.get(log.blockId) } } }) diff --git a/apps/sim/app/api/codegen/route.ts b/apps/sim/app/api/codegen/route.ts index 49aabdaec..71e8ff6f8 100644 --- a/apps/sim/app/api/codegen/route.ts +++ b/apps/sim/app/api/codegen/route.ts @@ -239,7 +239,7 @@ Example Scenario: User Prompt: "Fetch user data from an API. Use the User ID passed in as 'userId' and an API Key stored as the 'SERVICE_API_KEY' environment variable." Generated Code: -const userId = ; // Correct: Accessing input parameter without quotes +const userId = ; // Correct: Accessing input parameter without quotes const apiKey = {{SERVICE_API_KEY}}; // Correct: Accessing environment variable without quotes const url = \`https://api.example.com/users/\${userId}\`; @@ -273,7 +273,7 @@ Do not include import/require statements unless absolutely necessary and they ar Do not include markdown formatting or explanations. Output only the raw TypeScript code. Use modern TypeScript features where appropriate. Do not use semicolons. Example: -const userId = as string +const userId = as string const apiKey = {{SERVICE_API_KEY}} const response = await fetch(\`https://api.example.com/users/\${userId}\`, { headers: { Authorization: \`Bearer \${apiKey}\` } }) if (!response.ok) { diff --git a/apps/sim/app/api/providers/route.ts b/apps/sim/app/api/providers/route.ts index 5f116bc9c..f0962d1ba 100644 --- a/apps/sim/app/api/providers/route.ts +++ b/apps/sim/app/api/providers/route.ts @@ -137,24 +137,22 @@ export async function POST(request: NextRequest) { const safeExecutionData = { success: executionData.success, output: { - response: { - // Sanitize content to remove non-ASCII characters that would cause ByteString errors - content: executionData.output?.response?.content - ? String(executionData.output.response.content).replace(/[\u0080-\uFFFF]/g, '') - : '', - model: executionData.output?.response?.model, - tokens: executionData.output?.response?.tokens || { - prompt: 0, - completion: 0, - total: 0, - }, - // Sanitize any potential Unicode characters in tool calls - toolCalls: executionData.output?.response?.toolCalls - ? sanitizeToolCalls(executionData.output.response.toolCalls) - : undefined, - providerTiming: executionData.output?.response?.providerTiming, - cost: executionData.output?.response?.cost, + // Sanitize content to remove non-ASCII characters that would cause ByteString errors + content: executionData.output?.content + ? String(executionData.output.content).replace(/[\u0080-\uFFFF]/g, '') + : '', + model: executionData.output?.model, + tokens: executionData.output?.tokens || { + prompt: 0, + completion: 0, + total: 0, }, + // Sanitize any potential Unicode characters in tool calls + toolCalls: executionData.output?.toolCalls + ? sanitizeToolCalls(executionData.output.toolCalls) + : undefined, + providerTiming: executionData.output?.providerTiming, + cost: executionData.output?.cost, }, error: executionData.error, logs: [], // Strip logs from header to avoid encoding issues diff --git a/apps/sim/app/api/proxy/route.ts b/apps/sim/app/api/proxy/route.ts index f58092a18..2ea7e3d7f 100644 --- a/apps/sim/app/api/proxy/route.ts +++ b/apps/sim/app/api/proxy/route.ts @@ -46,11 +46,19 @@ const formatResponse = (responseData: any, status = 200) => { */ const createErrorResponse = (error: any, status = 500, additionalData = {}) => { const errorMessage = error instanceof Error ? error.message : String(error) + const errorStack = error instanceof Error ? error.stack : undefined + + logger.error('Creating error response', { + errorMessage, + status, + stack: process.env.NODE_ENV === 'development' ? errorStack : undefined, + }) return formatResponse( { success: false, error: errorMessage, + stack: process.env.NODE_ENV === 'development' ? errorStack : undefined, ...additionalData, }, status @@ -67,6 +75,7 @@ export async function GET(request: Request) { const requestId = crypto.randomUUID().slice(0, 8) if (!targetUrl) { + logger.error(`[${requestId}] Missing 'url' parameter`) return createErrorResponse("Missing 'url' parameter", 400) } @@ -126,6 +135,10 @@ export async function GET(request: Request) { : response.statusText || `HTTP error ${response.status}` : undefined + if (!response.ok) { + logger.error(`[${requestId}] External API error: ${response.status} ${response.statusText}`) + } + // Return the proxied response return formatResponse({ success: response.ok, @@ -139,6 +152,7 @@ export async function GET(request: Request) { logger.error(`[${requestId}] Proxy GET request failed`, { url: targetUrl, error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, }) return createErrorResponse(error) @@ -151,22 +165,40 @@ export async function POST(request: Request) { const startTimeISO = startTime.toISOString() try { - const { toolId, params } = await request.json() + // Parse request body + let requestBody + try { + requestBody = await request.json() + } catch (parseError) { + logger.error(`[${requestId}] Failed to parse request body`, { + error: parseError instanceof Error ? parseError.message : String(parseError), + }) + throw new Error('Invalid JSON in request body') + } - logger.debug(`[${requestId}] Proxy request for tool`, { - toolId, - hasParams: !!params && Object.keys(params).length > 0, - }) + const { toolId, params } = requestBody + if (!toolId) { + logger.error(`[${requestId}] Missing toolId in request`) + throw new Error('Missing toolId in request') + } + + logger.info(`[${requestId}] Processing tool: ${toolId}`) + + // Get tool const tool = getTool(toolId) + if (!tool) { + logger.error(`[${requestId}] Tool not found: ${toolId}`) + throw new Error(`Tool not found: ${toolId}`) + } + // Validate the tool and its parameters try { validateToolRequest(toolId, tool, params) - } catch (error) { - logger.warn(`[${requestId}] Tool validation failed`, { - toolId, - error: error instanceof Error ? error.message : String(error), + } catch (validationError) { + logger.warn(`[${requestId}] Tool validation failed for ${toolId}`, { + error: validationError instanceof Error ? validationError.message : String(validationError), }) // Add timing information even to error responses @@ -174,23 +206,18 @@ export async function POST(request: Request) { const endTimeISO = endTime.toISOString() const duration = endTime.getTime() - startTime.getTime() - return createErrorResponse(error, 400, { + return createErrorResponse(validationError, 400, { startTime: startTimeISO, endTime: endTimeISO, duration, }) } - if (!tool) { - logger.error(`[${requestId}] Tool not found`, { toolId }) - throw new Error(`Tool not found: ${toolId}`) - } - // Use executeTool with skipProxy=true to prevent recursive proxy calls, and skipPostProcess=true to prevent duplicate post-processing + // Execute tool const result = await executeTool(toolId, params, true, true) if (!result.success) { - logger.warn(`[${requestId}] Tool execution failed`, { - toolId, + logger.warn(`[${requestId}] Tool execution failed for ${toolId}`, { error: result.error || 'Unknown error', }) @@ -217,9 +244,13 @@ export async function POST(request: Request) { } // Fallback throw new Error('Tool returned an error') - } catch (e) { - if (e instanceof Error) { - throw e + } catch (transformError) { + logger.error(`[${requestId}] Error transformation failed for ${toolId}`, { + error: + transformError instanceof Error ? transformError.message : String(transformError), + }) + if (transformError instanceof Error) { + throw transformError } throw new Error('Tool returned an error') } @@ -246,12 +277,7 @@ export async function POST(request: Request) { }, } - logger.info(`[${requestId}] Tool executed successfully`, { - toolId, - duration, - startTime: startTimeISO, - endTime: endTimeISO, - }) + logger.info(`[${requestId}] Tool executed successfully: ${toolId} (${duration}ms)`) // Return the response with CORS headers return formatResponse(responseWithTimingData) @@ -259,6 +285,7 @@ export async function POST(request: Request) { logger.error(`[${requestId}] Proxy request failed`, { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, + name: error instanceof Error ? error.name : undefined, }) // Add timing information even to error responses diff --git a/apps/sim/app/api/user/settings/route.ts b/apps/sim/app/api/user/settings/route.ts index 5204aa730..3515f9afa 100644 --- a/apps/sim/app/api/user/settings/route.ts +++ b/apps/sim/app/api/user/settings/route.ts @@ -14,6 +14,7 @@ const SettingsSchema = z.object({ debugMode: z.boolean().optional(), autoConnect: z.boolean().optional(), autoFillEnvVars: z.boolean().optional(), + autoPan: z.boolean().optional(), telemetryEnabled: z.boolean().optional(), telemetryNotifiedUser: z.boolean().optional(), emailPreferences: z @@ -32,6 +33,7 @@ const defaultSettings = { debugMode: false, autoConnect: true, autoFillEnvVars: true, + autoPan: true, telemetryEnabled: true, telemetryNotifiedUser: false, emailPreferences: {}, @@ -65,6 +67,7 @@ export async function GET() { debugMode: userSettings.debugMode, autoConnect: userSettings.autoConnect, autoFillEnvVars: userSettings.autoFillEnvVars, + autoPan: userSettings.autoPan, telemetryEnabled: userSettings.telemetryEnabled, telemetryNotifiedUser: userSettings.telemetryNotifiedUser, emailPreferences: userSettings.emailPreferences ?? {}, diff --git a/apps/sim/app/api/workflows/[id]/deploy/route.test.ts b/apps/sim/app/api/workflows/[id]/deploy/route.test.ts index a0a6fc602..ce1765e07 100644 --- a/apps/sim/app/api/workflows/[id]/deploy/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/deploy/route.test.ts @@ -31,6 +31,27 @@ describe('Workflow Deployment API Route', () => { }), })) + // Mock serializer + vi.doMock('@/serializer', () => ({ + serializeWorkflow: vi.fn().mockReturnValue({ + version: '1.0', + blocks: [ + { + id: 'block-1', + metadata: { id: 'starter', name: 'Start' }, + position: { x: 100, y: 100 }, + config: { tool: 'starter', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + ], + connections: [], + loops: {}, + parallels: {}, + }), + })) + vi.doMock('@/lib/workflows/db-helpers', () => ({ loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue({ blocks: { @@ -75,6 +96,80 @@ describe('Workflow Deployment API Route', () => { }) }), })) + + // Mock the database schema module + vi.doMock('@/db/schema', () => ({ + workflow: {}, + apiKey: {}, + workflowBlocks: {}, + workflowEdges: {}, + workflowSubflows: {}, + })) + + // Mock drizzle-orm operators + vi.doMock('drizzle-orm', () => ({ + eq: vi.fn((field, value) => ({ field, value, type: 'eq' })), + and: vi.fn((...conditions) => ({ conditions, type: 'and' })), + })) + + // Mock the database module with proper chainable query builder + let selectCallCount = 0 + vi.doMock('@/db', () => ({ + db: { + select: vi.fn().mockImplementation(() => { + selectCallCount++ + return { + from: vi.fn().mockImplementation(() => ({ + where: vi.fn().mockImplementation(() => ({ + limit: vi.fn().mockImplementation(() => { + // First call: workflow lookup (should return workflow) + if (selectCallCount === 1) { + return Promise.resolve([{ userId: 'user-id', id: 'workflow-id' }]) + } + // Second call: blocks lookup + if (selectCallCount === 2) { + return Promise.resolve([ + { + id: 'block-1', + type: 'starter', + name: 'Start', + positionX: '100', + positionY: '100', + enabled: true, + subBlocks: {}, + data: {}, + }, + ]) + } + // Third call: edges lookup + if (selectCallCount === 3) { + return Promise.resolve([]) + } + // Fourth call: subflows lookup + if (selectCallCount === 4) { + return Promise.resolve([]) + } + // Fifth call: API key lookup (should return empty for new key test) + if (selectCallCount === 5) { + return Promise.resolve([]) + } + // Default: empty array + return Promise.resolve([]) + }), + })), + })), + } + }), + insert: vi.fn().mockImplementation(() => ({ + values: vi.fn().mockResolvedValue([{ id: 'mock-api-key-id' }]), + })), + update: vi.fn().mockImplementation(() => ({ + set: vi.fn().mockImplementation(() => ({ + where: vi.fn().mockResolvedValue([]), + })), + })), + }, + })) }) afterEach(() => { @@ -126,16 +221,7 @@ describe('Workflow Deployment API Route', () => { * This should generate a new API key */ it('should create new API key when deploying workflow for user with no API key', async () => { - const mockInsert = vi.fn().mockReturnValue({ - values: vi.fn().mockReturnValue(undefined), - }) - - const mockUpdate = vi.fn().mockReturnValue({ - set: vi.fn().mockReturnValue({ - where: vi.fn().mockResolvedValue([{ id: 'workflow-id' }]), - }), - }) - + // Override the global mock for this specific test vi.doMock('@/db', () => ({ db: { select: vi @@ -143,11 +229,7 @@ describe('Workflow Deployment API Route', () => { .mockReturnValueOnce({ from: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ - limit: vi.fn().mockResolvedValue([ - { - userId: 'user-id', - }, - ]), + limit: vi.fn().mockResolvedValue([{ userId: 'user-id', id: 'workflow-id' }]), }), }), }) @@ -184,8 +266,14 @@ describe('Workflow Deployment API Route', () => { }), }), }), - insert: mockInsert, - update: mockUpdate, + insert: vi.fn().mockImplementation(() => ({ + values: vi.fn().mockResolvedValue([{ id: 'mock-api-key-id' }]), + })), + update: vi.fn().mockImplementation(() => ({ + set: vi.fn().mockImplementation(() => ({ + where: vi.fn().mockResolvedValue([]), + })), + })), }, })) @@ -204,9 +292,6 @@ describe('Workflow Deployment API Route', () => { expect(data).toHaveProperty('apiKey', 'sim_testkeygenerated12345') expect(data).toHaveProperty('isDeployed', true) expect(data).toHaveProperty('deployedAt') - - expect(mockInsert).toHaveBeenCalled() - expect(mockUpdate).toHaveBeenCalled() }) /** @@ -214,14 +299,7 @@ describe('Workflow Deployment API Route', () => { * This should use the existing API key */ it('should use existing API key when deploying workflow', async () => { - const mockInsert = vi.fn() - - const mockUpdate = vi.fn().mockReturnValue({ - set: vi.fn().mockReturnValue({ - where: vi.fn().mockResolvedValue([{ id: 'workflow-id' }]), - }), - }) - + // Override the global mock for this specific test vi.doMock('@/db', () => ({ db: { select: vi @@ -229,11 +307,7 @@ describe('Workflow Deployment API Route', () => { .mockReturnValueOnce({ from: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ - limit: vi.fn().mockResolvedValue([ - { - userId: 'user-id', - }, - ]), + limit: vi.fn().mockResolvedValue([{ userId: 'user-id', id: 'workflow-id' }]), }), }), }) @@ -266,16 +340,18 @@ describe('Workflow Deployment API Route', () => { .mockReturnValueOnce({ from: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ - limit: vi.fn().mockResolvedValue([ - { - key: 'sim_existingtestapikey12345', - }, - ]), // Existing API key + limit: vi.fn().mockResolvedValue([{ key: 'sim_existingtestapikey12345' }]), // Existing API key }), }), }), - insert: mockInsert, - update: mockUpdate, + insert: vi.fn().mockImplementation(() => ({ + values: vi.fn().mockResolvedValue([{ id: 'mock-api-key-id' }]), + })), + update: vi.fn().mockImplementation(() => ({ + set: vi.fn().mockImplementation(() => ({ + where: vi.fn().mockResolvedValue([]), + })), + })), }, })) @@ -293,9 +369,6 @@ describe('Workflow Deployment API Route', () => { expect(data).toHaveProperty('apiKey', 'sim_existingtestapikey12345') expect(data).toHaveProperty('isDeployed', true) - - expect(mockInsert).not.toHaveBeenCalled() - expect(mockUpdate).toHaveBeenCalled() }) /** diff --git a/apps/sim/app/api/workflows/[id]/deploy/route.ts b/apps/sim/app/api/workflows/[id]/deploy/route.ts index 449e86b70..3ea517f14 100644 --- a/apps/sim/app/api/workflows/[id]/deploy/route.ts +++ b/apps/sim/app/api/workflows/[id]/deploy/route.ts @@ -139,7 +139,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return createErrorResponse(validation.error.message, validation.error.status) } - // Get the workflow to find the user (removed deprecated state column) + // Get the workflow to find the user const workflowData = await db .select({ userId: workflow.userId, diff --git a/apps/sim/app/api/workflows/[id]/execute/route.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.test.ts index 07805d36b..dce39e5ba 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.test.ts @@ -246,10 +246,7 @@ describe('Workflow Execution API Route', () => { expect.anything(), // serializedWorkflow expect.anything(), // processedBlockStates expect.anything(), // decryptedEnvVars - expect.objectContaining({ - // processedInput - input: requestBody, - }), + requestBody, // processedInput (direct input, not wrapped) expect.anything() // workflowVariables ) }) @@ -285,10 +282,7 @@ describe('Workflow Execution API Route', () => { expect.anything(), // serializedWorkflow expect.anything(), // processedBlockStates expect.anything(), // decryptedEnvVars - expect.objectContaining({ - // processedInput - input: structuredInput, - }), + structuredInput, // processedInput (direct input, not wrapped) expect.anything() // workflowVariables ) }) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 5c8879a07..119dd23e5 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -77,19 +77,12 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) { input ? JSON.stringify(input, null, 2) : 'No input provided' ) - // Validate and structure input for maximum compatibility - let processedInput = input - if (input && typeof input === 'object') { - // Ensure input is properly structured for the starter block - if (input.input === undefined) { - // If input is not already nested, structure it properly - processedInput = { input: input } - logger.info( - `[${requestId}] Restructured input for workflow:`, - JSON.stringify(processedInput, null, 2) - ) - } - } + // Use input directly for API workflows + const processedInput = input + logger.info( + `[${requestId}] Using input directly for workflow:`, + JSON.stringify(processedInput, null, 2) + ) try { runningExecutions.add(executionKey) @@ -381,13 +374,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ logger.info(`[${requestId}] No request body provided`) } - // Don't double-nest the input if it's already structured + // Pass the raw body directly as input for API workflows const hasContent = Object.keys(body).length > 0 - const input = hasContent ? { input: body } : {} + const input = hasContent ? body : {} logger.info(`[${requestId}] Input passed to workflow:`, JSON.stringify(input, null, 2)) - // Execute workflow with the structured input + // Execute workflow with the raw input const result = await executeWorkflow(validation.workflow, requestId, input) // Check if the workflow execution contains a response block output diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index fd0286faa..fb7cc0c82 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' +import { verifyInternalToken } from '@/lib/auth/internal' import { createLogger } from '@/lib/logs/console-logger' import { getUserEntityPermissions, hasAdminPermission } from '@/lib/permissions/utils' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers' @@ -28,14 +29,29 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const { id: workflowId } = await params try { - // Get the session - const session = await getSession() - if (!session?.user?.id) { - logger.warn(`[${requestId}] Unauthorized access attempt for workflow ${workflowId}`) - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + // Check for internal JWT token for server-side calls + const authHeader = request.headers.get('authorization') + let isInternalCall = false + + if (authHeader?.startsWith('Bearer ')) { + const token = authHeader.split(' ')[1] + isInternalCall = await verifyInternalToken(token) } - const userId = session.user.id + let userId: string | null = null + + if (isInternalCall) { + // For internal calls, we'll skip user-specific access checks + logger.info(`[${requestId}] Internal API call for workflow ${workflowId}`) + } else { + // Get the session for regular user calls + const session = await getSession() + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthorized access attempt for workflow ${workflowId}`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + userId = session.user.id + } // Fetch the workflow const workflowData = await db @@ -52,26 +68,31 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ // Check if user has access to this workflow let hasAccess = false - // Case 1: User owns the workflow - if (workflowData.userId === userId) { + if (isInternalCall) { + // Internal calls have full access hasAccess = true - } - - // Case 2: Workflow belongs to a workspace the user has permissions for - if (!hasAccess && workflowData.workspaceId) { - const userPermission = await getUserEntityPermissions( - userId, - 'workspace', - workflowData.workspaceId - ) - if (userPermission !== null) { + } else { + // Case 1: User owns the workflow + if (workflowData.userId === userId) { hasAccess = true } - } - if (!hasAccess) { - logger.warn(`[${requestId}] User ${userId} denied access to workflow ${workflowId}`) - return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + // Case 2: Workflow belongs to a workspace the user has permissions for + if (!hasAccess && workflowData.workspaceId && userId) { + const userPermission = await getUserEntityPermissions( + userId, + 'workspace', + workflowData.workspaceId + ) + if (userPermission !== null) { + hasAccess = true + } + } + + if (!hasAccess) { + logger.warn(`[${requestId}] User ${userId} denied access to workflow ${workflowId}`) + return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + } } // Try to load from normalized tables first diff --git a/apps/sim/app/chat/[subdomain]/chat-client.tsx b/apps/sim/app/chat/[subdomain]/chat-client.tsx index 697bc00bd..23833e814 100644 --- a/apps/sim/app/chat/[subdomain]/chat-client.tsx +++ b/apps/sim/app/chat/[subdomain]/chat-client.tsx @@ -297,7 +297,7 @@ export default function ChatClient({ subdomain }: { subdomain: string }) { try { // Send structured payload to maintain chat context const payload = { - message: + input: typeof userMessage.content === 'string' ? userMessage.content : JSON.stringify(userMessage.content), diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx index 9bb91ad2a..6ce33a675 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx @@ -140,12 +140,20 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) { result.logs?.filter((log) => !messageIdMap.has(log.blockId)) || [] if (nonStreamingLogs.length > 0) { - const outputsToRender = selectedOutputs.filter((outputId) => - nonStreamingLogs.some((log) => log.blockId === outputId.split('.')[0]) - ) + const outputsToRender = selectedOutputs.filter((outputId) => { + // Extract block ID correctly - handle both formats: + // - "blockId" (direct block ID) + // - "blockId_response.result" (block ID with path) + const blockIdForOutput = outputId.includes('_') + ? outputId.split('_')[0] + : outputId.split('.')[0] + return nonStreamingLogs.some((log) => log.blockId === blockIdForOutput) + }) for (const outputId of outputsToRender) { - const blockIdForOutput = outputId.split('.')[0] + const blockIdForOutput = outputId.includes('_') + ? outputId.split('_')[0] + : outputId.split('.')[0] const path = outputId.substring(blockIdForOutput.length + 1) const log = nonStreamingLogs.find((l) => l.blockId === blockIdForOutput) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components/output-select/output-select.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components/output-select/output-select.tsx index c3642e0d3..71b24b790 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components/output-select/output-select.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/components/output-select/output-select.tsx @@ -53,13 +53,41 @@ export function OutputSelect({ const addOutput = (path: string, outputObj: any, prefix = '') => { const fullPath = prefix ? `${prefix}.${path}` : path - if (typeof outputObj === 'object' && outputObj !== null) { - // For objects, recursively add each property + // If not an object or is null, treat as leaf node + if (typeof outputObj !== 'object' || outputObj === null) { + const output = { + id: `${block.id}_${fullPath}`, + label: `${blockName}.${fullPath}`, + blockId: block.id, + blockName: block.name || `Block ${block.id}`, + blockType: block.type, + path: fullPath, + } + outputs.push(output) + return + } + + // If has 'type' property, treat as schema definition (leaf node) + if ('type' in outputObj && typeof outputObj.type === 'string') { + const output = { + id: `${block.id}_${fullPath}`, + label: `${blockName}.${fullPath}`, + blockId: block.id, + blockName: block.name || `Block ${block.id}`, + blockType: block.type, + path: fullPath, + } + outputs.push(output) + return + } + + // For objects without type, recursively add each property + if (!Array.isArray(outputObj)) { Object.entries(outputObj).forEach(([key, value]) => { addOutput(key, value, fullPath) }) } else { - // Add leaf node as output option + // For arrays, treat as leaf node outputs.push({ id: `${block.id}_${fullPath}`, label: `${blockName}.${fullPath}`, @@ -71,10 +99,10 @@ export function OutputSelect({ } } - // Start with the response object - if (block.outputs.response) { - addOutput('response', block.outputs.response) - } + // Process all output properties directly (flattened structure) + Object.entries(block.outputs).forEach(([key, value]) => { + addOutput(key, value) + }) } }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/toolbar.tsx index 177413e6e..d4558a038 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/toolbar/toolbar.tsx @@ -145,11 +145,13 @@ export const Toolbar = React.memo(() => { {blocks.map((block) => ( ))} - {activeTab === 'blocks' && !searchQuery && ( - <> - - - + {((activeTab === 'blocks' && !searchQuery) || + (searchQuery && 'loop'.includes(searchQuery.toLowerCase()))) && ( + + )} + {((activeTab === 'blocks' && !searchQuery) || + (searchQuery && 'parallel'.includes(searchQuery.toLowerCase()))) && ( + )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx index b9f17f570..740153f7b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/connection-blocks/connection-blocks.tsx @@ -4,10 +4,11 @@ import { type ConnectedBlock, useBlockConnections, } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections' -import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { getBlock } from '@/blocks' interface ConnectionBlocksProps { blockId: string + horizontalHandles: boolean setIsConnecting: (isConnecting: boolean) => void isDisabled?: boolean } @@ -20,6 +21,7 @@ interface ResponseField { export function ConnectionBlocks({ blockId, + horizontalHandles, setIsConnecting, isDisabled = false, }: ConnectionBlocksProps) { @@ -39,6 +41,10 @@ export function ConnectionBlocks({ e.stopPropagation() // Prevent parent drag handlers from firing setIsConnecting(true) + + // If no specific field is provided, use all available output types + const outputType = field ? field.name : connection.outputType + e.dataTransfer.setData( 'application/json', JSON.stringify({ @@ -46,9 +52,13 @@ export function ConnectionBlocks({ connectionData: { id: connection.id, name: connection.name, - outputType: field ? field.name : connection.outputType, + outputType: outputType, sourceBlockId: connection.id, fieldType: field?.type, + // Include all available output types for reference + allOutputTypes: Array.isArray(connection.outputType) + ? connection.outputType + : [connection.outputType], }, }) ) @@ -59,147 +69,59 @@ export function ConnectionBlocks({ setIsConnecting(false) } - // Helper function to extract fields from JSON Schema - const extractFieldsFromSchema = (connection: ConnectedBlock): ResponseField[] => { - // Handle legacy format with fields array - if (connection.responseFormat?.fields) { - return connection.responseFormat.fields - } - - // Handle new JSON Schema format - const schema = connection.responseFormat?.schema || connection.responseFormat - // Safely check if schema and properties exist - if ( - !schema || - typeof schema !== 'object' || - !('properties' in schema) || - typeof schema.properties !== 'object' - ) { - return [] - } - return Object.entries(schema.properties).map(([name, prop]: [string, any]) => ({ - name, - type: Array.isArray(prop) ? 'array' : prop.type || 'string', - description: prop.description, - })) - } - - // Extract fields from starter block input format - const extractFieldsFromStarterInput = (connection: ConnectedBlock): ResponseField[] => { - // Only process for starter blocks - if (connection.type !== 'starter') return [] - - try { - // Get input format from subblock store - const inputFormat = useSubBlockStore.getState().getValue(connection.id, 'inputFormat') - - // Make sure we have a valid input format - if (!inputFormat || !Array.isArray(inputFormat) || inputFormat.length === 0) { - return [{ name: 'input', type: 'any' }] - } - - // Check if any fields have been configured with names - const hasConfiguredFields = inputFormat.some( - (field: any) => field.name && field.name.trim() !== '' - ) - - // If no fields have been configured, return the default input field - if (!hasConfiguredFields) { - return [{ name: 'input', type: 'any' }] - } - - // Map input fields to response fields - return inputFormat.map((field: any) => ({ - name: `input.${field.name}`, - type: field.type || 'string', - description: field.description, - })) - } catch (e) { - console.error('Error extracting fields from starter input format:', e) - return [{ name: 'input', type: 'any' }] - } - } - - // Deduplicate connections by ID - const connectionMap = incomingConnections.reduce( - (acc, connection) => { - acc[connection.id] = connection - return acc - }, - {} as Record - ) - - // Sort connections by name - const sortedConnections = Object.values(connectionMap).sort((a, b) => - a.name.localeCompare(b.name) - ) + // Use connections in distance order (already sorted and deduplicated by the hook) + const sortedConnections = incomingConnections // Helper function to render a connection card - const renderConnectionCard = (connection: ConnectedBlock, field?: ResponseField) => { - const displayName = connection.name.replace(/\s+/g, '').toLowerCase() + const renderConnectionCard = (connection: ConnectedBlock) => { + // Get block configuration for icon and color + const blockConfig = getBlock(connection.type) + const displayName = connection.name // Use the actual block name instead of transforming it + const Icon = blockConfig?.icon + const bgColor = blockConfig?.bgColor || '#6B7280' // Fallback to gray return ( handleDragStart(e, connection, field)} + onDragStart={(e) => handleDragStart(e, connection)} onDragEnd={handleDragEnd} className={cn( - 'group flex w-max items-center rounded-lg border bg-card p-2 shadow-sm transition-colors', + 'group flex w-max items-center gap-2 rounded-lg border bg-card p-2 shadow-sm transition-colors', !isDisabled ? 'cursor-grab hover:bg-accent/50 active:cursor-grabbing' : 'cursor-not-allowed opacity-60' )} > + {/* Block icon with color */} + {Icon && ( +
    + +
    + )}
    {displayName} - - {field - ? `.${field.name}` - : typeof connection.outputType === 'string' - ? `.${connection.outputType}` - : ''} -
    ) } - return ( -
    - {sortedConnections.map((connection, index) => { - // Special handling for starter blocks with input format - if (connection.type === 'starter') { - const starterFields = extractFieldsFromStarterInput(connection) + // Generate all connection cards - one per block, not per output field + const connectionCards: React.ReactNode[] = [] - if (starterFields.length > 0) { - return ( -
    - {starterFields.map((field) => renderConnectionCard(connection, field))} -
    - ) - } - } + sortedConnections.forEach((connection) => { + connectionCards.push(renderConnectionCard(connection)) + }) - // Regular connection handling - return ( -
    - {Array.isArray(connection.outputType) - ? // Handle array of field names - connection.outputType.map((fieldName) => { - // Try to find field in response format - const fields = extractFieldsFromSchema(connection) - const field = fields.find((f) => f.name === fieldName) || { - name: fieldName, - type: 'string', - } + // Position and layout based on handle orientation - reverse of ports + // When ports are horizontal: connection blocks on top, aligned to left, closest blocks on bottom row + // When ports are vertical (default): connection blocks on left, stack vertically, aligned to right + const containerClasses = horizontalHandles + ? 'absolute bottom-full left-0 flex max-w-[600px] flex-wrap-reverse gap-2 pb-3' + : 'absolute top-0 right-full flex max-h-[400px] max-w-[200px] flex-col items-end gap-2 overflow-y-auto pr-3' - return renderConnectionCard(connection, field) - }) - : renderConnectionCard(connection)} -
    - ) - })} -
    - ) + return
    {connectionCards}
    } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 9812ed5b5..a69b0c745 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -451,6 +451,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { blockId={id} setIsConnecting={setIsConnecting} isDisabled={!userPermissions.canEdit} + horizontalHandles={horizontalHandles} /> {/* Input Handle - Don't show for starter blocks */} @@ -698,7 +699,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { {Object.entries(config.outputs).map(([key, value]) => (
    {key}{' '} - {typeof value.type === 'object' ? ( + {typeof value === 'object' ? (
    {Object.entries(value.type).map(([typeKey, typeValue]) => (
    @@ -712,7 +713,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { ))}
    ) : ( - {value.type as string} + {value as string} )}
    ))} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts index 2108a0404..fca7b1287 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts @@ -1,4 +1,5 @@ import { shallow } from 'zustand/shallow' +import { BlockPathCalculator } from '@/lib/block-path-calculator' import { createLogger } from '@/lib/logs/console-logger' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -53,63 +54,6 @@ function extractFieldsFromSchema(schema: any): Field[] { })) } -/** - * Finds all blocks along paths leading to the target block - * This is a reverse traversal from the target node to find all ancestors - * along connected paths - * @param edges - List of all edges in the graph - * @param targetNodeId - ID of the target block we're finding connections for - * @returns Array of unique ancestor node IDs - */ -function findAllPathNodes(edges: any[], targetNodeId: string): string[] { - // We'll use a reverse topological sort approach by tracking "distance" from target - const nodeDistances = new Map() - const visited = new Set() - const queue: [string, number][] = [[targetNodeId, 0]] // [nodeId, distance] - const pathNodes = new Set() - - // Build a reverse adjacency list for faster traversal - const reverseAdjList: Record = {} - for (const edge of edges) { - if (!reverseAdjList[edge.target]) { - reverseAdjList[edge.target] = [] - } - reverseAdjList[edge.target].push(edge.source) - } - - // BFS to find all ancestors and their shortest distance from target - while (queue.length > 0) { - const [currentNodeId, distance] = queue.shift()! - - if (visited.has(currentNodeId)) { - // If we've seen this node before, update its distance if this path is shorter - const currentDistance = nodeDistances.get(currentNodeId) || Number.POSITIVE_INFINITY - if (distance < currentDistance) { - nodeDistances.set(currentNodeId, distance) - } - continue - } - - visited.add(currentNodeId) - nodeDistances.set(currentNodeId, distance) - - // Don't add the target node itself to the results - if (currentNodeId !== targetNodeId) { - pathNodes.add(currentNodeId) - } - - // Get all incoming edges from the reverse adjacency list - const incomingNodeIds = reverseAdjList[currentNodeId] || [] - - // Add all source nodes to the queue with incremented distance - for (const sourceId of incomingNodeIds) { - queue.push([sourceId, distance + 1]) - } - } - - return Array.from(pathNodes) -} - export function useBlockConnections(blockId: string) { const { edges, blocks } = useWorkflowStore( (state) => ({ @@ -120,7 +64,7 @@ export function useBlockConnections(blockId: string) { ) // Find all blocks along paths leading to this block - const allPathNodeIds = findAllPathNodes(edges, blockId) + const allPathNodeIds = BlockPathCalculator.findAllPathNodes(edges, blockId) // Map each path node to a ConnectedBlock structure const allPathConnections = allPathNodeIds diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 31e399410..620fe14e2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -82,9 +82,9 @@ export function useWorkflowExecution() { } // If this was a streaming response and we have the final content, update it - if (streamContent && result.output?.response && typeof streamContent === 'string') { + if (streamContent && result.output && typeof streamContent === 'string') { // Update the content with the final streaming content - enrichedResult.output.response.content = streamContent + enrichedResult.output.content = streamContent // Also update any block logs to include the content where appropriate if (enrichedResult.logs) { @@ -97,10 +97,9 @@ export function useWorkflowExecution() { if ( isStreamingBlock && (log.blockType === 'agent' || log.blockType === 'router') && - log.output?.response - ) { - log.output.response.content = streamContent - } + log.output + ) + log.output.content = streamContent } } } @@ -122,7 +121,7 @@ export function useWorkflowExecution() { return executionId } catch (error) { - logger.error('Error persisting logs:', { error }) + logger.error('Error persisting logs:', error) return executionId } } @@ -215,8 +214,8 @@ export function useWorkflowExecution() { result.logs?.forEach((log: BlockLog) => { if (streamedContent.has(log.blockId)) { const content = streamedContent.get(log.blockId) || '' - if (log.output?.response) { - log.output.response.content = content + if (log.output) { + log.output.content = content } useConsoleStore.getState().updateConsole(log.blockId, content) } @@ -225,9 +224,9 @@ export function useWorkflowExecution() { controller.enqueue( encoder.encode(`data: ${JSON.stringify({ event: 'final', data: result })}\n\n`) ) - persistLogs(executionId, result).catch((err) => { - logger.error('Error persisting logs:', { error: err }) - }) + persistLogs(executionId, result).catch((err) => + logger.error('Error persisting logs:', err) + ) } } catch (error: any) { controller.error(error) @@ -437,7 +436,7 @@ export function useWorkflowExecution() { const errorResult: ExecutionResult = { success: false, - output: { response: {} }, + output: {}, error: errorMessage, logs: [], } @@ -560,7 +559,7 @@ export function useWorkflowExecution() { // Create error result const errorResult = { success: false, - output: { response: {} }, + output: {}, error: errorMessage, logs: debugContext.blockLogs, } @@ -647,7 +646,7 @@ export function useWorkflowExecution() { let currentResult: ExecutionResult = { success: true, - output: { response: {} }, + output: {}, logs: debugContext.blockLogs, } @@ -743,7 +742,7 @@ export function useWorkflowExecution() { // Create error result const errorResult = { success: false, - output: { response: {} }, + output: {}, error: errorMessage, logs: debugContext.blockLogs, } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx index 28844be07..f0e7b71b7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx @@ -19,6 +19,7 @@ const TOOLTIPS = { debugMode: 'Enable visual debugging information during execution.', autoConnect: 'Automatically connect nodes.', autoFillEnvVars: 'Automatically fill API keys.', + autoPan: 'Automatically pan to active blocks during workflow execution.', } export function General() { @@ -30,11 +31,13 @@ export function General() { const isAutoConnectEnabled = useGeneralStore((state) => state.isAutoConnectEnabled) const isDebugModeEnabled = useGeneralStore((state) => state.isDebugModeEnabled) const isAutoFillEnvVarsEnabled = useGeneralStore((state) => state.isAutoFillEnvVarsEnabled) + const isAutoPanEnabled = useGeneralStore((state) => state.isAutoPanEnabled) const setTheme = useGeneralStore((state) => state.setTheme) const toggleAutoConnect = useGeneralStore((state) => state.toggleAutoConnect) const toggleDebugMode = useGeneralStore((state) => state.toggleDebugMode) const toggleAutoFillEnvVars = useGeneralStore((state) => state.toggleAutoFillEnvVars) + const toggleAutoPan = useGeneralStore((state) => state.toggleAutoPan) const loadSettings = useGeneralStore((state) => state.loadSettings) useEffect(() => { @@ -66,6 +69,12 @@ export function General() { } } + const handleAutoPanChange = (checked: boolean) => { + if (checked !== isAutoPanEnabled) { + toggleAutoPan() + } + } + const handleRetry = () => { setRetryCount((prev) => prev + 1) } @@ -200,6 +209,35 @@ export function General() { disabled={isLoading} />
    +
    +
    + + + + + + +

    {TOOLTIPS.autoPan}

    +
    +
    +
    + +
    )} diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts index f49a77299..776f29944 100644 --- a/apps/sim/blocks/blocks/agent.ts +++ b/apps/sim/blocks/blocks/agent.ts @@ -332,25 +332,9 @@ export const AgentBlock: BlockConfig = { tools: { type: 'json', required: false }, }, outputs: { - response: { - type: { - content: 'string', - model: 'string', - tokens: 'any', - toolCalls: 'any', - }, - dependsOn: { - subBlockId: 'responseFormat', - condition: { - whenEmpty: { - content: 'string', - model: 'string', - tokens: 'any', - toolCalls: 'any', - }, - whenFilled: 'json', - }, - }, - }, + content: 'string', + model: 'string', + tokens: 'any', + toolCalls: 'any', }, } diff --git a/apps/sim/blocks/blocks/airtable.ts b/apps/sim/blocks/blocks/airtable.ts index c2a2b9c59..676c69d3e 100644 --- a/apps/sim/blocks/blocks/airtable.ts +++ b/apps/sim/blocks/blocks/airtable.ts @@ -179,12 +179,8 @@ export const AirtableBlock: BlockConfig = { }, // Output structure depends on the operation, covered by AirtableResponse union type outputs: { - response: { - type: { - records: 'json', // Optional: for list, create, updateMultiple - record: 'json', // Optional: for get, update single - metadata: 'json', // Required: present in all responses - }, - }, + records: 'json', // Optional: for list, create, updateMultiple + record: 'json', // Optional: for get, update single + metadata: 'json', // Required: present in all responses }, } diff --git a/apps/sim/blocks/blocks/api.ts b/apps/sim/blocks/blocks/api.ts index f6fd424eb..68dbaca29 100644 --- a/apps/sim/blocks/blocks/api.ts +++ b/apps/sim/blocks/blocks/api.ts @@ -62,12 +62,8 @@ export const ApiBlock: BlockConfig = { params: { type: 'json', required: false }, }, outputs: { - response: { - type: { - data: 'any', - status: 'number', - headers: 'json', - }, - }, + data: 'any', + status: 'number', + headers: 'json', }, } diff --git a/apps/sim/blocks/blocks/autoblocks.ts b/apps/sim/blocks/blocks/autoblocks.ts index bd87f186e..502585c33 100644 --- a/apps/sim/blocks/blocks/autoblocks.ts +++ b/apps/sim/blocks/blocks/autoblocks.ts @@ -112,13 +112,9 @@ export const AutoblocksBlock: BlockConfig = { environment: { type: 'string', required: true }, }, outputs: { - response: { - type: { - promptId: 'string', - version: 'string', - renderedPrompt: 'string', - templates: 'json', - }, - }, + promptId: 'string', + version: 'string', + renderedPrompt: 'string', + templates: 'json', }, } diff --git a/apps/sim/blocks/blocks/browser_use.ts b/apps/sim/blocks/blocks/browser_use.ts index e713b3221..33bc2feab 100644 --- a/apps/sim/blocks/blocks/browser_use.ts +++ b/apps/sim/blocks/blocks/browser_use.ts @@ -76,13 +76,9 @@ export const BrowserUseBlock: BlockConfig = { save_browser_data: { type: 'boolean', required: false }, }, outputs: { - response: { - type: { - id: 'string', - success: 'boolean', - output: 'any', - steps: 'json', - }, - }, + id: 'string', + success: 'boolean', + output: 'any', + steps: 'json', }, } diff --git a/apps/sim/blocks/blocks/clay.ts b/apps/sim/blocks/blocks/clay.ts index 646cfd3ba..092ce98aa 100644 --- a/apps/sim/blocks/blocks/clay.ts +++ b/apps/sim/blocks/blocks/clay.ts @@ -50,10 +50,6 @@ Plain Text: Best for populating a table in free-form style. data: { type: 'json', required: true }, }, outputs: { - response: { - type: { - data: 'any', - }, - }, + data: 'any', }, } diff --git a/apps/sim/blocks/blocks/condition.ts b/apps/sim/blocks/blocks/condition.ts index 944d5c753..91601094d 100644 --- a/apps/sim/blocks/blocks/condition.ts +++ b/apps/sim/blocks/blocks/condition.ts @@ -37,13 +37,9 @@ export const ConditionBlock: BlockConfig = { }, inputs: {}, outputs: { - response: { - type: { - content: 'string', - conditionResult: 'boolean', - selectedPath: 'json', - selectedConditionId: 'string', - }, - }, + content: 'string', + conditionResult: 'boolean', + selectedPath: 'json', + selectedConditionId: 'string', }, } diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index b3c0d13fa..c6bfd743c 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -109,14 +109,10 @@ export const ConfluenceBlock: BlockConfig = { content: { type: 'string', required: false }, }, outputs: { - response: { - type: { - ts: 'string', - pageId: 'string', - content: 'string', - title: 'string', - success: 'boolean', - }, - }, + ts: 'string', + pageId: 'string', + content: 'string', + title: 'string', + success: 'boolean', }, } diff --git a/apps/sim/blocks/blocks/discord.ts b/apps/sim/blocks/blocks/discord.ts index 38862fee7..9206c2a6c 100644 --- a/apps/sim/blocks/blocks/discord.ts +++ b/apps/sim/blocks/blocks/discord.ts @@ -149,11 +149,7 @@ export const DiscordBlock: BlockConfig = { userId: { type: 'string', required: false }, }, outputs: { - response: { - type: { - message: 'string', - data: 'any', - }, - }, + message: 'string', + data: 'any', }, } diff --git a/apps/sim/blocks/blocks/elevenlabs.ts b/apps/sim/blocks/blocks/elevenlabs.ts index 39f7cd449..61fe71c5f 100644 --- a/apps/sim/blocks/blocks/elevenlabs.ts +++ b/apps/sim/blocks/blocks/elevenlabs.ts @@ -39,11 +39,7 @@ export const ElevenLabsBlock: BlockConfig = { }, outputs: { - response: { - type: { - audioUrl: 'string', - }, - }, + audioUrl: 'string', }, subBlocks: [ diff --git a/apps/sim/blocks/blocks/evaluator.ts b/apps/sim/blocks/blocks/evaluator.ts index a10dfd409..218629145 100644 --- a/apps/sim/blocks/blocks/evaluator.ts +++ b/apps/sim/blocks/blocks/evaluator.ts @@ -307,25 +307,9 @@ export const EvaluatorBlock: BlockConfig = { content: { type: 'string' as ParamType, required: true }, }, outputs: { - response: { - type: { - content: 'string', - model: 'string', - tokens: 'any', - cost: 'any', - }, - dependsOn: { - subBlockId: 'metrics', - condition: { - whenEmpty: { - content: 'string', - model: 'string', - tokens: 'any', - cost: 'any', - }, - whenFilled: 'json', - }, - }, - }, - }, + content: 'string', + model: 'string', + tokens: 'any', + cost: 'any', + } as any, } diff --git a/apps/sim/blocks/blocks/exa.ts b/apps/sim/blocks/blocks/exa.ts index 6e7ad1b97..754d3ea2d 100644 --- a/apps/sim/blocks/blocks/exa.ts +++ b/apps/sim/blocks/blocks/exa.ts @@ -190,16 +190,12 @@ export const ExaBlock: BlockConfig = { url: { type: 'string', required: false }, }, outputs: { - response: { - type: { - // Search output - results: 'json', - // Find Similar Links output - similarLinks: 'json', - // Answer output - answer: 'string', - citations: 'json', - }, - }, + // Search output + results: 'json', + // Find Similar Links output + similarLinks: 'json', + // Answer output + answer: 'string', + citations: 'json', }, } diff --git a/apps/sim/blocks/blocks/file.ts b/apps/sim/blocks/blocks/file.ts index 97d10b8ac..bee1f381e 100644 --- a/apps/sim/blocks/blocks/file.ts +++ b/apps/sim/blocks/blocks/file.ts @@ -130,11 +130,7 @@ export const FileBlock: BlockConfig = { file: { type: 'json', required: false }, }, outputs: { - response: { - type: { - files: 'json', - combinedContent: 'string', - }, - }, + files: 'json', + combinedContent: 'string', }, } diff --git a/apps/sim/blocks/blocks/firecrawl.ts b/apps/sim/blocks/blocks/firecrawl.ts index 20c0ab17e..3eb3213cf 100644 --- a/apps/sim/blocks/blocks/firecrawl.ts +++ b/apps/sim/blocks/blocks/firecrawl.ts @@ -90,16 +90,12 @@ export const FirecrawlBlock: BlockConfig = { scrapeOptions: { type: 'json', required: false }, }, outputs: { - response: { - type: { - // Scrape output - markdown: 'string', - html: 'any', - metadata: 'json', - // Search output - data: 'json', - warning: 'any', - }, - }, + // Scrape output + markdown: 'string', + html: 'any', + metadata: 'json', + // Search output + data: 'json', + warning: 'any', }, } diff --git a/apps/sim/blocks/blocks/function.ts b/apps/sim/blocks/blocks/function.ts index 124f1054e..f8706924a 100644 --- a/apps/sim/blocks/blocks/function.ts +++ b/apps/sim/blocks/blocks/function.ts @@ -27,11 +27,7 @@ export const FunctionBlock: BlockConfig = { timeout: { type: 'number', required: false }, }, outputs: { - response: { - type: { - result: 'any', - stdout: 'string', - }, - }, + result: 'any', + stdout: 'string', }, } diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index 06fc0b784..db5725968 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -167,11 +167,7 @@ export const GitHubBlock: BlockConfig = { branch: { type: 'string', required: false }, }, outputs: { - response: { - type: { - content: 'string', - metadata: 'json', - }, - }, + content: 'string', + metadata: 'json', }, } diff --git a/apps/sim/blocks/blocks/gmail.ts b/apps/sim/blocks/blocks/gmail.ts index a5e047613..d7be4e604 100644 --- a/apps/sim/blocks/blocks/gmail.ts +++ b/apps/sim/blocks/blocks/gmail.ts @@ -179,11 +179,7 @@ export const GmailBlock: BlockConfig = { maxResults: { type: 'number', required: false }, }, outputs: { - response: { - type: { - content: 'string', - metadata: 'json', - }, - }, + content: 'string', + metadata: 'json', }, } diff --git a/apps/sim/blocks/blocks/google.ts b/apps/sim/blocks/blocks/google.ts index 6224a7b19..4c8b3b18d 100644 --- a/apps/sim/blocks/blocks/google.ts +++ b/apps/sim/blocks/blocks/google.ts @@ -87,11 +87,7 @@ export const GoogleSearchBlock: BlockConfig = { }, outputs: { - response: { - type: { - items: 'json', - searchInformation: 'json', - } as any, - }, + items: 'json', + searchInformation: 'json', }, } diff --git a/apps/sim/blocks/blocks/google_calendar.ts b/apps/sim/blocks/blocks/google_calendar.ts index 8c3b7997c..02f3cd2ad 100644 --- a/apps/sim/blocks/blocks/google_calendar.ts +++ b/apps/sim/blocks/blocks/google_calendar.ts @@ -284,11 +284,7 @@ export const GoogleCalendarBlock: BlockConfig = { sendUpdates: { type: 'string', required: false }, }, outputs: { - response: { - type: { - content: 'string', - metadata: 'json', - }, - }, + content: 'string', + metadata: 'json', }, } diff --git a/apps/sim/blocks/blocks/google_docs.ts b/apps/sim/blocks/blocks/google_docs.ts index 27e978447..7c012cf2b 100644 --- a/apps/sim/blocks/blocks/google_docs.ts +++ b/apps/sim/blocks/blocks/google_docs.ts @@ -181,12 +181,8 @@ export const GoogleDocsBlock: BlockConfig = { content: { type: 'string', required: false }, }, outputs: { - response: { - type: { - content: 'string', - metadata: 'json', - updatedContent: 'boolean', - }, - }, + content: 'string', + metadata: 'json', + updatedContent: 'boolean', }, } diff --git a/apps/sim/blocks/blocks/google_drive.ts b/apps/sim/blocks/blocks/google_drive.ts index 1ad6e708d..6ca9ccabd 100644 --- a/apps/sim/blocks/blocks/google_drive.ts +++ b/apps/sim/blocks/blocks/google_drive.ts @@ -265,11 +265,7 @@ export const GoogleDriveBlock: BlockConfig = { pageSize: { type: 'number', required: false }, }, outputs: { - response: { - type: { - file: 'json', - files: 'json', - }, - }, + file: 'json', + files: 'json', }, } diff --git a/apps/sim/blocks/blocks/google_sheets.ts b/apps/sim/blocks/blocks/google_sheets.ts index 9ae2ed9ee..4d8c959d2 100644 --- a/apps/sim/blocks/blocks/google_sheets.ts +++ b/apps/sim/blocks/blocks/google_sheets.ts @@ -211,16 +211,12 @@ export const GoogleSheetsBlock: BlockConfig = { insertDataOption: { type: 'string', required: false }, }, outputs: { - response: { - type: { - data: 'json', - metadata: 'json', - updatedRange: 'string', - updatedRows: 'number', - updatedColumns: 'number', - updatedCells: 'number', - tableRange: 'string', - }, - }, + data: 'json', + metadata: 'json', + updatedRange: 'string', + updatedRows: 'number', + updatedColumns: 'number', + updatedCells: 'number', + tableRange: 'string', }, } diff --git a/apps/sim/blocks/blocks/guesty.ts b/apps/sim/blocks/blocks/guesty.ts index 5814d8db1..a8e0346b0 100644 --- a/apps/sim/blocks/blocks/guesty.ts +++ b/apps/sim/blocks/blocks/guesty.ts @@ -82,17 +82,13 @@ export const GuestyBlock: BlockConfig = { apiKey: { type: 'string', required: true }, }, outputs: { - response: { - type: { - content: 'string', - model: 'string', - usage: 'json', - }, - }, + content: 'string', + model: 'string', + usage: 'json', }, } diff --git a/apps/sim/blocks/blocks/image_generator.ts b/apps/sim/blocks/blocks/image_generator.ts index 63cea4354..2f6cffba4 100644 --- a/apps/sim/blocks/blocks/image_generator.ts +++ b/apps/sim/blocks/blocks/image_generator.ts @@ -153,12 +153,8 @@ export const ImageGeneratorBlock: BlockConfig = { apiKey: { type: 'string', required: true }, }, outputs: { - response: { - type: { - content: 'string', - image: 'string', - metadata: 'json', - }, - }, + content: 'string', + image: 'string', + metadata: 'json', }, } diff --git a/apps/sim/blocks/blocks/jina.ts b/apps/sim/blocks/blocks/jina.ts index a884e7331..0275dd150 100644 --- a/apps/sim/blocks/blocks/jina.ts +++ b/apps/sim/blocks/blocks/jina.ts @@ -51,10 +51,6 @@ export const JinaBlock: BlockConfig = { apiKey: { type: 'string', required: true }, }, outputs: { - response: { - type: { - content: 'string', - }, - }, + content: 'string', }, } diff --git a/apps/sim/blocks/blocks/jira.ts b/apps/sim/blocks/blocks/jira.ts index e0493868d..3ec546d1f 100644 --- a/apps/sim/blocks/blocks/jira.ts +++ b/apps/sim/blocks/blocks/jira.ts @@ -187,17 +187,13 @@ export const JiraBlock: BlockConfig = { issueType: { type: 'string', required: false }, }, outputs: { - response: { - type: { - ts: 'string', - issueKey: 'string', - summary: 'string', - description: 'string', - created: 'string', - updated: 'string', - success: 'boolean', - url: 'string', - }, - }, + ts: 'string', + issueKey: 'string', + summary: 'string', + description: 'string', + created: 'string', + updated: 'string', + success: 'boolean', + url: 'string', }, } diff --git a/apps/sim/blocks/blocks/knowledge.ts b/apps/sim/blocks/blocks/knowledge.ts index 394f90d5f..080497f9e 100644 --- a/apps/sim/blocks/blocks/knowledge.ts +++ b/apps/sim/blocks/blocks/knowledge.ts @@ -38,13 +38,9 @@ export const KnowledgeBlock: BlockConfig = { content: { type: 'string', required: false }, }, outputs: { - response: { - type: { - results: 'json', - query: 'string', - totalResults: 'number', - }, - }, + results: 'json', + query: 'string', + totalResults: 'number', }, subBlocks: [ { diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts index f4eacc5c8..8e2545811 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -99,11 +99,7 @@ export const LinearBlock: BlockConfig = { description: { type: 'string', required: false }, }, outputs: { - response: { - type: { - issues: 'json', - issue: 'json', - }, - }, + issues: 'json', + issue: 'json', }, } diff --git a/apps/sim/blocks/blocks/linkup.ts b/apps/sim/blocks/blocks/linkup.ts index 34408d420..ed984776a 100644 --- a/apps/sim/blocks/blocks/linkup.ts +++ b/apps/sim/blocks/blocks/linkup.ts @@ -63,11 +63,7 @@ export const LinkupBlock: BlockConfig = { }, outputs: { - response: { - type: { - answer: 'string', - sources: 'json', - }, - }, + answer: 'string', + sources: 'json', }, } diff --git a/apps/sim/blocks/blocks/mem0.ts b/apps/sim/blocks/blocks/mem0.ts index 38a64ee4e..e85bf0797 100644 --- a/apps/sim/blocks/blocks/mem0.ts +++ b/apps/sim/blocks/blocks/mem0.ts @@ -290,12 +290,8 @@ export const Mem0Block: BlockConfig = { limit: { type: 'number', required: false }, }, outputs: { - response: { - type: { - ids: 'any', - memories: 'any', - searchResults: 'any', - }, - }, + ids: 'any', + memories: 'any', + searchResults: 'any', }, } diff --git a/apps/sim/blocks/blocks/memory.ts b/apps/sim/blocks/blocks/memory.ts index 946769624..59c3ff970 100644 --- a/apps/sim/blocks/blocks/memory.ts +++ b/apps/sim/blocks/blocks/memory.ts @@ -105,12 +105,8 @@ export const MemoryBlock: BlockConfig = { content: { type: 'string', required: false }, }, outputs: { - response: { - type: { - memories: 'any', - id: 'string', - }, - }, + memories: 'any', + id: 'string', }, subBlocks: [ { diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index 4eb2939a8..9cd294d36 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -199,17 +199,13 @@ export const MicrosoftExcelBlock: BlockConfig = { valueInputOption: { type: 'string', required: false }, }, outputs: { - response: { - type: { - data: 'json', - metadata: 'json', - updatedRange: 'string', - updatedRows: 'number', - updatedColumns: 'number', - updatedCells: 'number', - index: 'number', - values: 'json', - }, - }, + data: 'json', + metadata: 'json', + updatedRange: 'string', + updatedRows: 'number', + updatedColumns: 'number', + updatedCells: 'number', + index: 'number', + values: 'json', }, } diff --git a/apps/sim/blocks/blocks/microsoft_teams.ts b/apps/sim/blocks/blocks/microsoft_teams.ts index ae2809a49..9296e7549 100644 --- a/apps/sim/blocks/blocks/microsoft_teams.ts +++ b/apps/sim/blocks/blocks/microsoft_teams.ts @@ -169,12 +169,8 @@ export const MicrosoftTeamsBlock: BlockConfig = { content: { type: 'string', required: true }, }, outputs: { - response: { - type: { - content: 'string', - metadata: 'json', - updatedContent: 'boolean', - }, - }, + content: 'string', + metadata: 'json', + updatedContent: 'boolean', }, } diff --git a/apps/sim/blocks/blocks/mistral_parse.ts b/apps/sim/blocks/blocks/mistral_parse.ts index 49adde5f4..8b13b3253 100644 --- a/apps/sim/blocks/blocks/mistral_parse.ts +++ b/apps/sim/blocks/blocks/mistral_parse.ts @@ -202,11 +202,7 @@ export const MistralParseBlock: BlockConfig = { // imageMinSize: { type: 'string', required: false }, }, outputs: { - response: { - type: { - content: 'string', - metadata: 'json', - }, - }, + content: 'string', + metadata: 'json', }, } diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index 87c8149c0..8f3596cdf 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -174,11 +174,7 @@ export const NotionBlock: BlockConfig = { properties: { type: 'string', required: false }, }, outputs: { - response: { - type: { - content: 'string', - metadata: 'any', - }, - }, + content: 'string', + metadata: 'any', }, } diff --git a/apps/sim/blocks/blocks/openai.ts b/apps/sim/blocks/blocks/openai.ts index 16eefd4da..7a67bd9c1 100644 --- a/apps/sim/blocks/blocks/openai.ts +++ b/apps/sim/blocks/blocks/openai.ts @@ -49,12 +49,8 @@ export const OpenAIBlock: BlockConfig = { apiKey: { type: 'string', required: true }, }, outputs: { - response: { - type: { - embeddings: 'json', - model: 'string', - usage: 'json', - }, - }, + embeddings: 'json', + model: 'string', + usage: 'json', }, } diff --git a/apps/sim/blocks/blocks/outlook.ts b/apps/sim/blocks/blocks/outlook.ts index 4c2c82473..67d0b5732 100644 --- a/apps/sim/blocks/blocks/outlook.ts +++ b/apps/sim/blocks/blocks/outlook.ts @@ -140,11 +140,7 @@ export const OutlookBlock: BlockConfig< maxResults: { type: 'number', required: false }, }, outputs: { - response: { - type: { - message: 'string', - results: 'json', - }, - }, + message: 'string', + results: 'json', }, } diff --git a/apps/sim/blocks/blocks/perplexity.ts b/apps/sim/blocks/blocks/perplexity.ts index 2c6fb5ce7..3035b3d5a 100644 --- a/apps/sim/blocks/blocks/perplexity.ts +++ b/apps/sim/blocks/blocks/perplexity.ts @@ -106,12 +106,8 @@ export const PerplexityBlock: BlockConfig = { apiKey: { type: 'string', required: true }, }, outputs: { - response: { - type: { - content: 'string', - model: 'string', - usage: 'json', - }, - }, + content: 'string', + model: 'string', + usage: 'json', }, } diff --git a/apps/sim/blocks/blocks/pinecone.ts b/apps/sim/blocks/blocks/pinecone.ts index 0b12f4abc..fea14bf69 100644 --- a/apps/sim/blocks/blocks/pinecone.ts +++ b/apps/sim/blocks/blocks/pinecone.ts @@ -268,15 +268,11 @@ export const PineconeBlock: BlockConfig = { }, outputs: { - response: { - type: { - matches: 'any', - upsertedCount: 'any', - data: 'any', - model: 'any', - vector_type: 'any', - usage: 'any', - }, - }, + matches: 'any', + upsertedCount: 'any', + data: 'any', + model: 'any', + vector_type: 'any', + usage: 'any', }, } diff --git a/apps/sim/blocks/blocks/reddit.ts b/apps/sim/blocks/blocks/reddit.ts index f4eb9700c..b901f4eed 100644 --- a/apps/sim/blocks/blocks/reddit.ts +++ b/apps/sim/blocks/blocks/reddit.ts @@ -181,13 +181,9 @@ export const RedditBlock: BlockConfig< commentLimit: { type: 'number', required: false }, }, outputs: { - response: { - type: { - subreddit: 'string', - posts: 'json', - post: 'json', - comments: 'json', - }, - }, + subreddit: 'string', + posts: 'json', + post: 'json', + comments: 'json', }, } diff --git a/apps/sim/blocks/blocks/response.ts b/apps/sim/blocks/blocks/response.ts index 4720c6cb2..3e6ba92d8 100644 --- a/apps/sim/blocks/blocks/response.ts +++ b/apps/sim/blocks/blocks/response.ts @@ -92,12 +92,8 @@ export const ResponseBlock: BlockConfig = { }, }, outputs: { - response: { - type: { - data: 'json', - status: 'number', - headers: 'json', - }, - }, + data: 'json', + status: 'number', + headers: 'json', }, } diff --git a/apps/sim/blocks/blocks/router.ts b/apps/sim/blocks/blocks/router.ts index 56f195153..20221f331 100644 --- a/apps/sim/blocks/blocks/router.ts +++ b/apps/sim/blocks/blocks/router.ts @@ -180,14 +180,10 @@ export const RouterBlock: BlockConfig = { apiKey: { type: 'string', required: true }, }, outputs: { - response: { - type: { - content: 'string', - model: 'string', - tokens: 'any', - cost: 'any', - selectedPath: 'json', - }, - }, + content: 'string', + model: 'string', + tokens: 'any', + cost: 'any', + selectedPath: 'json', }, } diff --git a/apps/sim/blocks/blocks/s3.ts b/apps/sim/blocks/blocks/s3.ts index 83c34ac1e..a5d4e86bd 100644 --- a/apps/sim/blocks/blocks/s3.ts +++ b/apps/sim/blocks/blocks/s3.ts @@ -96,11 +96,7 @@ export const S3Block: BlockConfig = { s3Uri: { type: 'string', required: true }, }, outputs: { - response: { - type: { - url: 'string', - metadata: 'json', - }, - }, + url: 'string', + metadata: 'json', }, } diff --git a/apps/sim/blocks/blocks/serper.ts b/apps/sim/blocks/blocks/serper.ts index 6b298bab8..92a94b1e7 100644 --- a/apps/sim/blocks/blocks/serper.ts +++ b/apps/sim/blocks/blocks/serper.ts @@ -69,10 +69,6 @@ export const SerperBlock: BlockConfig = { type: { type: 'string', required: false }, }, outputs: { - response: { - type: { - searchResults: 'json', - }, - }, + searchResults: 'json', }, } diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 4fb5a338f..c87c5e683 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -138,11 +138,7 @@ export const SlackBlock: BlockConfig = { text: { type: 'string', required: true }, }, outputs: { - response: { - type: { - ts: 'string', - channel: 'string', - }, - }, + ts: 'string', + channel: 'string', }, } diff --git a/apps/sim/blocks/blocks/stagehand.ts b/apps/sim/blocks/blocks/stagehand.ts index fa08c00b9..bd66fe4e9 100644 --- a/apps/sim/blocks/blocks/stagehand.ts +++ b/apps/sim/blocks/blocks/stagehand.ts @@ -64,10 +64,6 @@ export const StagehandBlock: BlockConfig = { apiKey: { type: 'string', required: true }, }, outputs: { - response: { - type: { - data: 'json', - }, - }, + data: 'json', }, } diff --git a/apps/sim/blocks/blocks/stagehand_agent.ts b/apps/sim/blocks/blocks/stagehand_agent.ts index f7f802234..bd8ddd368 100644 --- a/apps/sim/blocks/blocks/stagehand_agent.ts +++ b/apps/sim/blocks/blocks/stagehand_agent.ts @@ -83,11 +83,7 @@ export const StagehandAgentBlock: BlockConfig = { outputSchema: { type: 'json', required: false }, }, outputs: { - response: { - type: { - agentResult: 'json', - structuredOutput: 'any', - }, - }, + agentResult: 'json', + structuredOutput: 'any', }, } diff --git a/apps/sim/blocks/blocks/starter.ts b/apps/sim/blocks/blocks/starter.ts index 0dff75af3..177736689 100644 --- a/apps/sim/blocks/blocks/starter.ts +++ b/apps/sim/blocks/blocks/starter.ts @@ -1,14 +1,7 @@ import { StartIcon } from '@/components/icons' -import type { ToolResponse } from '@/tools/types' import type { BlockConfig } from '../types' -interface StarterBlockOutput extends ToolResponse { - output: { - input: any - } -} - -export const StarterBlock: BlockConfig = { +export const StarterBlock: BlockConfig = { type: 'starter', name: 'Starter', description: 'Start workflow', @@ -189,11 +182,5 @@ export const StarterBlock: BlockConfig = { inputs: { input: { type: 'json', required: false }, }, - outputs: { - response: { - type: { - input: 'any', - }, - }, - }, + outputs: {}, } diff --git a/apps/sim/blocks/blocks/supabase.ts b/apps/sim/blocks/blocks/supabase.ts index 8d5949e8a..5f2f88598 100644 --- a/apps/sim/blocks/blocks/supabase.ts +++ b/apps/sim/blocks/blocks/supabase.ts @@ -109,11 +109,7 @@ export const SupabaseBlock: BlockConfig = { data: { type: 'string', required: false, requiredForToolCall: true }, }, outputs: { - response: { - type: { - message: 'string', - results: 'json', - }, - }, + message: 'string', + results: 'json', }, } diff --git a/apps/sim/blocks/blocks/tavily.ts b/apps/sim/blocks/blocks/tavily.ts index f0f682555..b33e08d83 100644 --- a/apps/sim/blocks/blocks/tavily.ts +++ b/apps/sim/blocks/blocks/tavily.ts @@ -98,15 +98,11 @@ export const TavilyBlock: BlockConfig = { extract_depth: { type: 'string', required: false }, }, outputs: { - response: { - type: { - results: 'json', - answer: 'any', - query: 'string', - content: 'string', - title: 'string', - url: 'string', - }, - }, + results: 'json', + answer: 'any', + query: 'string', + content: 'string', + title: 'string', + url: 'string', }, } diff --git a/apps/sim/blocks/blocks/telegram.ts b/apps/sim/blocks/blocks/telegram.ts index d2321f02f..0e1180abf 100644 --- a/apps/sim/blocks/blocks/telegram.ts +++ b/apps/sim/blocks/blocks/telegram.ts @@ -55,11 +55,7 @@ export const TelegramBlock: BlockConfig = { text: { type: 'string', required: true }, }, outputs: { - response: { - type: { - ok: 'boolean', - result: 'json', - }, - }, + ok: 'boolean', + result: 'json', }, } diff --git a/apps/sim/blocks/blocks/thinking.ts b/apps/sim/blocks/blocks/thinking.ts index a6139c64c..63231c96f 100644 --- a/apps/sim/blocks/blocks/thinking.ts +++ b/apps/sim/blocks/blocks/thinking.ts @@ -36,11 +36,7 @@ export const ThinkingBlock: BlockConfig = { }, outputs: { - response: { - type: { - acknowledgedThought: 'string', - }, - }, + acknowledgedThought: 'string', }, tools: { diff --git a/apps/sim/blocks/blocks/translate.ts b/apps/sim/blocks/blocks/translate.ts index f8361c7e1..6579588ba 100644 --- a/apps/sim/blocks/blocks/translate.ts +++ b/apps/sim/blocks/blocks/translate.ts @@ -93,12 +93,8 @@ export const TranslateBlock: BlockConfig = { systemPrompt: { type: 'string', required: true }, }, outputs: { - response: { - type: { - content: 'string', - model: 'string', - tokens: 'any', - }, - }, + content: 'string', + model: 'string', + tokens: 'any', }, } diff --git a/apps/sim/blocks/blocks/twilio.ts b/apps/sim/blocks/blocks/twilio.ts index 67fbcf6ea..6d08dd02b 100644 --- a/apps/sim/blocks/blocks/twilio.ts +++ b/apps/sim/blocks/blocks/twilio.ts @@ -62,13 +62,9 @@ export const TwilioSMSBlock: BlockConfig = { fromNumber: { type: 'string', required: true }, }, outputs: { - response: { - type: { - success: 'boolean', - messageId: 'any', - status: 'any', - error: 'any', - }, - }, + success: 'boolean', + messageId: 'any', + status: 'any', + error: 'any', }, } diff --git a/apps/sim/blocks/blocks/typeform.ts b/apps/sim/blocks/blocks/typeform.ts index 7cc7d3fe6..a870311e2 100644 --- a/apps/sim/blocks/blocks/typeform.ts +++ b/apps/sim/blocks/blocks/typeform.ts @@ -215,23 +215,8 @@ export const TypeformBlock: BlockConfig = { inline: { type: 'boolean', required: false }, }, outputs: { - response: { - type: { - total_items: 'number', - page_count: 'number', - items: 'json', - }, - dependsOn: { - subBlockId: 'operation', - condition: { - whenEmpty: { - total_items: 'number', - page_count: 'number', - items: 'json', - }, - whenFilled: 'json', - }, - }, - }, + total_items: 'number', + page_count: 'number', + items: 'json', }, } diff --git a/apps/sim/blocks/blocks/vision.ts b/apps/sim/blocks/blocks/vision.ts index 8037561b1..53279a48e 100644 --- a/apps/sim/blocks/blocks/vision.ts +++ b/apps/sim/blocks/blocks/vision.ts @@ -53,12 +53,8 @@ export const VisionBlock: BlockConfig = { prompt: { type: 'string', required: false }, }, outputs: { - response: { - type: { - content: 'string', - model: 'any', - tokens: 'any', - }, - }, + content: 'string', + model: 'any', + tokens: 'any', }, } diff --git a/apps/sim/blocks/blocks/whatsapp.ts b/apps/sim/blocks/blocks/whatsapp.ts index d00f0bdb0..47485883c 100644 --- a/apps/sim/blocks/blocks/whatsapp.ts +++ b/apps/sim/blocks/blocks/whatsapp.ts @@ -64,12 +64,8 @@ export const WhatsAppBlock: BlockConfig = { accessToken: { type: 'string', required: true }, }, outputs: { - response: { - type: { - success: 'boolean', - messageId: 'any', - error: 'any', - }, - }, + success: 'boolean', + messageId: 'any', + error: 'any', }, } diff --git a/apps/sim/blocks/blocks/workflow.ts b/apps/sim/blocks/blocks/workflow.ts index 854bc814c..fac3ae58e 100644 --- a/apps/sim/blocks/blocks/workflow.ts +++ b/apps/sim/blocks/blocks/workflow.ts @@ -55,7 +55,7 @@ export const WorkflowBlock: BlockConfig = { title: 'Input Variable (Optional)', type: 'short-input', placeholder: 'Select a variable to pass to the child workflow', - description: 'This variable will be available as start.response.input in the child workflow', + description: 'This variable will be available as start.input in the child workflow', }, ], tools: { @@ -74,13 +74,9 @@ export const WorkflowBlock: BlockConfig = { }, }, outputs: { - response: { - type: { - success: 'boolean', - childWorkflowName: 'string', - result: 'json', - error: 'string', - }, - }, + success: 'boolean', + childWorkflowName: 'string', + result: 'json', + error: 'string', }, } diff --git a/apps/sim/blocks/blocks/x.ts b/apps/sim/blocks/blocks/x.ts index 6c9db2e30..6fac15183 100644 --- a/apps/sim/blocks/blocks/x.ts +++ b/apps/sim/blocks/blocks/x.ts @@ -211,17 +211,13 @@ export const XBlock: BlockConfig = { includeRecentTweets: { type: 'boolean', required: false }, }, outputs: { - response: { - type: { - tweet: 'json', - replies: 'any', - context: 'any', - tweets: 'json', - includes: 'any', - meta: 'json', - user: 'json', - recentTweets: 'any', - }, - }, + tweet: 'json', + replies: 'any', + context: 'any', + tweets: 'json', + includes: 'any', + meta: 'json', + user: 'json', + recentTweets: 'any', }, } diff --git a/apps/sim/blocks/blocks/youtube.ts b/apps/sim/blocks/blocks/youtube.ts index d40baf3b8..5ff45080d 100644 --- a/apps/sim/blocks/blocks/youtube.ts +++ b/apps/sim/blocks/blocks/youtube.ts @@ -46,11 +46,7 @@ export const YouTubeBlock: BlockConfig = { maxResults: { type: 'number', required: false }, }, outputs: { - response: { - type: { - items: 'json', - totalResults: 'number', - }, - }, + items: 'json', + totalResults: 'number', }, } diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index ea2ff6f37..36ceb018a 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -157,20 +157,10 @@ export interface BlockConfig { } } inputs: Record - outputs: { - response: { - type: ToolOutputToValueType> - dependsOn?: { - subBlockId: string - condition: { - whenEmpty: ToolOutputToValueType> - whenFilled: 'json' - } - } - visualization?: { - type: 'image' - url: string - } + outputs: ToolOutputToValueType> & { + visualization?: { + type: 'image' + url: string } } hideFromToolbar?: boolean @@ -179,11 +169,4 @@ export interface BlockConfig { // Output configuration rules export interface OutputConfig { type: BlockOutput - dependsOn?: { - subBlockId: string - condition: { - whenEmpty: BlockOutput - whenFilled: BlockOutput - } - } } diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index 6d3c7a2e1..13dc29b5e 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -1,48 +1,13 @@ -import type { BlockOutput, OutputConfig } from '@/blocks/types' -import type { SubBlockState } from '@/stores/workflows/workflow/types' - -interface CodeLine { - id: string - content: string -} - -function isEmptyValue(value: SubBlockState['value']): boolean { - if (value === null || value === undefined) return true - if (typeof value === 'string') return value.trim() === '' - if (typeof value === 'number') return false - if (Array.isArray(value)) { - // Handle code editor's array of lines format - if (value.length === 0) return true - if (isCodeEditorValue(value)) { - return value.every((line: any) => !line.content.trim()) - } - return value.length === 0 - } - return false -} - -function isCodeEditorValue(value: any[]): value is CodeLine[] { - return value.length > 0 && 'id' in value[0] && 'content' in value[0] -} +import type { BlockOutput } from '@/blocks/types' export function resolveOutputType( - outputs: Record, - subBlocks: Record + outputs: Record ): Record { const resolvedOutputs: Record = {} - for (const [key, outputConfig] of Object.entries(outputs)) { - // If no dependencies, use the type directly - if (!outputConfig.dependsOn) { - resolvedOutputs[key] = outputConfig.type - continue - } - - // Handle dependent output types - const subBlock = subBlocks[outputConfig.dependsOn.subBlockId] - resolvedOutputs[key] = isEmptyValue(subBlock?.value) - ? outputConfig.dependsOn.condition.whenEmpty - : outputConfig.dependsOn.condition.whenFilled + for (const [key, outputType] of Object.entries(outputs)) { + // Since dependsOn has been removed, just use the type directly + resolvedOutputs[key] = outputType as BlockOutput } return resolvedOutputs diff --git a/apps/sim/components/ui/tag-dropdown.test.tsx b/apps/sim/components/ui/tag-dropdown.test.tsx index f21fc8cd5..839970e95 100644 --- a/apps/sim/components/ui/tag-dropdown.test.tsx +++ b/apps/sim/components/ui/tag-dropdown.test.tsx @@ -274,7 +274,7 @@ describe('TagDropdown Search and Filtering', () => { 'loop.index', 'loop.currentItem', 'parallel.index', - 'block.response.data', + 'block.data', ] const searchTerm = 'user' @@ -288,7 +288,7 @@ describe('TagDropdown Search and Filtering', () => { 'variable.userName', 'loop.index', 'parallel.currentItem', - 'block.response.data', + 'block.data', 'variable.userAge', 'loop.currentItem', ] @@ -313,7 +313,7 @@ describe('TagDropdown Search and Filtering', () => { expect(variableTags).toEqual(['variable.userName', 'variable.userAge']) expect(loopTags).toEqual(['loop.index', 'loop.currentItem']) expect(parallelTags).toEqual(['parallel.currentItem']) - expect(blockTags).toEqual(['block.response.data']) + expect(blockTags).toEqual(['block.data']) }) }) @@ -358,22 +358,6 @@ describe('checkTagTrigger helper function', () => { }) describe('extractFieldsFromSchema helper function logic', () => { - test('should extract fields from legacy format with fields array', () => { - const responseFormat = { - fields: [ - { name: 'name', type: 'string', description: 'User name' }, - { name: 'age', type: 'number', description: 'User age' }, - ], - } - - const fields = extractFieldsFromSchema(responseFormat) - - expect(fields).toEqual([ - { name: 'name', type: 'string', description: 'User name' }, - { name: 'age', type: 'number', description: 'User age' }, - ]) - }) - test('should extract fields from JSON Schema format', () => { const responseFormat = { schema: { @@ -450,6 +434,26 @@ describe('extractFieldsFromSchema helper function logic', () => { { name: 'age', type: 'number', description: undefined }, ]) }) + + test('should handle flattened response format (new format)', () => { + const responseFormat = { + schema: { + properties: { + name: { type: 'string', description: 'User name' }, + age: { type: 'number', description: 'User age' }, + status: { type: 'boolean', description: 'Active status' }, + }, + }, + } + + const fields = extractFieldsFromSchema(responseFormat) + + expect(fields).toEqual([ + { name: 'name', type: 'string', description: 'User name' }, + { name: 'age', type: 'number', description: 'User age' }, + { name: 'status', type: 'boolean', description: 'Active status' }, + ]) + }) }) describe('TagDropdown Tag Ordering', () => { @@ -457,7 +461,7 @@ describe('TagDropdown Tag Ordering', () => { const variableTags = ['variable.userName', 'variable.userAge'] const loopTags = ['loop.index', 'loop.currentItem'] const parallelTags = ['parallel.index'] - const blockTags = ['block.response.data'] + const blockTags = ['block.data'] const orderedTags = [...variableTags, ...loopTags, ...parallelTags, ...blockTags] @@ -467,12 +471,12 @@ describe('TagDropdown Tag Ordering', () => { 'loop.index', 'loop.currentItem', 'parallel.index', - 'block.response.data', + 'block.data', ]) }) test('should create tag index map correctly', () => { - const orderedTags = ['variable.userName', 'loop.index', 'block.response.data'] + const orderedTags = ['variable.userName', 'loop.index', 'block.data'] const tagIndexMap = new Map() orderedTags.forEach((tag, index) => { @@ -481,7 +485,7 @@ describe('TagDropdown Tag Ordering', () => { expect(tagIndexMap.get('variable.userName')).toBe(0) expect(tagIndexMap.get('loop.index')).toBe(1) - expect(tagIndexMap.get('block.response.data')).toBe(2) + expect(tagIndexMap.get('block.data')).toBe(2) expect(tagIndexMap.get('nonexistent')).toBeUndefined() }) }) @@ -491,39 +495,39 @@ describe('TagDropdown Tag Selection Logic', () => { const testCases = [ { description: 'should remove existing closing bracket from incomplete tag', - inputValue: 'Hello ', - cursorPosition: 21, // cursor after the dot - tag: 'start.response.input', - expectedResult: 'Hello ', + inputValue: 'Hello ', + cursorPosition: 13, // cursor after the dot + tag: 'start.input', + expectedResult: 'Hello ', }, { description: 'should remove existing closing bracket when replacing tag content', - inputValue: 'Hello ', - cursorPosition: 22, // cursor after 'response.' - tag: 'start.response.data', - expectedResult: 'Hello ', + inputValue: 'Hello ', + cursorPosition: 12, // cursor after 'start.' + tag: 'start.data', + expectedResult: 'Hello ', }, { description: 'should preserve content after closing bracket', - inputValue: 'Hello world', - cursorPosition: 21, - tag: 'start.response.input', - expectedResult: 'Hello world', + inputValue: 'Hello world', + cursorPosition: 13, + tag: 'start.input', + expectedResult: 'Hello world', }, { description: 'should not affect closing bracket if text between contains invalid characters', - inputValue: 'Hello and ', - cursorPosition: 22, - tag: 'start.response.data', - expectedResult: 'Hello and ', + inputValue: 'Hello and ', + cursorPosition: 12, + tag: 'start.data', + expectedResult: 'Hello and ', }, { description: 'should handle case with no existing closing bracket', - inputValue: 'Hello ', + inputValue: 'Hello ', }, ] @@ -556,25 +560,25 @@ describe('TagDropdown Tag Selection Logic', () => { // Valid tag-like text expect(regex.test('')).toBe(true) // empty string expect(regex.test('input')).toBe(true) - expect(regex.test('response.data')).toBe(true) + expect(regex.test('content.data')).toBe(true) expect(regex.test('user_name')).toBe(true) expect(regex.test('item123')).toBe(true) - expect(regex.test('response.data.item_1')).toBe(true) + expect(regex.test('content.data.item_1')).toBe(true) // Invalid tag-like text (should not remove closing bracket) expect(regex.test('input> and more')).toBe(false) - expect(regex.test('response data')).toBe(false) // space + expect(regex.test('content data')).toBe(false) // space expect(regex.test('user-name')).toBe(false) // hyphen expect(regex.test('data[')).toBe(false) // bracket - expect(regex.test('response.data!')).toBe(false) // exclamation + expect(regex.test('content.data!')).toBe(false) // exclamation }) test('should find correct position of last open bracket', () => { const testCases = [ - { input: 'Hello and and { test('should find correct position of next closing bracket', () => { const testCases = [ { input: 'input>', expected: 5 }, - { input: 'response.data> more text', expected: 13 }, + { input: 'content.data> more text', expected: 12 }, { input: 'no closing bracket', expected: -1 }, { input: '>', expected: 0 }, { input: 'multiple > > > >last', expected: 9 }, diff --git a/apps/sim/components/ui/tag-dropdown.tsx b/apps/sim/components/ui/tag-dropdown.tsx index cb3c15b9f..035ab9534 100644 --- a/apps/sim/components/ui/tag-dropdown.tsx +++ b/apps/sim/components/ui/tag-dropdown.tsx @@ -1,33 +1,67 @@ import type React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' +import { BlockPathCalculator } from '@/lib/block-path-calculator' import { createLogger } from '@/lib/logs/console-logger' import { cn } from '@/lib/utils' -import { - type ConnectedBlock, - useBlockConnections, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections' import { getBlock } from '@/blocks' +import { Serializer } from '@/serializer' import { useVariablesStore } from '@/stores/panel/variables/store' import type { Variable } from '@/stores/panel/variables/types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' const logger = createLogger('TagDropdown') +// Type definitions for component data structures +interface BlockTagGroup { + blockName: string + blockId: string + blockType: string + tags: string[] + distance: number +} + interface Field { name: string type: string description?: string } -interface Metric { - name: string - description: string - range: { - min: number - max: number +// Helper function to extract fields from JSON Schema +export function extractFieldsFromSchema(schema: any): Field[] { + if (!schema || typeof schema !== 'object') { + return [] } + + // Handle legacy format with fields array + if (Array.isArray(schema.fields)) { + return schema.fields + } + + // Handle new JSON Schema format + const schemaObj = schema.schema || schema + if (!schemaObj || !schemaObj.properties || typeof schemaObj.properties !== 'object') { + return [] + } + + // Extract fields from schema properties + return Object.entries(schemaObj.properties).map(([name, prop]: [string, any]) => { + // Handle array format like ['string', 'array'] + if (Array.isArray(prop)) { + return { + name, + type: prop.includes('array') ? 'array' : prop[0] || 'string', + description: undefined, + } + } + + // Handle object format like { type: 'string', description: '...' } + return { + name, + type: prop.type || 'string', + description: prop.description, + } + }) } interface TagDropdownProps { @@ -42,32 +76,42 @@ interface TagDropdownProps { style?: React.CSSProperties } -// Add a helper function to extract fields from JSON Schema -export const extractFieldsFromSchema = (responseFormat: any): Field[] => { - if (!responseFormat) return [] +// Check if tag trigger '<' should show dropdown +export const checkTagTrigger = (text: string, cursorPosition: number): { show: boolean } => { + if (cursorPosition >= 1) { + const textBeforeCursor = text.slice(0, cursorPosition) + const lastOpenBracket = textBeforeCursor.lastIndexOf('<') + const lastCloseBracket = textBeforeCursor.lastIndexOf('>') - // Handle legacy format with fields array - if (Array.isArray(responseFormat.fields)) { - return responseFormat.fields + // Show if we have an unclosed '<' that's not part of a completed tag + if (lastOpenBracket !== -1 && (lastCloseBracket === -1 || lastCloseBracket < lastOpenBracket)) { + return { show: true } + } + } + return { show: false } +} + +// Generate output paths from block configuration outputs +const generateOutputPaths = (outputs: Record, prefix = ''): string[] => { + const paths: string[] = [] + + for (const [key, value] of Object.entries(outputs)) { + const currentPath = prefix ? `${prefix}.${key}` : key + + if (typeof value === 'string') { + // Simple type like 'string', 'number', 'json', 'any' + paths.push(currentPath) + } else if (typeof value === 'object' && value !== null) { + // Nested object - recurse + const subPaths = generateOutputPaths(value, currentPath) + paths.push(...subPaths) + } else { + // Fallback - add the path + paths.push(currentPath) + } } - // Handle new JSON Schema format - const schema = responseFormat.schema || responseFormat - if ( - !schema || - typeof schema !== 'object' || - !('properties' in schema) || - typeof schema.properties !== 'object' || - schema.properties === null - ) { - return [] - } - - return Object.entries(schema.properties).map(([name, prop]: [string, any]) => ({ - name, - type: Array.isArray(prop) ? 'array' : prop.type || 'string', - description: prop.description, - })) + return paths } export const TagDropdown: React.FC = ({ @@ -81,90 +125,129 @@ export const TagDropdown: React.FC = ({ onClose, style, }) => { + // Component state const [selectedIndex, setSelectedIndex] = useState(0) - // Get available tags from workflow state + // Store hooks for workflow data const blocks = useWorkflowStore((state) => state.blocks) const loops = useWorkflowStore((state) => state.loops) const parallels = useWorkflowStore((state) => state.parallels) - const _edges = useWorkflowStore((state) => state.edges) + const edges = useWorkflowStore((state) => state.edges) const workflowId = useWorkflowRegistry((state) => state.activeWorkflowId) - // Get variables from variables store + // Store hooks for variables const getVariablesByWorkflowId = useVariablesStore((state) => state.getVariablesByWorkflowId) const loadVariables = useVariablesStore((state) => state.loadVariables) const variables = useVariablesStore((state) => state.variables) const workflowVariables = workflowId ? getVariablesByWorkflowId(workflowId) : [] - // Get all connected blocks using useBlockConnections - const { incomingConnections } = useBlockConnections(blockId) - - // Load variables when workflowId changes + // Load variables when workflow changes useEffect(() => { if (workflowId) { loadVariables(workflowId) } }, [workflowId, loadVariables]) - // Extract search term from input + // Extract current search term from input const searchTerm = useMemo(() => { const textBeforeCursor = inputValue.slice(0, cursorPosition) const match = textBeforeCursor.match(/<([^>]*)$/) return match ? match[1].toLowerCase() : '' }, [inputValue, cursorPosition]) - // Get source block and compute tags - const { tags, variableInfoMap = {} } = useMemo(() => { - // Helper function to get output paths - const getOutputPaths = (obj: any, prefix = '', isStarterBlock = false): string[] => { - if (typeof obj !== 'object' || obj === null) { - return prefix ? [prefix] : [] + // Generate all available tags using BlockPathCalculator and clean block outputs + const { + tags, + variableInfoMap = {}, + blockTagGroups = [], + } = useMemo(() => { + // Handle active source block (drag & drop from specific block) + if (activeSourceBlockId) { + const sourceBlock = blocks[activeSourceBlockId] + if (!sourceBlock) { + return { tags: [], variableInfoMap: {}, blockTagGroups: [] } } - // Special handling for starter block with input format - if (isStarterBlock && prefix === 'response') { - try { - // Check if there's an input format defined - const inputFormatValue = useSubBlockStore - .getState() - .getValue(activeSourceBlockId || blockId, 'inputFormat') - if (inputFormatValue && Array.isArray(inputFormatValue) && inputFormatValue.length > 0) { - // Check if any fields have been configured with names - const hasConfiguredFields = inputFormatValue.some( - (field: any) => field.name && field.name.trim() !== '' - ) - - // If no fields have been configured, return the default input path - if (!hasConfiguredFields) { - return ['response.input'] - } - - // Return fields from input format - return inputFormatValue.map((field: any) => `response.input.${field.name}`) - } - } catch (e) { - logger.error('Error parsing input format:', { e }) - } - - return ['response.input'] + const blockConfig = getBlock(sourceBlock.type) + if (!blockConfig) { + return { tags: [], variableInfoMap: {}, blockTagGroups: [] } } - if ('type' in obj && typeof obj.type === 'string') { - return [prefix] + const blockName = sourceBlock.name || sourceBlock.type + const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() + + // Handle blocks with no outputs (like starter) - show as just + let blockTags: string[] + if (Object.keys(blockConfig.outputs).length === 0) { + blockTags = [normalizedBlockName] + } else { + const outputPaths = generateOutputPaths(blockConfig.outputs) + blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) } - return Object.entries(obj).flatMap(([key, value]) => { - const newPrefix = prefix ? `${prefix}.${key}` : key - return getOutputPaths(value, newPrefix, isStarterBlock) - }) + const blockTagGroups: BlockTagGroup[] = [ + { + blockName, + blockId: activeSourceBlockId, + blockType: sourceBlock.type, + tags: blockTags, + distance: 0, + }, + ] + + return { + tags: blockTags, + variableInfoMap: {}, + blockTagGroups, + } } - // Variables as tags - format as variable.{variableName} + // Create serialized workflow for BlockPathCalculator + const serializer = new Serializer() + const serializedWorkflow = serializer.serializeWorkflow(blocks, edges, loops, parallels) + + // Find accessible blocks using BlockPathCalculator + const accessibleBlockIds = BlockPathCalculator.findAllPathNodes( + serializedWorkflow.connections, + blockId + ) + + // Always include starter block + const starterBlock = Object.values(blocks).find((block) => block.type === 'starter') + if (starterBlock && !accessibleBlockIds.includes(starterBlock.id)) { + accessibleBlockIds.push(starterBlock.id) + } + + // Calculate distances from starter block for ordering + const blockDistances: Record = {} + if (starterBlock) { + const adjList: Record = {} + for (const edge of edges) { + if (!adjList[edge.source]) adjList[edge.source] = [] + adjList[edge.source].push(edge.target) + } + + const visited = new Set() + const queue: [string, number][] = [[starterBlock.id, 0]] + + while (queue.length > 0) { + const [currentNodeId, distance] = queue.shift()! + if (visited.has(currentNodeId)) continue + visited.add(currentNodeId) + blockDistances[currentNodeId] = distance + + const outgoingNodeIds = adjList[currentNodeId] || [] + for (const targetId of outgoingNodeIds) { + queue.push([targetId, distance + 1]) + } + } + } + + // Create variable tags const variableTags = workflowVariables.map( (variable: Variable) => `variable.${variable.name.replace(/\s+/g, '')}` ) - // Create a map of variable tags to their type information const variableInfoMap = workflowVariables.reduce( (acc, variable) => { const tagName = `variable.${variable.name.replace(/\s+/g, '')}` @@ -177,225 +260,73 @@ export const TagDropdown: React.FC = ({ {} as Record ) - // Loop tags - Add if this block is in a loop + // Generate loop tags if current block is in a loop const loopTags: string[] = [] - - // Check if the current block is part of a loop const containingLoop = Object.entries(loops).find(([_, loop]) => loop.nodes.includes(blockId)) - if (containingLoop) { const [_loopId, loop] = containingLoop const loopType = loop.loopType || 'for' - - // Add loop.index for all loop types loopTags.push('loop.index') - - // Add forEach specific properties if (loopType === 'forEach') { - // Add loop.currentItem and loop.items loopTags.push('loop.currentItem') loopTags.push('loop.items') } } - // Parallel tags - Add if this block is in a parallel + // Generate parallel tags if current block is in parallel const parallelTags: string[] = [] - - // Check if the current block is part of a parallel const containingParallel = Object.entries(parallels || {}).find(([_, parallel]) => parallel.nodes.includes(blockId) ) - if (containingParallel) { - // Add parallel.index for all parallel blocks parallelTags.push('parallel.index') - - // Add parallel.currentItem and parallel.items parallelTags.push('parallel.currentItem') parallelTags.push('parallel.items') } - // If we have an active source block ID from a drop, use that specific block only - if (activeSourceBlockId) { - const sourceBlock = blocks[activeSourceBlockId] - if (!sourceBlock) return { tags: [...variableTags] } + // Create block tag groups from accessible blocks + const blockTagGroups: BlockTagGroup[] = [] + const allBlockTags: string[] = [] - const blockName = sourceBlock.name || sourceBlock.type + for (const accessibleBlockId of accessibleBlockIds) { + const accessibleBlock = blocks[accessibleBlockId] + if (!accessibleBlock) continue + + const blockConfig = getBlock(accessibleBlock.type) + if (!blockConfig) continue + + const blockName = accessibleBlock.name || accessibleBlock.type const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() - // First check for evaluator metrics - if (sourceBlock.type === 'evaluator') { - try { - const metricsValue = useSubBlockStore - .getState() - .getValue(activeSourceBlockId, 'metrics') as unknown as Metric[] - if (Array.isArray(metricsValue)) { - return { - tags: [ - ...variableTags, - ...metricsValue.map( - (metric) => `${normalizedBlockName}.response.${metric.name.toLowerCase()}` - ), - ], - } - } - } catch (e) { - logger.error('Error parsing metrics:', { e }) - } + // Handle blocks with no outputs (like starter) - show as just + let blockTags: string[] + if (Object.keys(blockConfig.outputs).length === 0) { + blockTags = [normalizedBlockName] + } else { + const outputPaths = generateOutputPaths(blockConfig.outputs) + blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) } - // Then check for response format - try { - const responseFormatValue = useSubBlockStore - .getState() - .getValue(activeSourceBlockId, 'responseFormat') - if (responseFormatValue) { - const responseFormat = - typeof responseFormatValue === 'string' - ? JSON.parse(responseFormatValue) - : responseFormatValue + blockTagGroups.push({ + blockName, + blockId: accessibleBlockId, + blockType: accessibleBlock.type, + tags: blockTags, + distance: blockDistances[accessibleBlockId] || 0, + }) - if (responseFormat) { - const fields = extractFieldsFromSchema(responseFormat) - if (fields.length > 0) { - return { - tags: [ - ...variableTags, - ...fields.map((field: Field) => `${normalizedBlockName}.response.${field.name}`), - ], - } - } - } - } - } catch (e) { - logger.error('Error parsing response format:', { e }) - } - - // Fall back to default outputs if no response format - const outputPaths = getOutputPaths(sourceBlock.outputs, '', sourceBlock.type === 'starter') - return { - tags: [...variableTags, ...outputPaths.map((path) => `${normalizedBlockName}.${path}`)], - } + allBlockTags.push(...blockTags) } - // Find parallel and loop blocks connected via end-source handles - const endSourceConnections: ConnectedBlock[] = [] + // Sort block groups by distance (closest first) + blockTagGroups.sort((a, b) => a.distance - b.distance) - // Get all edges that connect to this block - const incomingEdges = useWorkflowStore - .getState() - .edges.filter((edge) => edge.target === blockId) - - for (const edge of incomingEdges) { - const sourceBlock = blocks[edge.source] - if (!sourceBlock) continue - - // Check if this is a parallel-end-source or loop-end-source connection - if (edge.sourceHandle === 'parallel-end-source' && sourceBlock.type === 'parallel') { - const blockName = sourceBlock.name || sourceBlock.type - const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() - - // Add the parallel block as a referenceable block with its aggregated results - endSourceConnections.push({ - id: sourceBlock.id, - type: sourceBlock.type, - outputType: ['response'], - name: blockName, - responseFormat: { - fields: [ - { - name: 'completed', - type: 'boolean', - description: 'Whether all executions completed', - }, - { - name: 'results', - type: 'array', - description: 'Aggregated results from all parallel executions', - }, - { name: 'message', type: 'string', description: 'Status message' }, - ], - }, - }) - } else if (edge.sourceHandle === 'loop-end-source' && sourceBlock.type === 'loop') { - const blockName = sourceBlock.name || sourceBlock.type - const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() - - // Add the loop block as a referenceable block with its aggregated results - endSourceConnections.push({ - id: sourceBlock.id, - type: sourceBlock.type, - outputType: ['response'], - name: blockName, - responseFormat: { - fields: [ - { - name: 'completed', - type: 'boolean', - description: 'Whether all iterations completed', - }, - { - name: 'results', - type: 'array', - description: 'Aggregated results from all loop iterations', - }, - { name: 'message', type: 'string', description: 'Status message' }, - ], - }, - }) - } + return { + tags: [...variableTags, ...loopTags, ...parallelTags, ...allBlockTags], + variableInfoMap, + blockTagGroups, } - - // Use all incoming connections plus end-source connections - const allConnections = [...incomingConnections, ...endSourceConnections] - - const sourceTags = allConnections.flatMap((connection: ConnectedBlock) => { - const blockName = connection.name || connection.type - const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() - - // Extract fields from response format - if (connection.responseFormat) { - const fields = extractFieldsFromSchema(connection.responseFormat) - if (fields.length > 0) { - return fields.map((field: Field) => `${normalizedBlockName}.response.${field.name}`) - } - } - - // For evaluator blocks, use metrics - if (connection.type === 'evaluator') { - try { - const metricsValue = useSubBlockStore - .getState() - .getValue(connection.id, 'metrics') as unknown as Metric[] - if (Array.isArray(metricsValue)) { - return metricsValue.map( - (metric) => `${normalizedBlockName}.response.${metric.name.toLowerCase()}` - ) - } - } catch (e) { - logger.error('Error parsing metrics:', { e }) - return [] - } - } - - // Fall back to default outputs if no response format - const sourceBlock = blocks[connection.id] - if (!sourceBlock) return [] - - const outputPaths = getOutputPaths(sourceBlock.outputs, '', sourceBlock.type === 'starter') - return outputPaths.map((path) => `${normalizedBlockName}.${path}`) - }) - - return { tags: [...variableTags, ...loopTags, ...parallelTags, ...sourceTags], variableInfoMap } - }, [ - blocks, - incomingConnections, - blockId, - activeSourceBlockId, - workflowVariables, - loops, - parallels, - ]) + }, [blocks, edges, loops, parallels, blockId, activeSourceBlockId, workflowVariables]) // Filter tags based on search term const filteredTags = useMemo(() => { @@ -403,12 +334,11 @@ export const TagDropdown: React.FC = ({ return tags.filter((tag: string) => tag.toLowerCase().includes(searchTerm)) }, [tags, searchTerm]) - // Group tags into variables, loops, and blocks - const { variableTags, loopTags, parallelTags, blockTags } = useMemo(() => { + // Group filtered tags by category + const { variableTags, loopTags, parallelTags, filteredBlockTagGroups } = useMemo(() => { const varTags: string[] = [] const loopTags: string[] = [] const parTags: string[] = [] - const blkTags: string[] = [] filteredTags.forEach((tag) => { if (tag.startsWith('variable.')) { @@ -417,20 +347,32 @@ export const TagDropdown: React.FC = ({ loopTags.push(tag) } else if (tag.startsWith('parallel.')) { parTags.push(tag) - } else { - blkTags.push(tag) } }) - return { variableTags: varTags, loopTags: loopTags, parallelTags: parTags, blockTags: blkTags } - }, [filteredTags]) + // Filter block tag groups based on search term + const filteredBlockTagGroups = blockTagGroups + .map((group) => ({ + ...group, + tags: group.tags.filter((tag) => !searchTerm || tag.toLowerCase().includes(searchTerm)), + })) + .filter((group) => group.tags.length > 0) - // Create ordered tags array that matches the display order for keyboard navigation + return { + variableTags: varTags, + loopTags: loopTags, + parallelTags: parTags, + filteredBlockTagGroups, + } + }, [filteredTags, blockTagGroups, searchTerm]) + + // Create ordered tags for keyboard navigation const orderedTags = useMemo(() => { - return [...variableTags, ...loopTags, ...parallelTags, ...blockTags] - }, [variableTags, loopTags, parallelTags, blockTags]) + const allBlockTags = filteredBlockTagGroups.flatMap((group) => group.tags) + return [...variableTags, ...loopTags, ...parallelTags, ...allBlockTags] + }, [variableTags, loopTags, parallelTags, filteredBlockTagGroups]) - // Create a map for efficient tag index lookups + // Create efficient tag index lookup map const tagIndexMap = useMemo(() => { const map = new Map() orderedTags.forEach((tag, index) => { @@ -439,19 +381,7 @@ export const TagDropdown: React.FC = ({ return map }, [orderedTags]) - // Reset selection when filtered results change - useEffect(() => { - setSelectedIndex(0) - }, [searchTerm]) - - // Ensure selectedIndex stays within bounds when orderedTags changes - useEffect(() => { - if (selectedIndex >= orderedTags.length) { - setSelectedIndex(Math.max(0, orderedTags.length - 1)) - } - }, [orderedTags.length, selectedIndex]) - - // Handle tag selection + // Handle tag selection and text replacement const handleTagSelect = useCallback( (tag: string) => { const textBeforeCursor = inputValue.slice(0, cursorPosition) @@ -461,34 +391,26 @@ export const TagDropdown: React.FC = ({ const lastOpenBracket = textBeforeCursor.lastIndexOf('<') if (lastOpenBracket === -1) return - // Process the tag if it's a variable tag + // Process variable tags to maintain compatibility let processedTag = tag if (tag.startsWith('variable.')) { - // Get the variable name from the tag (after 'variable.') const variableName = tag.substring('variable.'.length) - - // Find the variable in the store by name const variableObj = Object.values(variables).find( (v) => v.name.replace(/\s+/g, '') === variableName ) - // We still use the full tag format internally to maintain compatibility if (variableObj) { processedTag = tag } } - // Check if there's a closing bracket in textAfterCursor that belongs to the current tag - // Find the first '>' in textAfterCursor (if any) + // Handle existing closing bracket const nextCloseBracket = textAfterCursor.indexOf('>') let remainingTextAfterCursor = textAfterCursor - // If there's a '>' right after the cursor or with only whitespace/tag content in between, - // it's likely part of the existing tag being edited, so we should skip it if (nextCloseBracket !== -1) { const textBetween = textAfterCursor.slice(0, nextCloseBracket) - // If the text between cursor and '>' contains only tag-like characters (letters, dots, numbers) - // then it's likely part of the current tag being edited + // If text between cursor and '>' contains only tag-like characters, skip it if (/^[a-zA-Z0-9._]*$/.test(textBetween)) { remainingTextAfterCursor = textAfterCursor.slice(nextCloseBracket + 1) } @@ -502,7 +424,17 @@ export const TagDropdown: React.FC = ({ [inputValue, cursorPosition, variables, onSelect, onClose] ) - // Add and remove keyboard event listener + // Reset selection when search results change + useEffect(() => setSelectedIndex(0), [searchTerm]) + + // Keep selection within bounds when tags change + useEffect(() => { + if (selectedIndex >= orderedTags.length) { + setSelectedIndex(Math.max(0, orderedTags.length - 1)) + } + }, [orderedTags.length, selectedIndex]) + + // Handle keyboard navigation useEffect(() => { if (visible) { const handleKeyboardEvent = (e: KeyboardEvent) => { @@ -539,7 +471,7 @@ export const TagDropdown: React.FC = ({ } }, [visible, selectedIndex, orderedTags, handleTagSelect, onClose]) - // Don't render if not visible or no tags + // Early return if dropdown should not be visible if (!visible || tags.length === 0 || orderedTags.length === 0) return null return ( @@ -555,6 +487,7 @@ export const TagDropdown: React.FC = ({
    No matching tags found
    ) : ( <> + {/* Variables section */} {variableTags.length > 0 && ( <>
    @@ -578,8 +511,8 @@ export const TagDropdown: React.FC = ({ )} onMouseEnter={() => setSelectedIndex(tagIndex >= 0 ? tagIndex : 0)} onMouseDown={(e) => { - e.preventDefault() // Prevent input blur - e.stopPropagation() // Prevent event bubbling + e.preventDefault() + e.stopPropagation() handleTagSelect(tag) }} onClick={(e) => { @@ -609,6 +542,7 @@ export const TagDropdown: React.FC = ({ )} + {/* Loop section */} {loopTags.length > 0 && ( <> {variableTags.length > 0 &&
    } @@ -620,10 +554,10 @@ export const TagDropdown: React.FC = ({ const tagIndex = tagIndexMap.get(tag) ?? -1 const loopProperty = tag.split('.')[1] - // Choose appropriate icon/label based on type + // Choose appropriate icon and description based on loop property let tagIcon = 'L' let tagDescription = '' - const bgColor = '#8857E6' // Purple for loop variables + const bgColor = '#8857E6' if (loopProperty === 'currentItem') { tagIcon = 'i' @@ -649,8 +583,8 @@ export const TagDropdown: React.FC = ({ )} onMouseEnter={() => setSelectedIndex(tagIndex >= 0 ? tagIndex : 0)} onMouseDown={(e) => { - e.preventDefault() // Prevent input blur - e.stopPropagation() // Prevent event bubbling + e.preventDefault() + e.stopPropagation() handleTagSelect(tag) }} onClick={(e) => { @@ -676,6 +610,7 @@ export const TagDropdown: React.FC = ({ )} + {/* Parallel section */} {parallelTags.length > 0 && ( <> {loopTags.length > 0 &&
    } @@ -687,10 +622,10 @@ export const TagDropdown: React.FC = ({ const tagIndex = tagIndexMap.get(tag) ?? -1 const parallelProperty = tag.split('.')[1] - // Choose appropriate icon/label based on type + // Choose appropriate icon and description based on parallel property let tagIcon = 'P' let tagDescription = '' - const bgColor = '#FF5757' // Red for parallel variables + const bgColor = '#FF5757' if (parallelProperty === 'currentItem') { tagIcon = 'i' @@ -716,8 +651,8 @@ export const TagDropdown: React.FC = ({ )} onMouseEnter={() => setSelectedIndex(tagIndex >= 0 ? tagIndex : 0)} onMouseDown={(e) => { - e.preventDefault() // Prevent input blur - e.stopPropagation() // Prevent event bubbling + e.preventDefault() + e.stopPropagation() handleTagSelect(tag) }} onClick={(e) => { @@ -743,68 +678,72 @@ export const TagDropdown: React.FC = ({ )} - {blockTags.length > 0 && ( + {/* Block sections */} + {filteredBlockTagGroups.length > 0 && ( <> {(variableTags.length > 0 || loopTags.length > 0 || parallelTags.length > 0) && (
    )} -
    - Blocks -
    -
    - {blockTags.map((tag: string) => { - const tagIndex = tagIndexMap.get(tag) ?? -1 + {filteredBlockTagGroups.map((group) => { + // Get block color from configuration + const blockConfig = getBlock(group.blockType) + const blockColor = blockConfig?.bgColor || '#2F55FF' - // Get block name from tag (first part before the dot) - const blockName = tag.split('.')[0] + return ( +
    +
    + {group.blockName} +
    +
    + {group.tags.map((tag: string) => { + const tagIndex = tagIndexMap.get(tag) ?? -1 + // Extract path after block name (e.g., "field" from "blockname.field") + // For root reference blocks, show the block name instead of empty path + const tagParts = tag.split('.') + const path = tagParts.slice(1).join('.') + const displayText = path || group.blockName - // Get block type from blocks - const blockType = Object.values(blocks).find( - (block) => - (block.name || block.type || '').replace(/\s+/g, '').toLowerCase() === - blockName - )?.type - - // Get block color from block config - const blockConfig = blockType ? getBlock(blockType) : null - const blockColor = blockConfig?.bgColor || '#2F55FF' // Default to blue if not found - - return ( - - ) - })} -
    + return ( + + ) + })} +
    +
    + ) + })} )} @@ -813,18 +752,3 @@ export const TagDropdown: React.FC = ({
    ) } - -// Helper function to check for '<' trigger -export const checkTagTrigger = (text: string, cursorPosition: number): { show: boolean } => { - if (cursorPosition >= 1) { - const textBeforeCursor = text.slice(0, cursorPosition) - const lastOpenBracket = textBeforeCursor.lastIndexOf('<') - const lastCloseBracket = textBeforeCursor.lastIndexOf('>') - - // Show if we have an unclosed '<' that's not part of a completed tag - if (lastOpenBracket !== -1 && (lastCloseBracket === -1 || lastCloseBracket < lastOpenBracket)) { - return { show: true } - } - } - return { show: false } -} diff --git a/apps/sim/contexts/socket-context.tsx b/apps/sim/contexts/socket-context.tsx index 914496982..553a045d0 100644 --- a/apps/sim/contexts/socket-context.tsx +++ b/apps/sim/contexts/socket-context.tsx @@ -168,7 +168,6 @@ export function SocketProvider({ children, user }: SocketProviderProps) { socketInstance.on('connect', () => { setIsConnected(true) setIsConnecting(false) - logger.info('Socket connected successfully', { socketId: socketInstance.id, connected: socketInstance.connected, diff --git a/apps/sim/db/migrations/0050_big_mattie_franklin.sql b/apps/sim/db/migrations/0050_big_mattie_franklin.sql new file mode 100644 index 000000000..5e08f6229 --- /dev/null +++ b/apps/sim/db/migrations/0050_big_mattie_franklin.sql @@ -0,0 +1 @@ +ALTER TABLE "settings" ADD COLUMN "auto_pan" boolean DEFAULT true NOT NULL; \ No newline at end of file diff --git a/apps/sim/db/migrations/meta/0050_snapshot.json b/apps/sim/db/migrations/meta/0050_snapshot.json new file mode 100644 index 000000000..3188a425b --- /dev/null +++ b/apps/sim/db/migrations/meta/0050_snapshot.json @@ -0,0 +1,4468 @@ +{ + "id": "0057a619-6f4a-4eb6-ba10-1199608959e9", + "prevId": "3b0b9bbe-390d-4b9d-b2d1-ba3dde284edb", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subdomain_idx": { + "name": "subdomain_idx", + "columns": [ + { + "expression": "subdomain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_uploaded_at_idx": { + "name": "doc_kb_uploaded_at_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_metadata_gin_idx": { + "name": "emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 100, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketplace": { + "name": "marketplace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "marketplace_workflow_id_workflow_id_fk": { + "name": "marketplace_workflow_id_workflow_id_fk", + "tableFrom": "marketplace", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "marketplace_author_id_user_id_fk": { + "name": "marketplace_author_id_user_id_fk", + "tableFrom": "marketplace", + "tableTo": "user", + "columnsFrom": ["author_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_idx": { + "name": "memory_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_key_idx": { + "name": "memory_workflow_key_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workflow_id_workflow_id_fk": { + "name": "memory_workflow_id_workflow_id_fk", + "tableFrom": "memory", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "debug_mode": { + "name": "debug_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_fill_env_vars": { + "name": "auto_fill_env_vars", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_pan": { + "name": "auto_pan", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_notified_user": { + "name": "telemetry_notified_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "general": { + "name": "general", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR (metadata IS NOT NULL AND (metadata->>'perSeatAllowance' IS NOT NULL OR metadata->>'totalAllowance' IS NOT NULL))" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'5'" + }, + "usage_limit_set_by": { + "name": "usage_limit_set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_state": { + "name": "deployed_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "collaborators": { + "name": "collaborators", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketplace_data": { + "name": "marketplace_data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extent": { + "name": "extent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_parent_id_idx": { + "name": "workflow_blocks_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_parent_idx": { + "name": "workflow_blocks_workflow_parent_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_type_idx": { + "name": "workflow_blocks_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_source_block_idx": { + "name": "workflow_edges_source_block_idx", + "columns": [ + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_target_block_idx": { + "name": "workflow_edges_target_block_idx", + "columns": [ + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_blocks": { + "name": "workflow_execution_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_name": { + "name": "block_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_type": { + "name": "block_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_stack_trace": { + "name": "error_stack_trace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_data": { + "name": "input_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output_data": { + "name": "output_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_input": { + "name": "cost_input", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "cost_output": { + "name": "cost_output", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "tokens_prompt": { + "name": "tokens_prompt", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_completion": { + "name": "tokens_completion", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_total": { + "name": "tokens_total", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_blocks_execution_id_idx": { + "name": "execution_blocks_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_workflow_id_idx": { + "name": "execution_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_block_id_idx": { + "name": "execution_blocks_block_id_idx", + "columns": [ + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_status_idx": { + "name": "execution_blocks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_duration_idx": { + "name": "execution_blocks_duration_idx", + "columns": [ + { + "expression": "duration_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_cost_idx": { + "name": "execution_blocks_cost_idx", + "columns": [ + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_workflow_execution_idx": { + "name": "execution_blocks_workflow_execution_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_execution_status_idx": { + "name": "execution_blocks_execution_status_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_started_at_idx": { + "name": "execution_blocks_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_execution_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "block_count": { + "name": "block_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "success_count": { + "name": "success_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_count": { + "name": "error_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_input_cost": { + "name": "total_input_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_output_cost": { + "name": "total_output_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_idx": { + "name": "workflow_execution_logs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_cost_idx": { + "name": "workflow_execution_logs_cost_idx", + "columns": [ + { + "expression": "total_cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_duration_idx": { + "name": "workflow_execution_logs_duration_idx", + "columns": [ + { + "expression": "total_duration_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_logs": { + "name": "workflow_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_logs_workflow_id_workflow_id_fk": { + "name": "workflow_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workflow_schedule_workflow_id_unique": { + "name": "workflow_schedule_workflow_id_unique", + "nullsNotDistinct": false, + "columns": ["workflow_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_member": { + "name": "workspace_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_workspace_idx": { + "name": "user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_member_workspace_id_workspace_id_fk": { + "name": "workspace_member_workspace_id_workspace_id_fk", + "tableFrom": "workspace_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_member_user_id_user_id_fk": { + "name": "workspace_member_user_id_user_id_fk", + "tableFrom": "workspace_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json index e89a32e95..281411d9b 100644 --- a/apps/sim/db/migrations/meta/_journal.json +++ b/apps/sim/db/migrations/meta/_journal.json @@ -344,6 +344,13 @@ "when": 1751430703326, "tag": "0049_fancy_cardiac", "breakpoints": true + }, + { + "idx": 50, + "version": "7", + "when": 1751659528896, + "tag": "0050_big_mattie_franklin", + "breakpoints": true } ] } diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index 376a8b4eb..9a6587582 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -394,6 +394,7 @@ export const settings = pgTable('settings', { debugMode: boolean('debug_mode').notNull().default(false), autoConnect: boolean('auto_connect').notNull().default(true), autoFillEnvVars: boolean('auto_fill_env_vars').notNull().default(true), + autoPan: boolean('auto_pan').notNull().default(true), // Privacy settings telemetryEnabled: boolean('telemetry_enabled').notNull().default(true), diff --git a/apps/sim/executor/__test-utils__/executor-mocks.ts b/apps/sim/executor/__test-utils__/executor-mocks.ts index a37187ae5..6a058981b 100644 --- a/apps/sim/executor/__test-utils__/executor-mocks.ts +++ b/apps/sim/executor/__test-utils__/executor-mocks.ts @@ -15,7 +15,7 @@ export const createMockHandler = ( block.metadata?.id === handlerName || handlerName === 'generic' const defaultExecuteResult = { - response: { result: `${handlerName} executed` }, + result: `${handlerName} executed`, } return vi.fn().mockImplementation(() => ({ @@ -614,12 +614,8 @@ export const createFunctionBlockHandler = vi.fn().mockImplementation(() => ({ canHandle: (block: any) => block.metadata?.id === 'function', execute: vi.fn().mockImplementation(async (block, inputs) => { return { - response: { - result: inputs.code - ? new Function(inputs.code)() - : { key: inputs.key, value: inputs.value }, - stdout: '', - }, + result: inputs.code ? new Function(inputs.code)() : { key: inputs.key, value: inputs.value }, + stdout: '', } }), })) @@ -679,13 +675,11 @@ export const createParallelBlockHandler = vi.fn().mockImplementation(() => { } return { - response: { - parallelId, - parallelCount, - distributionType: 'distributed', - started: true, - message: `Initialized ${parallelCount} parallel executions`, - }, + parallelId, + parallelCount, + distributionType: 'distributed', + started: true, + message: `Initialized ${parallelCount} parallel executions`, } } @@ -714,22 +708,18 @@ export const createParallelBlockHandler = vi.fn().mockImplementation(() => { } return { - response: { - parallelId, - parallelCount: parallelState.parallelCount, - completed: true, - message: `Completed all ${parallelState.parallelCount} executions`, - }, + parallelId, + parallelCount: parallelState.parallelCount, + completed: true, + message: `Completed all ${parallelState.parallelCount} executions`, } } return { - response: { - parallelId, - parallelCount: parallelState.parallelCount, - waiting: true, - message: 'Waiting for iterations to complete', - }, + parallelId, + parallelCount: parallelState.parallelCount, + waiting: true, + message: 'Waiting for iterations to complete', } }), } diff --git a/apps/sim/executor/__test-utils__/test-executor.ts b/apps/sim/executor/__test-utils__/test-executor.ts index 61ce054d3..973da3f0c 100644 --- a/apps/sim/executor/__test-utils__/test-executor.ts +++ b/apps/sim/executor/__test-utils__/test-executor.ts @@ -26,7 +26,7 @@ export class TestExecutor extends Executor { return { success: true, output: { - response: { result: 'Test execution completed' }, + result: 'Test execution completed', } as NormalizedBlockOutput, logs: [], metadata: { @@ -39,7 +39,7 @@ export class TestExecutor extends Executor { // If validation fails, return a failure result return { success: false, - output: { response: {} } as NormalizedBlockOutput, + output: {} as NormalizedBlockOutput, error: error.message, logs: [], } diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index 404bf092d..b2e0235b8 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -210,14 +210,12 @@ describe('AgentBlockHandler', () => { mockGetProviderFromModel.mockReturnValue('openai') const expectedOutput = { - response: { - content: 'Mocked response content', - model: 'mock-model', - tokens: { prompt: 10, completion: 20, total: 30 }, - toolCalls: { list: [], count: 0 }, - providerTiming: { total: 100 }, - cost: 0.001, - }, + content: 'Mocked response content', + model: 'mock-model', + tokens: { prompt: 10, completion: 20, total: 30 }, + toolCalls: { list: [], count: 0 }, + providerTiming: { total: 100 }, + cost: 0.001, } const result = await handler.execute(mockBlock, inputs, mockContext) @@ -587,14 +585,12 @@ describe('AgentBlockHandler', () => { mockGetProviderFromModel.mockReturnValue('openai') const expectedOutput = { - response: { - content: 'Mocked response content', - model: 'mock-model', - tokens: { prompt: 10, completion: 20, total: 30 }, - toolCalls: { list: [], count: 0 }, // Assuming no tool calls in this mock response - providerTiming: { total: 100 }, - cost: 0.001, - }, + content: 'Mocked response content', + model: 'mock-model', + tokens: { prompt: 10, completion: 20, total: 30 }, + toolCalls: { list: [], count: 0 }, // Assuming no tool calls in this mock response + providerTiming: { total: 100 }, + cost: 0.001, } const result = await handler.execute(mockBlock, inputs, mockContext) @@ -691,14 +687,12 @@ describe('AgentBlockHandler', () => { const result = await handler.execute(mockBlock, inputs, mockContext) expect(result).toEqual({ - response: { - result: 'Success', - score: 0.95, - tokens: { prompt: 10, completion: 20, total: 30 }, - toolCalls: { list: [], count: 0 }, - providerTiming: { total: 100 }, - cost: undefined, - }, + result: 'Success', + score: 0.95, + tokens: { prompt: 10, completion: 20, total: 30 }, + toolCalls: { list: [], count: 0 }, + providerTiming: { total: 100 }, + cost: undefined, }) }) @@ -733,13 +727,12 @@ describe('AgentBlockHandler', () => { const result = await handler.execute(mockBlock, inputs, mockContext) expect(result).toEqual({ - response: { - content: 'Regular text response', - model: 'mock-model', - tokens: { prompt: 10, completion: 20, total: 30 }, - toolCalls: { list: [], count: 0 }, - providerTiming: { total: 100 }, - }, + content: 'Regular text response', + model: 'mock-model', + tokens: { prompt: 10, completion: 20, total: 30 }, + toolCalls: { list: [], count: 0 }, + providerTiming: { total: 100 }, + cost: undefined, }) }) @@ -793,7 +786,7 @@ describe('AgentBlockHandler', () => { stream: mockStreamBody, execution: { success: true, - output: { response: {} }, + output: {}, logs: [], metadata: { duration: 0, @@ -821,7 +814,7 @@ describe('AgentBlockHandler', () => { expect((result as StreamingExecution).execution).toHaveProperty('success', true) expect((result as StreamingExecution).execution).toHaveProperty('output') - expect((result as StreamingExecution).execution.output).toHaveProperty('response') + expect((result as StreamingExecution).execution.output).toBeDefined() expect((result as StreamingExecution).execution).toHaveProperty('logs') }) @@ -835,11 +828,9 @@ describe('AgentBlockHandler', () => { const mockExecutionData = { success: true, output: { - response: { - content: '', - model: 'mock-model', - tokens: { prompt: 10, completion: 20, total: 30 }, - }, + content: '', + model: 'mock-model', + tokens: { prompt: 10, completion: 20, total: 30 }, }, logs: [ { @@ -891,7 +882,7 @@ describe('AgentBlockHandler', () => { expect(result).toHaveProperty('execution') expect((result as StreamingExecution).execution.success).toBe(true) - expect((result as StreamingExecution).execution.output.response.model).toBe('mock-model') + expect((result as StreamingExecution).execution.output.model).toBe('mock-model') const logs = (result as StreamingExecution).execution.logs expect(logs?.length).toBe(1) if (logs && logs.length > 0 && logs[0]) { @@ -918,11 +909,9 @@ describe('AgentBlockHandler', () => { execution: { success: true, output: { - response: { - content: 'Test streaming content', - model: 'gpt-4o', - tokens: { prompt: 10, completion: 5, total: 15 }, - }, + content: 'Test streaming content', + model: 'gpt-4o', + tokens: { prompt: 10, completion: 5, total: 15 }, }, logs: [], metadata: { @@ -950,10 +939,8 @@ describe('AgentBlockHandler', () => { expect(result).toHaveProperty('execution') expect((result as StreamingExecution).execution.success).toBe(true) - expect((result as StreamingExecution).execution.output.response.content).toBe( - 'Test streaming content' - ) - expect((result as StreamingExecution).execution.output.response.model).toBe('gpt-4o') + expect((result as StreamingExecution).execution.output.content).toBe('Test streaming content') + expect((result as StreamingExecution).execution.output.model).toBe('gpt-4o') }) it('should process memories in advanced mode with system prompt and user prompt', async () => { @@ -1006,18 +993,16 @@ describe('AgentBlockHandler', () => { systemPrompt: 'You are a helpful assistant.', userPrompt: 'Continue our conversation.', memories: { - response: { - memories: [ - { - key: 'conversation-1', - type: 'agent', - data: [ - { role: 'user', content: 'Hi there!' }, - { role: 'assistant', content: 'Hello! How can I help you?' }, - ], - }, - ], - }, + memories: [ + { + key: 'conversation-1', + type: 'agent', + data: [ + { role: 'user', content: 'Hi there!' }, + { role: 'assistant', content: 'Hello! How can I help you?' }, + ], + }, + ], }, apiKey: 'test-api-key', } diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 3bae10043..aeb2fae04 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -194,9 +194,7 @@ export class AgentBlockHandler implements BlockHandler { if (!memories) return [] let memoryArray: any[] = [] - if (memories?.response?.memories && Array.isArray(memories.response.memories)) { - memoryArray = memories.response.memories - } else if (memories?.memories && Array.isArray(memories.memories)) { + if (memories?.memories && Array.isArray(memories.memories)) { memoryArray = memories.memories } else if (Array.isArray(memories)) { memoryArray = memories @@ -473,7 +471,7 @@ export class AgentBlockHandler implements BlockHandler { stream: response.body!, execution: { success: executionData.success, - output: executionData.output || { response: {} }, + output: executionData.output || {}, error: executionData.error, logs: [], // Logs are stripped from headers, will be populated by executor metadata: executionData.metadata || { @@ -621,7 +619,7 @@ export class AgentBlockHandler implements BlockHandler { const streamingExec = response as StreamingExecution logger.info(`Received StreamingExecution for block ${block.id}`) - if (streamingExec.execution.output?.response) { + if (streamingExec.execution.output) { const execution = streamingExec.execution as any if (block.metadata?.name) execution.blockName = block.metadata.name if (block.metadata?.id) execution.blockType = block.metadata.id @@ -637,7 +635,7 @@ export class AgentBlockHandler implements BlockHandler { stream, execution: { success: true, - output: { response: {} }, + output: {}, logs: [], metadata: { duration: 0, @@ -667,10 +665,8 @@ export class AgentBlockHandler implements BlockHandler { try { const parsedContent = JSON.parse(result.content) return { - response: { - ...parsedContent, - ...this.createResponseMetadata(result), - }, + ...parsedContent, + ...this.createResponseMetadata(result), } } catch (error) { logger.error('Failed to parse response content:', { error }) @@ -680,11 +676,9 @@ export class AgentBlockHandler implements BlockHandler { private processStandardResponse(result: any): BlockOutput { return { - response: { - content: result.content, - model: result.model, - ...this.createResponseMetadata(result), - }, + content: result.content, + model: result.model, + ...this.createResponseMetadata(result), } } diff --git a/apps/sim/executor/handlers/api/api-handler.test.ts b/apps/sim/executor/handlers/api/api-handler.test.ts index 32d258df1..54927db80 100644 --- a/apps/sim/executor/handlers/api/api-handler.test.ts +++ b/apps/sim/executor/handlers/api/api-handler.test.ts @@ -92,7 +92,7 @@ describe('ApiBlockHandler', () => { body: JSON.stringify({ key: 'value' }), } - const expectedOutput = { response: { data: 'Success' } } + const expectedOutput = { data: 'Success' } mockExecuteTool.mockResolvedValue({ success: true, output: { data: 'Success' } }) @@ -113,7 +113,7 @@ describe('ApiBlockHandler', () => { method: 'GET', } - const expectedOutput = { response: { content: '', success: true } } + const expectedOutput = { data: null, status: 200, headers: {} } const result = await handler.execute(mockBlock, inputs, mockContext) diff --git a/apps/sim/executor/handlers/api/api-handler.ts b/apps/sim/executor/handlers/api/api-handler.ts index 89740c8ca..31c7df53d 100644 --- a/apps/sim/executor/handlers/api/api-handler.ts +++ b/apps/sim/executor/handlers/api/api-handler.ts @@ -1,5 +1,4 @@ import { createLogger } from '@/lib/logs/console-logger' -import type { BlockOutput } from '@/blocks/types' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' import { getTool } from '@/tools/utils' @@ -19,7 +18,7 @@ export class ApiBlockHandler implements BlockHandler { block: SerializedBlock, inputs: Record, context: ExecutionContext - ): Promise { + ): Promise { const tool = getTool(block.config.tool) if (!tool) { throw new Error(`Tool not found: ${block.config.tool}`) @@ -27,7 +26,7 @@ export class ApiBlockHandler implements BlockHandler { // Early return with empty success response if URL is not provided or empty if (tool.name?.includes('HTTP') && (!inputs.url || inputs.url.trim() === '')) { - return { response: { content: '', success: true } } + return { data: null, status: 200, headers: {} } } // Pre-validate common HTTP request issues to provide better error messages @@ -154,7 +153,7 @@ export class ApiBlockHandler implements BlockHandler { throw error } - return { response: result.output } + return result.output } catch (error: any) { // Ensure we have a meaningful error message if (!error.message || error.message === 'undefined (undefined)') { diff --git a/apps/sim/executor/handlers/condition/condition-handler.test.ts b/apps/sim/executor/handlers/condition/condition-handler.test.ts index 040184ecd..9d7e91cbc 100644 --- a/apps/sim/executor/handlers/condition/condition-handler.test.ts +++ b/apps/sim/executor/handlers/condition/condition-handler.test.ts @@ -96,7 +96,7 @@ describe('ConditionBlockHandler', () => { [ mockSourceBlock.id, { - output: { response: { value: 10, text: 'hello' } }, + output: { value: 10, text: 'hello' }, executed: true, executionTime: 100, }, @@ -129,32 +129,30 @@ describe('ConditionBlockHandler', () => { it('should execute condition block correctly and select first path', async () => { const conditions = [ - { id: 'cond1', title: 'if', value: 'context.response.value > 5' }, + { id: 'cond1', title: 'if', value: 'context.value > 5' }, { id: 'else1', title: 'else', value: '' }, ] const inputs = { conditions: JSON.stringify(conditions) } const expectedOutput = { - response: { - value: 10, - text: 'hello', - conditionResult: true, - selectedPath: { - blockId: mockTargetBlock1.id, - blockType: 'target', - blockTitle: 'Target Block 1', - }, - selectedConditionId: 'cond1', + value: 10, + text: 'hello', + conditionResult: true, + selectedPath: { + blockId: mockTargetBlock1.id, + blockType: 'target', + blockTitle: 'Target Block 1', }, + selectedConditionId: 'cond1', } // Mock directly in the test - mockResolver.resolveBlockReferences.mockReturnValue('context.response.value > 5') + mockResolver.resolveBlockReferences.mockReturnValue('context.value > 5') - const result = (await handler.execute(mockBlock, inputs, mockContext)) as { response: any } + const result = await handler.execute(mockBlock, inputs, mockContext) expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith( - 'context.response.value > 5', + 'context.value > 5', mockContext, mockBlock ) @@ -170,23 +168,21 @@ describe('ConditionBlockHandler', () => { const inputs = { conditions: JSON.stringify(conditions) } const expectedOutput = { - response: { - value: 10, - text: 'hello', - conditionResult: true, - selectedPath: { - blockId: mockTargetBlock2.id, - blockType: 'target', - blockTitle: 'Target Block 2', - }, - selectedConditionId: 'else1', + value: 10, + text: 'hello', + conditionResult: true, + selectedPath: { + blockId: mockTargetBlock2.id, + blockType: 'target', + blockTitle: 'Target Block 2', }, + selectedConditionId: 'else1', } // Mock directly in the test mockResolver.resolveBlockReferences.mockReturnValue('context.value < 0') - const result = (await handler.execute(mockBlock, inputs, mockContext)) as { response: any } + const result = await handler.execute(mockBlock, inputs, mockContext) expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith( 'context.value < 0', @@ -207,7 +203,7 @@ describe('ConditionBlockHandler', () => { it('should resolve references in conditions before evaluation', async () => { const conditions = [ - { id: 'cond1', title: 'if', value: '{{source-block-1.response.value}} > 5' }, + { id: 'cond1', title: 'if', value: '{{source-block-1.value}} > 5' }, { id: 'else1', title: 'else', value: '' }, ] const inputs = { conditions: JSON.stringify(conditions) } @@ -215,10 +211,10 @@ describe('ConditionBlockHandler', () => { // Mock directly in the test mockResolver.resolveBlockReferences.mockReturnValue('10 > 5') - const _result = (await handler.execute(mockBlock, inputs, mockContext)) as { response: any } + const _result = await handler.execute(mockBlock, inputs, mockContext) expect(mockResolver.resolveBlockReferences).toHaveBeenCalledWith( - '{{source-block-1.response.value}} > 5', + '{{source-block-1.value}} > 5', mockContext, mockBlock ) @@ -320,9 +316,9 @@ describe('ConditionBlockHandler', () => { // Mock directly in the test mockResolver.resolveBlockReferences.mockReturnValue('context.item === "apple"') - const result = (await handler.execute(mockBlock, inputs, mockContext)) as { response: any } + const result = await handler.execute(mockBlock, inputs, mockContext) expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1') - expect(result.response.selectedConditionId).toBe('cond1') + expect((result as any).selectedConditionId).toBe('cond1') }) }) diff --git a/apps/sim/executor/handlers/condition/condition-handler.ts b/apps/sim/executor/handlers/condition/condition-handler.ts index 2d90ffc00..3f479fdf2 100644 --- a/apps/sim/executor/handlers/condition/condition-handler.ts +++ b/apps/sim/executor/handlers/condition/condition-handler.ts @@ -199,16 +199,14 @@ export class ConditionBlockHandler implements BlockHandler { // Return output, preserving source output structure if possible return { - response: { - ...((sourceOutput as any)?.response || {}), // Keep original response fields if they exist - conditionResult: true, // Indicate a path was successfully chosen - selectedPath: { - blockId: targetBlock.id, - blockType: targetBlock.metadata?.id || 'unknown', - blockTitle: targetBlock.metadata?.name || 'Untitled Block', - }, - selectedConditionId: selectedCondition.id, + ...((sourceOutput as any) || {}), // Keep original fields if they exist + conditionResult: true, // Indicate a path was successfully chosen + selectedPath: { + blockId: targetBlock.id, + blockType: targetBlock.metadata?.id || 'unknown', + blockTitle: targetBlock.metadata?.name || 'Untitled Block', }, + selectedConditionId: selectedCondition.id, } } } diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts index cd436c457..096f3ca6a 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts @@ -1,7 +1,6 @@ import '../../__test-utils__/mock-dependencies' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' -import type { BlockOutput } from '@/blocks/types' import { getProviderFromModel } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' import type { ExecutionContext } from '../../types' @@ -86,21 +85,6 @@ describe('EvaluatorBlockHandler', () => { temperature: 0.1, } - const expectedOutput: BlockOutput = { - response: { - content: 'This is the content to evaluate.', - model: 'mock-model', - tokens: { prompt: 50, completion: 10, total: 60 }, - cost: { - input: 0, - output: 0, - total: 0, - }, - score1: 5, - score2: 8, - }, - } - const result = await handler.execute(mockBlock, inputs, mockContext) expect(mockGetProviderFromModel).toHaveBeenCalledWith('gpt-4o') @@ -134,7 +118,18 @@ describe('EvaluatorBlockHandler', () => { temperature: 0.1, }) - expect(result).toEqual(expectedOutput) + expect(result).toEqual({ + content: 'This is the content to evaluate.', + model: 'mock-model', + tokens: { prompt: 50, completion: 10, total: 60 }, + cost: { + input: 0, + output: 0, + total: 0, + }, + score1: 5, + score2: 8, + }) }) it('should process JSON string content correctly', async () => { @@ -221,7 +216,7 @@ describe('EvaluatorBlockHandler', () => { const result = await handler.execute(mockBlock, inputs, mockContext) - expect((result as any).response.quality).toBe(9) + expect((result as any).quality).toBe(9) }) it('should handle invalid/non-JSON response gracefully (scores = 0)', async () => { @@ -246,7 +241,7 @@ describe('EvaluatorBlockHandler', () => { const result = await handler.execute(mockBlock, inputs, mockContext) - expect((result as any).response.score).toBe(0) + expect((result as any).score).toBe(0) }) it('should handle partially valid JSON response (extracts what it can)', async () => { @@ -273,8 +268,8 @@ describe('EvaluatorBlockHandler', () => { }) const result = await handler.execute(mockBlock, inputs, mockContext) - expect((result as any).response.accuracy).toBe(0) - expect((result as any).response.fluency).toBe(0) + expect((result as any).accuracy).toBe(0) + expect((result as any).fluency).toBe(0) }) it('should extract metric scores ignoring case', async () => { @@ -299,7 +294,7 @@ describe('EvaluatorBlockHandler', () => { const result = await handler.execute(mockBlock, inputs, mockContext) - expect((result as any).response.camelcasescore).toBe(7) + expect((result as any).camelcasescore).toBe(7) }) it('should handle missing metrics in response (score = 0)', async () => { @@ -327,8 +322,8 @@ describe('EvaluatorBlockHandler', () => { const result = await handler.execute(mockBlock, inputs, mockContext) - expect((result as any).response.presentscore).toBe(4) - expect((result as any).response.missingscore).toBe(0) + expect((result as any).presentscore).toBe(4) + expect((result as any).missingscore).toBe(0) }) it('should handle server error responses', async () => { diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts index 642a7b134..8d41c5bd7 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts @@ -251,21 +251,19 @@ export class EvaluatorBlockHandler implements BlockHandler { // Create result with metrics as direct fields for easy access const outputResult = { - response: { - content: inputs.content, - model: result.model, - tokens: { - prompt: result.tokens?.prompt || 0, - completion: result.tokens?.completion || 0, - total: result.tokens?.total || 0, - }, - cost: { - input: costCalculation.input, - output: costCalculation.output, - total: costCalculation.total, - }, - ...metricScores, + content: inputs.content, + model: result.model, + tokens: { + prompt: result.tokens?.prompt || 0, + completion: result.tokens?.completion || 0, + total: result.tokens?.total || 0, }, + cost: { + input: costCalculation.input, + output: costCalculation.output, + total: costCalculation.total, + }, + ...metricScores, } return outputResult diff --git a/apps/sim/executor/handlers/function/function-handler.test.ts b/apps/sim/executor/handlers/function/function-handler.test.ts index d61fc2ee2..f40f5be94 100644 --- a/apps/sim/executor/handlers/function/function-handler.test.ts +++ b/apps/sim/executor/handlers/function/function-handler.test.ts @@ -1,5 +1,4 @@ import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' -import type { BlockOutput } from '@/blocks/types' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' import type { ExecutionContext } from '../../types' @@ -81,7 +80,7 @@ describe('FunctionBlockHandler', () => { blockNameMapping: {}, _context: { workflowId: mockContext.workflowId }, } - const expectedOutput: BlockOutput = { response: { result: 'Success' } } + const expectedOutput: any = { result: 'Success' } const result = await handler.execute(mockBlock, inputs, mockContext) @@ -106,7 +105,7 @@ describe('FunctionBlockHandler', () => { blockNameMapping: {}, _context: { workflowId: mockContext.workflowId }, } - const expectedOutput: BlockOutput = { response: { result: 'Success' } } + const expectedOutput: any = { result: 'Success' } const result = await handler.execute(mockBlock, inputs, mockContext) diff --git a/apps/sim/executor/handlers/function/function-handler.ts b/apps/sim/executor/handlers/function/function-handler.ts index 8fcda9009..d895093b4 100644 --- a/apps/sim/executor/handlers/function/function-handler.ts +++ b/apps/sim/executor/handlers/function/function-handler.ts @@ -1,5 +1,4 @@ import { createLogger } from '@/lib/logs/console-logger' -import type { BlockOutput } from '@/blocks/types' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' import type { BlockHandler, ExecutionContext } from '../../types' @@ -18,7 +17,7 @@ export class FunctionBlockHandler implements BlockHandler { block: SerializedBlock, inputs: Record, context: ExecutionContext - ): Promise { + ): Promise { const codeContent = Array.isArray(inputs.code) ? inputs.code.map((c: { content: string }) => c.content).join('\n') : inputs.code @@ -53,6 +52,6 @@ export class FunctionBlockHandler implements BlockHandler { throw new Error(result.error || 'Function execution failed') } - return { response: result.output } + return result.output } } diff --git a/apps/sim/executor/handlers/generic/generic-handler.test.ts b/apps/sim/executor/handlers/generic/generic-handler.test.ts index 75109192a..34a74842a 100644 --- a/apps/sim/executor/handlers/generic/generic-handler.test.ts +++ b/apps/sim/executor/handlers/generic/generic-handler.test.ts @@ -1,7 +1,6 @@ import '../../__test-utils__/mock-dependencies' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' -import type { BlockOutput } from '@/blocks/types' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' import type { ToolConfig } from '@/tools/types' @@ -88,7 +87,7 @@ describe('GenericBlockHandler', () => { ...inputs, _context: { workflowId: mockContext.workflowId }, } - const expectedOutput: BlockOutput = { response: { customResult: 'OK' } } + const expectedOutput: any = { customResult: 'OK' } const result = await handler.execute(mockBlock, inputs, mockContext) diff --git a/apps/sim/executor/handlers/generic/generic-handler.ts b/apps/sim/executor/handlers/generic/generic-handler.ts index b4e392cae..0c45b11b1 100644 --- a/apps/sim/executor/handlers/generic/generic-handler.ts +++ b/apps/sim/executor/handlers/generic/generic-handler.ts @@ -1,5 +1,4 @@ import { createLogger } from '@/lib/logs/console-logger' -import type { BlockOutput } from '@/blocks/types' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' import { getTool } from '@/tools/utils' @@ -22,7 +21,7 @@ export class GenericBlockHandler implements BlockHandler { block: SerializedBlock, inputs: Record, context: ExecutionContext - ): Promise { + ): Promise { logger.info(`Executing block: ${block.id} (Type: ${block.metadata?.id})`) const tool = getTool(block.config.tool) if (!tool) { @@ -60,7 +59,7 @@ export class GenericBlockHandler implements BlockHandler { throw error } - return { response: result.output } + return result.output } catch (error: any) { // Ensure we have a meaningful error message if (!error.message || error.message === 'undefined (undefined)') { diff --git a/apps/sim/executor/handlers/loop/loop-handler.test.ts b/apps/sim/executor/handlers/loop/loop-handler.test.ts index 7e1ef005e..3c7e7a13e 100644 --- a/apps/sim/executor/handlers/loop/loop-handler.test.ts +++ b/apps/sim/executor/handlers/loop/loop-handler.test.ts @@ -81,8 +81,8 @@ describe('LoopBlockHandler', () => { expect(mockContext.activeExecutionPath.has('inner-block')).toBe(true) // Type guard to check if result has the expected structure - if (typeof result === 'object' && result !== null && 'response' in result) { - const response = result.response as any + if (typeof result === 'object' && result !== null) { + const response = result as any expect(response.currentIteration).toBe(0) // Still shows current iteration as 0 expect(response.maxIterations).toBe(3) expect(response.completed).toBe(false) @@ -102,8 +102,8 @@ describe('LoopBlockHandler', () => { // But it should not activate the inner block either since we're at max iterations expect(mockContext.activeExecutionPath.has('inner-block')).toBe(false) - if (typeof result === 'object' && result !== null && 'response' in result) { - const response = result.response as any + if (typeof result === 'object' && result !== null) { + const response = result as any expect(response.completed).toBe(false) // Not completed until all blocks execute expect(response.message).toContain('Final iteration') } @@ -122,8 +122,8 @@ describe('LoopBlockHandler', () => { expect(mockContext.loopItems.get('loop-1')).toBe('item1') - if (typeof result === 'object' && result !== null && 'response' in result) { - const response = result.response as any + if (typeof result === 'object' && result !== null) { + const response = result as any expect(response.loopType).toBe('forEach') expect(response.maxIterations).toBe(3) // Limited by items length } @@ -162,8 +162,8 @@ describe('LoopBlockHandler', () => { expect(mockContext.loopIterations.get('loop-1')).toBe(1) expect(mockContext.loopItems.get('loop-1')).toBe('a') - if (typeof result === 'object' && result !== null && 'response' in result) { - const response = result.response as any + if (typeof result === 'object' && result !== null) { + const response = result as any expect(response.maxIterations).toBe(2) // Should be limited to 2, not 10 expect(response.completed).toBe(false) } @@ -173,8 +173,8 @@ describe('LoopBlockHandler', () => { expect(mockContext.loopIterations.get('loop-1')).toBe(2) expect(mockContext.loopItems.get('loop-1')).toBe('b') - if (typeof result === 'object' && result !== null && 'response' in result) { - const response = result.response as any + if (typeof result === 'object' && result !== null) { + const response = result as any expect(response.completed).toBe(false) } diff --git a/apps/sim/executor/handlers/loop/loop-handler.ts b/apps/sim/executor/handlers/loop/loop-handler.ts index 5e071d751..5795355ab 100644 --- a/apps/sim/executor/handlers/loop/loop-handler.ts +++ b/apps/sim/executor/handlers/loop/loop-handler.ts @@ -95,15 +95,13 @@ export class LoopBlockHandler implements BlockHandler { // Don't mark as completed here - let the loop manager handle it after all blocks execute // Just return that this is the final iteration return { - response: { - loopId: block.id, - currentIteration: currentIteration - 1, // Report the actual last iteration number - maxIterations, - loopType: loop.loopType || 'for', - completed: false, // Not completed until all blocks in this iteration execute - message: `Final iteration ${currentIteration} of ${maxIterations}`, - }, - } + loopId: block.id, + currentIteration: currentIteration - 1, // Report the actual last iteration number + maxIterations, + loopType: loop.loopType || 'for', + completed: false, // Not completed until all blocks in this iteration execute + message: `Final iteration ${currentIteration} of ${maxIterations}`, + } as Record } // For forEach loops, set the current item BEFORE incrementing @@ -140,15 +138,13 @@ export class LoopBlockHandler implements BlockHandler { } return { - response: { - loopId: block.id, - currentIteration, - maxIterations, - loopType: loop.loopType || 'for', - completed: false, - message: `Starting iteration ${currentIteration + 1} of ${maxIterations}`, - }, - } + loopId: block.id, + currentIteration, + maxIterations, + loopType: loop.loopType || 'for', + completed: false, + message: `Starting iteration ${currentIteration + 1} of ${maxIterations}`, + } as Record } /** diff --git a/apps/sim/executor/handlers/parallel/parallel-handler.test.ts b/apps/sim/executor/handlers/parallel/parallel-handler.test.ts index fa69b3189..3eaa01ea3 100644 --- a/apps/sim/executor/handlers/parallel/parallel-handler.test.ts +++ b/apps/sim/executor/handlers/parallel/parallel-handler.test.ts @@ -71,8 +71,7 @@ describe('ParallelBlockHandler', () => { // First execution - initialize parallel and set up iterations const result = await handler.execute(block, {}, context) - expect(result).toHaveProperty('response') - expect((result as any).response).toMatchObject({ + expect(result as any).toMatchObject({ parallelId: 'parallel-1', parallelCount: 3, distributionType: 'distributed', @@ -128,8 +127,7 @@ describe('ParallelBlockHandler', () => { // Second execution - check waiting state const result = await handler.execute(block, {}, context) - expect(result).toHaveProperty('response') - expect((result as any).response).toMatchObject({ + expect(result as any).toMatchObject({ parallelId: 'parallel-1', parallelCount: 2, completedExecutions: 0, @@ -157,8 +155,8 @@ describe('ParallelBlockHandler', () => { distributionItems: ['item1', 'item2'], completedExecutions: 0, executionResults: new Map([ - ['iteration_0', { 'agent-1': { response: { result: 'result1' } } }], - ['iteration_1', { 'agent-1': { response: { result: 'result2' } } }], + ['iteration_0', { 'agent-1': { result: 'result1' } }], + ['iteration_1', { 'agent-1': { result: 'result2' } }], ]), activeIterations: new Set(), currentIteration: 1, @@ -182,15 +180,11 @@ describe('ParallelBlockHandler', () => { // Execution after all iterations complete const result = await handler.execute(block, {}, context) - expect(result).toHaveProperty('response') - expect((result as any).response).toMatchObject({ + expect(result as any).toMatchObject({ parallelId: 'parallel-1', parallelCount: 2, completed: true, - results: [ - { 'agent-1': { response: { result: 'result1' } } }, - { 'agent-1': { response: { result: 'result2' } } }, - ], + results: [{ 'agent-1': { result: 'result1' } }, { 'agent-1': { result: 'result2' } }], message: 'Completed all 2 executions', }) @@ -214,8 +208,7 @@ describe('ParallelBlockHandler', () => { const result = await handler.execute(block, {}, context) - expect(result).toHaveProperty('response') - expect((result as any).response).toMatchObject({ + expect(result as any).toMatchObject({ parallelId: 'parallel-1', parallelCount: 2, distributionType: 'distributed', @@ -243,8 +236,7 @@ describe('ParallelBlockHandler', () => { const result = await handler.execute(block, {}, context) - expect(result).toHaveProperty('response') - expect((result as any).response).toMatchObject({ + expect(result as any).toMatchObject({ parallelId: 'parallel-1', parallelCount: 3, distributionType: 'distributed', @@ -267,8 +259,7 @@ describe('ParallelBlockHandler', () => { const result = await handler.execute(block, {}, context) - expect(result).toHaveProperty('response') - expect((result as any).response).toMatchObject({ + expect(result as any).toMatchObject({ parallelId: 'parallel-1', parallelCount: 1, distributionType: 'count', @@ -316,8 +307,8 @@ describe('ParallelBlockHandler', () => { // Initialize parallel const initResult = await handler.execute(parallelBlock, {}, context) - expect((initResult as any).response.started).toBe(true) - expect((initResult as any).response.parallelCount).toBe(3) + expect((initResult as any).started).toBe(true) + expect((initResult as any).parallelCount).toBe(3) // Simulate all virtual blocks being executed const parallelState = context.parallelExecutions?.get('parallel-1') @@ -343,13 +334,13 @@ describe('ParallelBlockHandler', () => { const aggregatedResult = await handler.execute(parallelBlock, {}, context) // Verify results are aggregated - expect((aggregatedResult as any).response.completed).toBe(true) - expect((aggregatedResult as any).response.results).toHaveLength(3) + expect((aggregatedResult as any).completed).toBe(true) + expect((aggregatedResult as any).results).toHaveLength(3) // Verify block state is stored const blockState = context.blockStates.get('parallel-1') expect(blockState).toBeDefined() - expect(blockState?.output.response.results).toHaveLength(3) + expect(blockState?.output.results).toHaveLength(3) // Verify both downstream blocks are activated expect(context.activeExecutionPath.has('function-1')).toBe(true) @@ -360,7 +351,7 @@ describe('ParallelBlockHandler', () => { // Simulate downstream blocks trying to access results // This should work without errors - const storedResults = context.blockStates.get('parallel-1')?.output.response.results + const storedResults = context.blockStates.get('parallel-1')?.output.results expect(storedResults).toBeDefined() expect(storedResults).toHaveLength(3) }) @@ -379,7 +370,7 @@ describe('ParallelBlockHandler', () => { const parallel2Block = createMockBlock('parallel-2') parallel2Block.config.params = { parallelType: 'collection', - collection: '', // This references the first parallel + collection: '', // This references the first parallel } // Set up context with both parallels @@ -415,7 +406,7 @@ describe('ParallelBlockHandler', () => { config: { tool: 'function', params: { - code: 'return ;', + code: 'return ;', }, }, inputs: {}, @@ -451,7 +442,7 @@ describe('ParallelBlockHandler', () => { 'parallel-2': { id: 'parallel-2', nodes: [], - distribution: '', + distribution: '', }, }, }, @@ -465,26 +456,26 @@ describe('ParallelBlockHandler', () => { for (let i = 0; i < 2; i++) { context.executedBlocks.add(`agent-1_parallel_parallel-1_iteration_${i}`) parallelState!.executionResults.set(`iteration_${i}`, { - 'agent-1': { response: { content: `Result ${i}` } }, + 'agent-1': { content: `Result ${i}` }, }) } // Re-execute first parallel to aggregate results const result = await handler.execute(parallel1Block, {}, context) - expect((result as any).response.completed).toBe(true) + expect((result as any).completed).toBe(true) // Verify the block state is available const blockState = context.blockStates.get('parallel-1') expect(blockState).toBeDefined() - expect(blockState?.output.response.results).toHaveLength(2) + expect(blockState?.output.results).toHaveLength(2) - // Now when function block tries to resolve , it should work + // Now when function block tries to resolve , it should work // even though parallel-2 exists on the canvas expect(() => { // This simulates what the resolver would do const state = context.blockStates.get('parallel-1') if (!state) throw new Error('No state found for block parallel-1') - const results = state.output?.response?.results + const results = state.output?.results if (!results) throw new Error('No results found') return results }).not.toThrow() diff --git a/apps/sim/executor/handlers/parallel/parallel-handler.ts b/apps/sim/executor/handlers/parallel/parallel-handler.ts index f133cf99b..9aaf17b8a 100644 --- a/apps/sim/executor/handlers/parallel/parallel-handler.ts +++ b/apps/sim/executor/handlers/parallel/parallel-handler.ts @@ -56,7 +56,7 @@ export class ParallelBlockHandler implements BlockHandler { // Check if we already have aggregated results stored (from a previous completion check) const existingBlockState = context.blockStates.get(block.id) - if (existingBlockState?.output?.response?.results) { + if (existingBlockState?.output?.results) { logger.info(`Parallel ${block.id} already has aggregated results, returning them`) return existingBlockState.output } @@ -72,14 +72,12 @@ export class ParallelBlockHandler implements BlockHandler { // Store the aggregated results in the block state so subsequent blocks can reference them const aggregatedOutput = { - response: { - parallelId: block.id, - parallelCount: parallelState.parallelCount, - completed: true, - results, - message: `Completed all ${parallelState.parallelCount} executions`, - }, - } + parallelId: block.id, + parallelCount: parallelState.parallelCount, + completed: true, + results, + message: `Completed all ${parallelState.parallelCount} executions`, + } as Record // Store the aggregated results in context so blocks connected to parallel-end-source can access them context.blockStates.set(block.id, { @@ -199,14 +197,12 @@ export class ParallelBlockHandler implements BlockHandler { } return { - response: { - parallelId: block.id, - parallelCount, - distributionType: parallelType === 'count' ? 'count' : 'distributed', - started: true, - message: `Initialized ${parallelCount} parallel execution${parallelCount > 1 ? 's' : ''}`, - }, - } + parallelId: block.id, + parallelCount, + distributionType: parallelType === 'count' ? 'count' : 'distributed', + started: true, + message: `Initialized ${parallelCount} parallel execution${parallelCount > 1 ? 's' : ''}`, + } as Record } // Check if all virtual blocks have completed @@ -222,7 +218,7 @@ export class ParallelBlockHandler implements BlockHandler { // Check if we already have aggregated results stored (from a previous completion check) const existingBlockState = context.blockStates.get(block.id) - if (existingBlockState?.output?.response?.results) { + if (existingBlockState?.output?.results) { logger.info(`Parallel ${block.id} already has aggregated results, returning them`) return existingBlockState.output } @@ -238,14 +234,12 @@ export class ParallelBlockHandler implements BlockHandler { // Store the aggregated results in the block state so subsequent blocks can reference them const aggregatedOutput = { - response: { - parallelId: block.id, - parallelCount: parallelState.parallelCount, - completed: true, - results, - message: `Completed all ${parallelState.parallelCount} executions`, - }, - } + parallelId: block.id, + parallelCount: parallelState.parallelCount, + completed: true, + results, + message: `Completed all ${parallelState.parallelCount} executions`, + } as Record // Store the aggregated results in context so blocks connected to parallel-end-source can access them context.blockStates.set(block.id, { @@ -288,15 +282,13 @@ export class ParallelBlockHandler implements BlockHandler { // Still waiting for iterations to complete const completedCount = this.countCompletedIterations(block.id, context) return { - response: { - parallelId: block.id, - parallelCount: parallelState.parallelCount, - completedExecutions: completedCount, - activeIterations: parallelState.parallelCount - completedCount, - waiting: true, - message: `${completedCount} of ${parallelState.parallelCount} iterations completed`, - }, - } + parallelId: block.id, + parallelCount: parallelState.parallelCount, + completedExecutions: completedCount, + activeIterations: parallelState.parallelCount - completedCount, + waiting: true, + message: `${completedCount} of ${parallelState.parallelCount} iterations completed`, + } as Record } /** diff --git a/apps/sim/executor/handlers/router/router-handler.test.ts b/apps/sim/executor/handlers/router/router-handler.test.ts index 2ad38cf71..435e28c44 100644 --- a/apps/sim/executor/handlers/router/router-handler.test.ts +++ b/apps/sim/executor/handlers/router/router-handler.test.ts @@ -11,7 +11,6 @@ import { vi, } from 'vitest' import { generateRouterPrompt } from '@/blocks/blocks/router' -import type { BlockOutput } from '@/blocks/types' import { getProviderFromModel } from '@/providers/utils' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' import { PathTracker } from '../../path' @@ -147,24 +146,6 @@ describe('RouterBlockHandler', () => { }, ] - const expectedOutput: BlockOutput = { - response: { - content: 'Choose the best option.', - model: 'mock-model', - tokens: { prompt: 100, completion: 5, total: 105 }, - cost: { - input: 0, - output: 0, - total: 0, - }, - selectedPath: { - blockId: 'target-block-1', - blockType: 'target', - blockTitle: 'Option A', - }, - }, - } - const result = await handler.execute(mockBlock, inputs, mockContext) expect(mockGenerateRouterPrompt).toHaveBeenCalledWith(inputs.prompt, expectedTargetBlocks) @@ -189,7 +170,21 @@ describe('RouterBlockHandler', () => { temperature: 0.5, }) - expect(result).toEqual(expectedOutput) + expect(result).toEqual({ + content: 'Choose the best option.', + model: 'mock-model', + tokens: { prompt: 100, completion: 5, total: 105 }, + cost: { + input: 0, + output: 0, + total: 0, + }, + selectedPath: { + blockId: 'target-block-1', + blockType: 'target', + blockTitle: 'Option A', + }, + }) }) it('should throw error if target block is missing', async () => { diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index 40a88781e..66fb3cd42 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -101,24 +101,22 @@ export class RouterBlockHandler implements BlockHandler { ) return { - response: { - content: inputs.prompt, - model: result.model, - tokens: { - prompt: tokens.prompt || 0, - completion: tokens.completion || 0, - total: tokens.total || 0, - }, - cost: { - input: cost.input, - output: cost.output, - total: cost.total, - }, - selectedPath: { - blockId: chosenBlock.id, - blockType: chosenBlock.type || 'unknown', - blockTitle: chosenBlock.title || 'Untitled Block', - }, + content: inputs.prompt, + model: result.model, + tokens: { + prompt: tokens.prompt || 0, + completion: tokens.completion || 0, + total: tokens.total || 0, + }, + cost: { + input: cost.input, + output: cost.output, + total: cost.total, + }, + selectedPath: { + blockId: chosenBlock.id, + blockType: chosenBlock.type || 'unknown', + blockTitle: chosenBlock.title || 'Untitled Block', }, } } catch (error) { diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts index 69c6a6f2e..dd2a1ff98 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -208,7 +208,7 @@ describe('WorkflowBlockHandler', () => { it('should map successful child output correctly', () => { const childResult = { success: true, - output: { response: { data: 'test result' } }, + output: { data: 'test result' }, } const result = (handler as any).mapChildOutputToParent( @@ -219,11 +219,9 @@ describe('WorkflowBlockHandler', () => { ) expect(result).toEqual({ - response: { - success: true, - childWorkflowName: 'Child Workflow', - result: { data: 'test result' }, - }, + success: true, + childWorkflowName: 'Child Workflow', + result: { data: 'test result' }, }) }) @@ -241,17 +239,15 @@ describe('WorkflowBlockHandler', () => { ) expect(result).toEqual({ - response: { - success: false, - childWorkflowName: 'Child Workflow', - error: 'Child workflow failed', - }, + success: false, + childWorkflowName: 'Child Workflow', + error: 'Child workflow failed', }) }) it('should handle nested response structures', () => { const childResult = { - response: { response: { nested: 'data' } }, + output: { nested: 'data' }, } const result = (handler as any).mapChildOutputToParent( @@ -262,11 +258,9 @@ describe('WorkflowBlockHandler', () => { ) expect(result).toEqual({ - response: { - success: true, - childWorkflowName: 'Child Workflow', - result: { nested: 'data' }, - }, + success: true, + childWorkflowName: 'Child Workflow', + result: { nested: 'data' }, }) }) }) diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index 3c3f02414..3459b782d 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -1,4 +1,6 @@ +import { generateInternalToken } from '@/lib/auth/internal' import { createLogger } from '@/lib/logs/console-logger' +import { getBaseUrl } from '@/lib/urls/utils' import type { BlockOutput } from '@/blocks/types' import { Serializer } from '@/serializer' import type { SerializedBlock } from '@/serializer/types' @@ -69,7 +71,7 @@ export class WorkflowBlockHandler implements BlockHandler { ) // Prepare the input for the child workflow - // The input from this block should be passed as start.response.input to the child workflow + // The input from this block should be passed as start.input to the child workflow let childWorkflowInput = {} if (inputs.input !== undefined) { @@ -125,8 +127,20 @@ export class WorkflowBlockHandler implements BlockHandler { */ private async loadChildWorkflow(workflowId: string) { try { - // Fetch workflow from API - const response = await fetch(`/api/workflows/${workflowId}`) + // Fetch workflow from API with internal authentication header + const headers: Record = { + 'Content-Type': 'application/json', + } + + // Add internal auth header for server-side calls + if (typeof window === 'undefined') { + const token = await generateInternalToken() + headers.Authorization = `Bearer ${token}` + } + + const response = await fetch(`${getBaseUrl()}/api/workflows/${workflowId}`, { + headers, + }) if (!response.ok) { if (response.status === 404) { @@ -145,7 +159,7 @@ export class WorkflowBlockHandler implements BlockHandler { logger.info(`Loaded child workflow: ${workflowData.name} (${workflowId})`) - // Extract the workflow state (API returns normalized data in state field) + // Extract the workflow state const workflowState = workflowData.state if (!workflowState || !workflowState.blocks) { @@ -153,7 +167,7 @@ export class WorkflowBlockHandler implements BlockHandler { return null } - // Use blocks directly since API returns data from normalized tables + // Use blocks directly since DB format should match UI format const serializedWorkflow = this.serializer.serializeWorkflow( workflowState.blocks, workflowState.edges || [], @@ -186,29 +200,23 @@ export class WorkflowBlockHandler implements BlockHandler { if (!success) { logger.warn(`Child workflow ${childWorkflowName} failed`) return { - response: { - success: false, - childWorkflowName, - error: childResult.error || 'Child workflow execution failed', - }, + success: false, + childWorkflowName, + error: childResult.error || 'Child workflow execution failed', } as Record } - // Extract the actual result content from the nested structure + // Extract the actual result content from the flattened structure let result = childResult - if (childResult?.output?.response) { - result = childResult.output.response - } else if (childResult?.response?.response) { - result = childResult.response.response + if (childResult?.output) { + result = childResult.output } // Return a properly structured response with all required fields return { - response: { - success: true, - childWorkflowName, - result, - }, + success: true, + childWorkflowName, + result, } as Record } } diff --git a/apps/sim/executor/index.test.ts b/apps/sim/executor/index.test.ts index e60c7f894..afb1e4e3e 100644 --- a/apps/sim/executor/index.test.ts +++ b/apps/sim/executor/index.test.ts @@ -66,7 +66,7 @@ describe('Executor', () => { test('should create an executor instance with new options object format', () => { const workflow = createMinimalWorkflow() const initialStates = { - block1: { response: { result: 'Initial state' } }, + block1: { result: { value: 'Initial state' } }, } const envVars = { API_KEY: 'test-key', BASE_URL: 'https://example.com' } const workflowInput = { query: 'test query' } @@ -111,7 +111,7 @@ describe('Executor', () => { test('should handle legacy constructor with individual parameters', () => { const workflow = createMinimalWorkflow() const initialStates = { - block1: { response: { result: 'Initial state' } }, + block1: { result: { value: 'Initial state' } }, } const envVars = { API_KEY: 'test-key' } const workflowInput = { query: 'test query' } @@ -225,7 +225,6 @@ describe('Executor', () => { if ('success' in result) { expect(result).toHaveProperty('success') expect(result).toHaveProperty('output') - expect(result.output).toHaveProperty('response') // Our mocked implementation results in a false success value // In real usage, this would be true for successful executions @@ -373,7 +372,7 @@ describe('Executor', () => { // Create a mock context for debug continuation const mockContext = createMockContext() mockContext.blockStates.set('starter', { - output: { response: { input: {} } }, + output: { input: {} }, executed: true, executionTime: 0, }) @@ -389,61 +388,27 @@ describe('Executor', () => { /** * Additional tests to improve coverage */ - describe('normalizeBlockOutput', () => { - test('should normalize different block outputs correctly', () => { + describe('block output handling', () => { + test('should handle different block outputs correctly', () => { const workflow = createMinimalWorkflow() const executor = new Executor(workflow) - // Access the private method for testing - const normalizeOutput = (executor as any).normalizeBlockOutput.bind(executor) - - // Test normalizing agent block output - const agentBlock = { metadata: { id: 'agent' } } - const agentOutput = { response: { content: 'Agent response' } } - expect(normalizeOutput(agentOutput, agentBlock)).toEqual(agentOutput) - - // Test normalizing router block output - const routerBlock = { metadata: { id: 'router' } } - const routerOutput = { selectedPath: { blockId: 'target' } } - const normalizedRouterOutput = normalizeOutput(routerOutput, routerBlock) - expect(normalizedRouterOutput.response.selectedPath).toEqual(routerOutput.selectedPath) - - // Test normalizing function block output - const functionBlock = { metadata: { id: 'function' } } - const functionOutput = { result: 'Function result', stdout: 'Output' } - const normalizedFunctionOutput = normalizeOutput(functionOutput, functionBlock) - expect(normalizedFunctionOutput.response.result).toEqual(functionOutput.result) - expect(normalizedFunctionOutput.response.stdout).toEqual(functionOutput.stdout) - - // Test generic output normalization - const genericBlock = { metadata: { id: 'unknown' } } - const genericOutput = 'Simple string result' - const normalizedGenericOutput = normalizeOutput(genericOutput, genericBlock) - expect(normalizedGenericOutput.response.result).toEqual(genericOutput) + // Test basic workflow execution + expect(executor).toBeDefined() + expect(typeof executor.execute).toBe('function') }) - test('should normalize error outputs correctly', () => { + test('should handle error outputs correctly', () => { const workflow = createMinimalWorkflow() const executor = new Executor(workflow) - const normalizeOutput = (executor as any).normalizeBlockOutput.bind(executor) - // Test error output with error property - const errorOutput = { error: 'Test error message', status: 400 } - const normalizedErrorOutput = normalizeOutput(errorOutput, { metadata: { id: 'api' } }) + // Test error handling functionality + const extractErrorMessage = (executor as any).extractErrorMessage.bind(executor) - expect(normalizedErrorOutput).toHaveProperty('error', 'Test error message') - expect(normalizedErrorOutput.response).toHaveProperty('error', 'Test error message') - expect(normalizedErrorOutput.response).toHaveProperty('status', 400) - - // Test object with response.error - const responseErrorOutput = { response: { error: 'Response error', data: 'test' } } - const normalizedResponseError = normalizeOutput(responseErrorOutput, { - metadata: { id: 'api' }, - }) - - expect(normalizedResponseError).toHaveProperty('error', 'Response error') - expect(normalizedResponseError.response).toHaveProperty('error', 'Response error') - expect(normalizedResponseError.response).toHaveProperty('data', 'test') + // Test error message extraction + const error = new Error('Test error message') + const errorMessage = extractErrorMessage(error) + expect(errorMessage).toBe('Test error message') }) }) @@ -467,7 +432,6 @@ describe('Executor', () => { context.blockStates.set('block1', { output: { error: 'Test error', - response: { error: 'Test error' }, }, executed: true, }) @@ -579,17 +543,13 @@ describe('Executor', () => { // Create an error output manually const errorOutput = { - response: { - error: errorMessage, - status: testError.status || 500, - }, error: errorMessage, + status: testError.status || 500, } // Verify the error output structure expect(errorOutput).toHaveProperty('error') - expect(errorOutput.response).toHaveProperty('error') - expect(errorOutput.response).toHaveProperty('status') + expect(errorOutput).toHaveProperty('status') }) test('should handle "undefined (undefined)" error case', () => { @@ -676,35 +636,51 @@ describe('Executor', () => { test('should handle multi-input blocks with inactive sources correctly', () => { // Create workflow with router -> multiple APIs -> single agent const routerWorkflow = { + version: '1.0', blocks: [ { id: 'start', + position: { x: 0, y: 0 }, metadata: { id: 'starter', name: 'Start' }, - config: { params: {} }, + config: { tool: 'test-tool', params: {} }, + inputs: {}, + outputs: {}, enabled: true, }, { id: 'router', + position: { x: 100, y: 0 }, metadata: { id: 'router', name: 'Router' }, - config: { params: { prompt: 'test', model: 'gpt-4' } }, + config: { tool: 'test-tool', params: { prompt: 'test', model: 'gpt-4' } }, + inputs: {}, + outputs: {}, enabled: true, }, { id: 'api1', + position: { x: 200, y: -50 }, metadata: { id: 'api', name: 'API 1' }, - config: { params: { url: 'http://api1.com', method: 'GET' } }, + config: { tool: 'test-tool', params: { url: 'http://api1.com', method: 'GET' } }, + inputs: {}, + outputs: {}, enabled: true, }, { id: 'api2', + position: { x: 200, y: 50 }, metadata: { id: 'api', name: 'API 2' }, - config: { params: { url: 'http://api2.com', method: 'GET' } }, + config: { tool: 'test-tool', params: { url: 'http://api2.com', method: 'GET' } }, + inputs: {}, + outputs: {}, enabled: true, }, { id: 'agent', + position: { x: 300, y: 0 }, metadata: { id: 'agent', name: 'Agent' }, - config: { params: { model: 'gpt-4', userPrompt: 'test' } }, + config: { tool: 'test-tool', params: { model: 'gpt-4', userPrompt: 'test' } }, + inputs: {}, + outputs: {}, enabled: true, }, ], @@ -794,8 +770,11 @@ describe('Executor', () => { // Add router block to workflow workflow.blocks.push({ id: 'router1', + position: { x: 200, y: 0 }, metadata: { id: 'router', name: 'Router' }, - config: { params: {} }, + config: { tool: 'test-tool', params: {} }, + inputs: {}, + outputs: {}, enabled: true, }) diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index f12606656..a74e70c5d 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -1,3 +1,4 @@ +import { BlockPathCalculator } from '@/lib/block-path-calculator' import { createLogger } from '@/lib/logs/console-logger' import type { BlockOutput } from '@/blocks/types' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' @@ -127,11 +128,18 @@ export class Executor { this.loopManager = new LoopManager(this.actualWorkflow.loops || {}) this.parallelManager = new ParallelManager(this.actualWorkflow.parallels || {}) + + // Calculate accessible blocks for consistent reference resolution + const accessibleBlocksMap = BlockPathCalculator.calculateAccessibleBlocksForWorkflow( + this.actualWorkflow + ) + this.resolver = new InputResolver( this.actualWorkflow, this.environmentVariables, this.workflowVariables, - this.loopManager + this.loopManager, + accessibleBlocksMap ) this.pathTracker = new PathTracker(this.actualWorkflow) @@ -161,7 +169,7 @@ export class Executor { async execute(workflowId: string): Promise { const { setIsExecuting, setIsDebugging, setPendingBlocks, reset } = useExecutionStore.getState() const startTime = new Date() - let finalOutput: NormalizedBlockOutput = { response: {} } + let finalOutput: NormalizedBlockOutput = {} // Track workflow execution start trackWorkflowTelemetry('workflow_execution_started', { @@ -258,16 +266,16 @@ export class Executor { const blockId = (streamingExec.execution as any).blockId const blockState = context.blockStates.get(blockId) - if (blockState?.output?.response) { - blockState.output.response.content = fullContent + if (blockState?.output) { + blockState.output.content = fullContent } } catch (readerError: any) { logger.error('Error reading stream for executor:', readerError) // Set partial content if available const blockId = (streamingExec.execution as any).blockId const blockState = context.blockStates.get(blockId) - if (blockState?.output?.response && fullContent) { - blockState.output.response.content = fullContent + if (blockState?.output && fullContent) { + blockState.output.content = fullContent } } finally { try { @@ -376,7 +384,7 @@ export class Executor { */ async continueExecution(blockIds: string[], context: ExecutionContext): Promise { const { setPendingBlocks } = useExecutionStore.getState() - let finalOutput: NormalizedBlockOutput = { response: {} } + let finalOutput: NormalizedBlockOutput = {} try { // Execute the current layer - using the original context, not a clone @@ -616,19 +624,17 @@ export class Executor { // If no fields matched the input format, extract the raw input to use instead const rawInputData = this.workflowInput?.input !== undefined - ? this.workflowInput.input // Use the nested input data + ? this.workflowInput.input // Use the input value : this.workflowInput // Fallback to direct input // Use the structured input if we processed fields, otherwise use raw input const finalInput = hasProcessedFields ? structuredInput : rawInputData - // Initialize the starter block with structured input - // Ensure both input and direct fields are available + // Initialize the starter block with structured input (flattened) const starterOutput = { - response: { - input: finalInput, - ...finalInput, // Add input fields directly at response level too - }, + input: finalInput, + conversationId: this.workflowInput?.conversationId, // Add conversationId to root + ...finalInput, // Add input fields directly at top level } logger.info(`[Executor] Starter output:`, JSON.stringify(starterOutput, null, 2)) @@ -641,29 +647,36 @@ export class Executor { } else { // Handle structured input (like API calls or chat messages) if (this.workflowInput && typeof this.workflowInput === 'object') { - // Preserve complete workflowInput structure to maintain JSON format - // when referenced through - - const starterOutput = { - response: { - input: this.workflowInput, - // Add top-level fields for backward compatibility - message: this.workflowInput.input, + // Check if this is a chat workflow input (has both input and conversationId) + if ( + Object.hasOwn(this.workflowInput, 'input') && + Object.hasOwn(this.workflowInput, 'conversationId') + ) { + // Chat workflow: extract input and conversationId to root level + const starterOutput = { + input: this.workflowInput.input, conversationId: this.workflowInput.conversationId, - }, - } + } - context.blockStates.set(starterBlock.id, { - output: starterOutput, - executed: true, - executionTime: 0, - }) + context.blockStates.set(starterBlock.id, { + output: starterOutput, + executed: true, + executionTime: 0, + }) + } else { + // API workflow: spread the raw data directly (no wrapping) + const starterOutput = { ...this.workflowInput } + + context.blockStates.set(starterBlock.id, { + output: starterOutput, + executed: true, + executionTime: 0, + }) + } } else { // Fallback for primitive input values const starterOutput = { - response: { - input: this.workflowInput, - }, + input: this.workflowInput, } context.blockStates.set(starterBlock.id, { @@ -676,13 +689,28 @@ export class Executor { } catch (e) { logger.warn('Error processing starter block input format:', e) - // Error handler fallback - preserve structure for both direct access and backward compatibility - const starterOutput = { - response: { + // Error handler fallback - use appropriate structure + let starterOutput: any + if (this.workflowInput && typeof this.workflowInput === 'object') { + // Check if this is a chat workflow input (has both input and conversationId) + if ( + Object.hasOwn(this.workflowInput, 'input') && + Object.hasOwn(this.workflowInput, 'conversationId') + ) { + // Chat workflow: extract input and conversationId to root level + starterOutput = { + input: this.workflowInput.input, + conversationId: this.workflowInput.conversationId, + } + } else { + // API workflow: spread the raw data directly (no wrapping) + starterOutput = { ...this.workflowInput } + } + } else { + // Primitive input + starterOutput = { input: this.workflowInput, - message: this.workflowInput?.input, - conversationId: this.workflowInput?.conversationId, - }, + } } logger.info('[Executor] Fallback starter output:', JSON.stringify(starterOutput, null, 2)) @@ -891,9 +919,7 @@ export class Executor { return incomingConnections.every((conn) => { const sourceExecuted = executedBlocks.has(conn.source) const sourceBlockState = context.blockStates.get(conn.source) - const hasSourceError = - sourceBlockState?.output?.error !== undefined || - sourceBlockState?.output?.response?.error !== undefined + const hasSourceError = sourceBlockState?.output?.error !== undefined // For error connections, check if the source had an error if (conn.sourceHandle === 'error') { @@ -933,9 +959,7 @@ export class Executor { const sourceBlock = this.actualWorkflow.blocks.find((b) => b.id === conn.source) const sourceBlockState = context.blockStates.get(sourceId) || context.blockStates.get(conn.source) - const hasSourceError = - sourceBlockState?.output?.error !== undefined || - sourceBlockState?.output?.response?.error !== undefined + const hasSourceError = sourceBlockState?.output?.error !== undefined // Special handling for loop-start-source connections if (conn.sourceHandle === 'loop-start-source') { @@ -1046,7 +1070,7 @@ export class Executor { } }) - useExecutionStore.setState({ activeBlockIds }) + setActiveBlocks(activeBlockIds) const results = await Promise.all( blockIds.map((blockId) => this.executeBlock(blockId, context)) @@ -1061,7 +1085,7 @@ export class Executor { return results } catch (error) { // If there's an uncaught error, clear all active blocks as a safety measure - useExecutionStore.setState({ activeBlockIds: new Set() }) + setActiveBlocks(new Set()) throw error } } @@ -1131,7 +1155,7 @@ export class Executor { } // Check if this block needs the starter block's output - // This is especially relevant for API, function, and conditions that might reference + // This is especially relevant for API, function, and conditions that might reference const starterBlock = this.actualWorkflow.blocks.find((b) => b.metadata?.id === 'starter') if (starterBlock) { const starterState = context.blockStates.get(starterBlock.id) @@ -1265,8 +1289,13 @@ export class Executor { return streamingExec } - // Normalize the output - const output = this.normalizeBlockOutput(rawOutput, block) + // Handle error outputs and ensure object structure + const output: NormalizedBlockOutput = + rawOutput && typeof rawOutput === 'object' && rawOutput.error + ? { error: rawOutput.error, status: rawOutput.status || 500 } + : typeof rawOutput === 'object' && rawOutput !== null + ? rawOutput + : { result: rawOutput } // Update the context with the execution result // Use virtual block ID for parallel executions @@ -1364,7 +1393,7 @@ export class Executor { // Skip console logging for infrastructure blocks like loops and parallels if (block.metadata?.id !== 'loop' && block.metadata?.id !== 'parallel') { addConsole({ - output: { response: {} }, + output: {}, success: false, error: error.message || @@ -1392,11 +1421,8 @@ export class Executor { // Create error output with appropriate structure const errorOutput: NormalizedBlockOutput = { - response: { - error: this.extractErrorMessage(error), - status: error.status || 500, - }, error: this.extractErrorMessage(error), + status: error.status || 500, } // Set block state with error output @@ -1481,160 +1507,6 @@ export class Executor { return true } - /** - * Normalizes a block output to ensure it has the expected structure. - * Handles different block types with appropriate response formats. - * - * @param output - Raw output from block execution - * @param block - Block that produced the output - * @returns Normalized output with consistent structure - */ - private normalizeBlockOutput(output: any, block: SerializedBlock): NormalizedBlockOutput { - // Handle error outputs - if (output && typeof output === 'object' && output.error) { - return { - response: { - error: output.error, - status: output.status || 500, - }, - error: output.error, - } - } - - if (output && typeof output === 'object' && 'response' in output) { - // If response already contains an error, maintain it - if (output.response?.error) { - return { - ...output, - error: output.response.error, - } - } - return output as NormalizedBlockOutput - } - - const blockType = block.metadata?.id - - if (blockType === 'agent') { - return output - } - - if (blockType === 'router') { - return { - response: { - content: '', - model: '', - tokens: { prompt: 0, completion: 0, total: 0 }, - selectedPath: output?.selectedPath || { - blockId: '', - blockType: '', - blockTitle: '', - }, - }, - } - } - - if (blockType === 'condition') { - if (output && typeof output === 'object' && 'response' in output) { - return { - response: { - ...output.response, - conditionResult: output.response.conditionResult || false, - selectedPath: output.response.selectedPath || { - blockId: '', - blockType: '', - blockTitle: '', - }, - selectedConditionId: output.response.selectedConditionId || '', - }, - } - } - - return { - response: { - conditionResult: output?.conditionResult || false, - selectedPath: output?.selectedPath || { - blockId: '', - blockType: '', - blockTitle: '', - }, - selectedConditionId: output?.selectedConditionId || '', - }, - } - } - - if (blockType === 'function') { - return { - response: { - result: output?.result, - stdout: output?.stdout || '', - }, - } - } - - if (blockType === 'api') { - return { - response: { - data: output?.data, - status: output?.status || 0, - headers: output?.headers || {}, - }, - } - } - - if (blockType === 'evaluator') { - const evaluatorResponse: { - content: string - model: string - [key: string]: any - } = { - content: output?.content || '', - model: output?.model || '', - } - - if (output && typeof output === 'object') { - Object.keys(output).forEach((key) => { - if (key !== 'content' && key !== 'model') { - evaluatorResponse[key] = output[key] - } - }) - } - - return { response: evaluatorResponse } - } - - if (blockType === 'loop') { - return { - response: { - loopId: output?.loopId || block.id, - currentIteration: output?.currentIteration || 0, - maxIterations: output?.maxIterations || 0, - loopType: output?.loopType || 'for', - completed: output?.completed || false, - results: output?.results || [], - message: output?.message || '', - }, - } - } - - if (blockType === 'parallel') { - return { - response: { - parallelId: output?.parallelId || block.id, - parallelCount: output?.parallelCount || 1, - distributionType: output?.distributionType || 'simple', - completed: output?.completed || false, - completedCount: output?.completedCount || 0, - results: output?.results || [], - message: output?.message || '', - }, - } - } - - return { - response: { result: output }, - } - } - /** * Creates a new block log entry with initial values. * diff --git a/apps/sim/executor/loops.test.ts b/apps/sim/executor/loops.test.ts index 7dd43350d..3de1e4923 100644 --- a/apps/sim/executor/loops.test.ts +++ b/apps/sim/executor/loops.test.ts @@ -172,12 +172,12 @@ describe('LoopManager', () => { // Add some block states to verify they get reset mockContext.blockStates.set('block-1', { - output: { response: { result: 'test' } }, + output: { result: 'test' }, executed: true, executionTime: 100, }) mockContext.blockStates.set('block-2', { - output: { response: { result: 'test2' } }, + output: { result: 'test2' }, executed: true, executionTime: 200, }) @@ -215,9 +215,9 @@ describe('LoopManager', () => { loopType: 'for', forEachItems: null, executionResults: new Map([ - ['iteration_0', { iteration: { 'block-1': { response: { result: 'result1' } } } }], - ['iteration_1', { iteration: { 'block-1': { response: { result: 'result2' } } } }], - ['iteration_2', { iteration: { 'block-1': { response: { result: 'result3' } } } }], + ['iteration_0', { iteration: { 'block-1': { result: 'result1' } } }], + ['iteration_1', { iteration: { 'block-1': { result: 'result2' } } }], + ['iteration_2', { iteration: { 'block-1': { result: 'result3' } } }], ]), currentIteration: 3, }) @@ -232,8 +232,8 @@ describe('LoopManager', () => { // Verify loop block state was updated with aggregated results const loopBlockState = mockContext.blockStates.get('loop-1') expect(loopBlockState).toBeDefined() - expect(loopBlockState?.output.response.completed).toBe(true) - expect(loopBlockState?.output.response.results).toHaveLength(3) + expect(loopBlockState?.output.completed).toBe(true) + expect(loopBlockState?.output.results).toHaveLength(3) // Verify end connection was activated expect(mockContext.activeExecutionPath.has('after-loop')).toBe(true) @@ -259,8 +259,8 @@ describe('LoopManager', () => { expect(mockContext.completedLoops.has('loop-1')).toBe(true) const loopBlockState = mockContext.blockStates.get('loop-1') - expect(loopBlockState?.output.response.loopType).toBe('forEach') - expect(loopBlockState?.output.response.maxIterations).toBe(3) + expect(loopBlockState?.output.loopType).toBe('forEach') + expect(loopBlockState?.output.maxIterations).toBe(3) }) test('should handle forEach loops with object items', async () => { @@ -284,7 +284,7 @@ describe('LoopManager', () => { expect(mockContext.completedLoops.has('loop-1')).toBe(true) const loopBlockState = mockContext.blockStates.get('loop-1') - expect(loopBlockState?.output.response.maxIterations).toBe(2) + expect(loopBlockState?.output.maxIterations).toBe(2) }) test('should handle forEach loops with string items', async () => { @@ -307,7 +307,7 @@ describe('LoopManager', () => { describe('storeIterationResult', () => { test('should create new loop state if none exists', () => { - const output = { response: { result: 'test result' } } + const output = { result: 'test result' } manager.storeIterationResult(mockContext, 'loop-1', 0, 'block-1', output) @@ -330,8 +330,8 @@ describe('LoopManager', () => { currentIteration: 0, }) - const output1 = { response: { result: 'result1' } } - const output2 = { response: { result: 'result2' } } + const output1 = { result: 'result1' } + const output2 = { result: 'result2' } manager.storeIterationResult(mockContext, 'loop-1', 0, 'block-1', output1) manager.storeIterationResult(mockContext, 'loop-1', 0, 'block-2', output2) @@ -346,7 +346,7 @@ describe('LoopManager', () => { const forEachLoop = createForEachLoop(['item1', 'item2']) manager = new LoopManager({ 'loop-1': forEachLoop }) - const output = { response: { result: 'test result' } } + const output = { result: 'test result' } manager.storeIterationResult(mockContext, 'loop-1', 0, 'block-1', output) @@ -391,11 +391,11 @@ describe('LoopManager', () => { describe('getCurrentItem', () => { test('should return current item for loop', () => { - mockContext.loopItems.set('loop-1', 'current-item') + mockContext.loopItems.set('loop-1', ['current-item']) const item = manager.getCurrentItem('loop-1', mockContext) - expect(item).toBe('current-item') + expect(item).toEqual(['current-item']) }) test('should return undefined for non-existent loop item', () => { @@ -477,7 +477,7 @@ describe('LoopManager', () => { // Set block-1 to have no error (successful execution) mockContext.blockStates.set('block-1', { - output: { response: { result: 'success' } }, + output: { result: 'success' }, executed: true, executionTime: 100, }) @@ -521,7 +521,6 @@ describe('LoopManager', () => { // Set block-1 to have an error mockContext.blockStates.set('block-1', { output: { - response: { error: 'Something went wrong' }, error: 'Something went wrong', }, executed: true, @@ -633,8 +632,8 @@ describe('LoopManager', () => { loopType: 'for', forEachItems: null, executionResults: new Map([ - ['iteration_0', { iteration: { 'block-1': { response: { result: 'result1' } } } }], - ['iteration_1', { iteration: { 'block-1': { response: { result: 'result2' } } } }], + ['iteration_0', { iteration: { 'block-1': { result: 'result1' } } }], + ['iteration_1', { iteration: { 'block-1': { result: 'result2' } } }], ]), currentIteration: 2, }) diff --git a/apps/sim/executor/loops.ts b/apps/sim/executor/loops.ts index 85673e79a..b12b13981 100644 --- a/apps/sim/executor/loops.ts +++ b/apps/sim/executor/loops.ts @@ -134,15 +134,13 @@ export class LoopManager { // Store the aggregated results in the loop block's state so subsequent blocks can reference them const aggregatedOutput = { - response: { - loopId, - currentIteration: maxIterations - 1, // Last iteration index - maxIterations, - loopType: loop.loopType || 'for', - completed: true, - results, - message: `Completed all ${maxIterations} iterations`, - }, + loopId, + currentIteration: maxIterations - 1, // Last iteration index + maxIterations, + loopType: loop.loopType || 'for', + completed: true, + results, + message: `Completed all ${maxIterations} iterations`, } // Store the aggregated results in context so blocks connected to loop-end-source can access them @@ -447,8 +445,7 @@ export class LoopManager { ): void { // For regular blocks, check if they had an error const blockState = context.blockStates.get(blockId) - const hasError = - blockState?.output?.error !== undefined || blockState?.output?.response?.error !== undefined + const hasError = blockState?.output?.error !== undefined // Follow appropriate connections based on error state for (const conn of outgoing) { diff --git a/apps/sim/executor/parallels.test.ts b/apps/sim/executor/parallels.test.ts index fdfdf2867..51d4c84e5 100644 --- a/apps/sim/executor/parallels.test.ts +++ b/apps/sim/executor/parallels.test.ts @@ -273,7 +273,7 @@ describe('ParallelManager', () => { context.parallelExecutions?.set('parallel-1', state) - const output = { response: { result: 'test result' } } + const output = { result: 'test result' } manager.storeIterationResult(context, 'parallel-1', 1, output) diff --git a/apps/sim/executor/parallels.ts b/apps/sim/executor/parallels.ts index 0530c694c..a9d3598ff 100644 --- a/apps/sim/executor/parallels.ts +++ b/apps/sim/executor/parallels.ts @@ -112,7 +112,7 @@ export class ParallelManager { if (allVirtualBlocksExecuted && !context.completedLoops.has(parallelId)) { // Check if the parallel block already has aggregated results stored const blockState = context.blockStates.get(parallelId) - if (blockState?.output?.response?.completed && blockState?.output?.response?.results) { + if (blockState?.output?.completed && blockState?.output?.results) { logger.info( `Parallel ${parallelId} already has aggregated results, marking as completed without re-execution` ) diff --git a/apps/sim/executor/path.test.ts b/apps/sim/executor/path.test.ts index de08c93fa..1c33fafb8 100644 --- a/apps/sim/executor/path.test.ts +++ b/apps/sim/executor/path.test.ts @@ -168,7 +168,7 @@ describe('PathTracker', () => { describe('router blocks', () => { it('should update router decision and activate selected path', () => { const blockState: BlockState = { - output: { response: { selectedPath: { blockId: 'block1' } } }, + output: { selectedPath: { blockId: 'block1' } }, executed: true, executionTime: 100, } @@ -182,7 +182,7 @@ describe('PathTracker', () => { it('should not update if no selected path', () => { const blockState: BlockState = { - output: { response: {} }, + output: {}, executed: true, executionTime: 100, } @@ -198,7 +198,7 @@ describe('PathTracker', () => { describe('condition blocks', () => { it('should update condition decision and activate selected connection', () => { const blockState: BlockState = { - output: { response: { selectedConditionId: 'if' } }, + output: { selectedConditionId: 'if' }, executed: true, executionTime: 100, } @@ -212,7 +212,7 @@ describe('PathTracker', () => { it('should not activate if no matching connection', () => { const blockState: BlockState = { - output: { response: { selectedConditionId: 'unknown' } }, + output: { selectedConditionId: 'unknown' }, executed: true, executionTime: 100, } @@ -237,7 +237,7 @@ describe('PathTracker', () => { describe('regular blocks', () => { it('should activate outgoing connections on success', () => { const blockState: BlockState = { - output: { response: { data: 'success' } }, + output: { data: 'success' }, executed: true, executionTime: 100, } @@ -259,7 +259,7 @@ describe('PathTracker', () => { sourceHandle: 'error', }) const blockState: BlockState = { - output: { error: 'Something failed', response: { error: 'Something failed' } }, + output: { error: 'Something failed' }, executed: true, executionTime: 100, } @@ -332,12 +332,12 @@ describe('PathTracker', () => { it('should handle multiple blocks in one update', () => { const blockState1: BlockState = { - output: { response: { data: 'success' } }, + output: { data: 'success' }, executed: true, executionTime: 100, } const blockState2: BlockState = { - output: { response: { selectedPath: { blockId: 'block1' } } }, + output: { selectedPath: { blockId: 'block1' } }, executed: true, executionTime: 150, } @@ -480,15 +480,13 @@ describe('PathTracker', () => { }) it('should activate downstream paths when router selects a target', () => { - // Mock router output selecting api1 + // Mock router output selecting api1 - based on implementation, it expects selectedPath directly mockContext.blockStates.set('router1', { output: { - response: { - selectedPath: { - blockId: 'api1', - blockType: 'api', - blockTitle: 'API 1', - }, + selectedPath: { + blockId: 'api1', + blockType: 'api', + blockTitle: 'API 1', }, }, executed: true, @@ -521,15 +519,13 @@ describe('PathTracker', () => { pathTracker = new PathTracker(mockWorkflow) - // Mock router output selecting api1 + // Mock router output selecting api1 - based on implementation, it expects selectedPath directly mockContext.blockStates.set('router1', { output: { - response: { - selectedPath: { - blockId: 'api1', - blockType: 'api', - blockTitle: 'API 1', - }, + selectedPath: { + blockId: 'api1', + blockType: 'api', + blockTitle: 'API 1', }, }, executed: true, @@ -554,12 +550,10 @@ describe('PathTracker', () => { mockContext.blockStates.set('router1', { output: { - response: { - selectedPath: { - blockId: 'api1', - blockType: 'api', - blockTitle: 'API 1', - }, + selectedPath: { + blockId: 'api1', + blockType: 'api', + blockTitle: 'API 1', }, }, executed: true, @@ -590,12 +584,10 @@ describe('PathTracker', () => { mockContext.blockStates.set('router1', { output: { - response: { - selectedPath: { - blockId: 'api1', - blockType: 'api', - blockTitle: 'API 1', - }, + selectedPath: { + blockId: 'api1', + blockType: 'api', + blockTitle: 'API 1', }, }, executed: true, diff --git a/apps/sim/executor/path.ts b/apps/sim/executor/path.ts index 4e35393b9..4fa1f6c48 100644 --- a/apps/sim/executor/path.ts +++ b/apps/sim/executor/path.ts @@ -160,7 +160,7 @@ export class PathTracker { */ private updateRouterPaths(block: SerializedBlock, context: ExecutionContext): void { const routerOutput = context.blockStates.get(block.id)?.output - const selectedPath = routerOutput?.response?.selectedPath?.blockId + const selectedPath = routerOutput?.selectedPath?.blockId if (selectedPath) { context.decisions.router.set(block.id, selectedPath) @@ -192,7 +192,7 @@ export class PathTracker { */ private updateConditionPaths(block: SerializedBlock, context: ExecutionContext): void { const conditionOutput = context.blockStates.get(block.id)?.output - const selectedConditionId = conditionOutput?.response?.selectedConditionId + const selectedConditionId = conditionOutput?.selectedConditionId if (!selectedConditionId) return @@ -247,9 +247,7 @@ export class PathTracker { * Check if a block has an error */ private blockHasError(blockState: BlockState | undefined): boolean { - return ( - blockState?.output?.error !== undefined || blockState?.output?.response?.error !== undefined - ) + return blockState?.output?.error !== undefined } /** diff --git a/apps/sim/executor/resolver.test.ts b/apps/sim/executor/resolver.test.ts index fd3d0db58..520dbc13d 100644 --- a/apps/sim/executor/resolver.test.ts +++ b/apps/sim/executor/resolver.test.ts @@ -71,19 +71,33 @@ describe('InputResolver', () => { enabled: false, }, ], - connections: [], // Using connections instead of edges to match SerializedWorkflow type + connections: [ + // Add connections so blocks can reference each other + { source: 'starter-block', target: 'function-block' }, + { source: 'function-block', target: 'condition-block' }, + { source: 'condition-block', target: 'api-block' }, + { source: 'api-block', target: 'disabled-block' }, + ], loops: {}, } // Mock execution context mockContext = { workflowId: 'test-workflow', + workflow: sampleWorkflow, // Add workflow reference blockStates: new Map([ - ['starter-block', { output: { response: { input: 'Hello World', type: 'text' } } }], - ['function-block', { output: { response: { result: '42' } } }], // String value as it would be in real app + ['starter-block', { output: { input: 'Hello World', type: 'text' } }], + ['function-block', { output: { result: '42' } }], // String value as it would be in real app ]), activeExecutionPath: new Set(['starter-block', 'function-block']), + blockLogs: [], + metadata: { duration: 0 }, + environmentVariables: {}, + decisions: { router: new Map(), condition: new Map() }, loopIterations: new Map(), + loopItems: new Map(), + completedLoops: new Set(), + executedBlocks: new Set(['starter-block', 'function-block']), } // Mock environment variables @@ -138,8 +152,34 @@ describe('InputResolver', () => { }, } + // Create accessibility map for block references + const accessibleBlocksMap = new Map>() + // Allow all blocks to reference each other for testing + const allBlockIds = sampleWorkflow.blocks.map((b) => b.id) + // Add common test block IDs + const testBlockIds = ['test-block', 'test-block-2', 'generic-block'] + const allIds = [...allBlockIds, ...testBlockIds] + + // Set up accessibility for workflow blocks + sampleWorkflow.blocks.forEach((block) => { + const accessibleBlocks = new Set(allIds) + accessibleBlocksMap.set(block.id, accessibleBlocks) + }) + + // Set up accessibility for test blocks + testBlockIds.forEach((testId) => { + const accessibleBlocks = new Set(allIds) + accessibleBlocksMap.set(testId, accessibleBlocks) + }) + // Create resolver - resolver = new InputResolver(sampleWorkflow, mockEnvironmentVars, mockWorkflowVars) + resolver = new InputResolver( + sampleWorkflow, + mockEnvironmentVars, + mockWorkflowVars, + undefined, + accessibleBlocksMap + ) }) afterEach(() => { @@ -284,9 +324,9 @@ describe('InputResolver', () => { config: { tool: 'generic', params: { - starterRef: '', - functionRef: '', - nameRef: '', // Reference by name + starterRef: '', + functionRef: '', + nameRef: '', // Reference by name }, }, inputs: { @@ -313,8 +353,8 @@ describe('InputResolver', () => { config: { tool: 'generic', params: { - startRef: '', - startType: '', + startRef: '', + startType: '', }, }, inputs: { @@ -339,7 +379,7 @@ describe('InputResolver', () => { config: { tool: 'generic', params: { - inactiveRef: '', // Not in activeExecutionPath + inactiveRef: '', // Not in activeExecutionPath }, }, inputs: { @@ -356,9 +396,13 @@ describe('InputResolver', () => { }) it('should throw an error for references to disabled blocks', () => { - // Enable the disabled block but keep it out of execution path + // Add connection from disabled block to test block so it's accessible + sampleWorkflow.connections.push({ source: 'disabled-block', target: 'test-block' }) + + // Make sure disabled block stays disabled and add it to active path for validation const disabledBlock = sampleWorkflow.blocks.find((b) => b.id === 'disabled-block')! disabledBlock.enabled = false + mockContext.activeExecutionPath.add('disabled-block') const block: SerializedBlock = { id: 'test-block', @@ -367,7 +411,7 @@ describe('InputResolver', () => { config: { tool: 'generic', params: { - disabledRef: '', + disabledRef: '', }, }, inputs: { @@ -520,14 +564,14 @@ describe('InputResolver', () => { id: 'row1', cells: { Key: 'inputKey', - Value: '', + Value: '', }, }, { id: 'row2', cells: { Key: 'resultKey', - Value: '', + Value: '', }, }, ], @@ -640,7 +684,7 @@ describe('InputResolver', () => { config: { tool: 'condition', params: { - conditions: ' === "Hello World"', + conditions: ' === "Hello World"', }, }, inputs: { @@ -653,7 +697,7 @@ describe('InputResolver', () => { const result = resolver.resolveInputs(block, mockContext) // Conditions should be passed through without parsing for condition blocks - expect(result.conditions).toBe(' === "Hello World"') + expect(result.conditions).toBe(' === "Hello World"') }) }) @@ -736,7 +780,7 @@ describe('InputResolver', () => { environmentVariables: {}, decisions: { router: new Map(), condition: new Map() }, loopIterations: new Map([['loop-1', 1]]), - loopItems: new Map([['loop-1', 'item1']]), + loopItems: new Map([['loop-1', ['item1']]]), completedLoops: new Set(), executedBlocks: new Set(), activeExecutionPath: new Set(['function-1']), @@ -745,7 +789,7 @@ describe('InputResolver', () => { const resolvedInputs = resolver.resolveInputs(functionBlock, context) - expect(resolvedInputs.item).toBe('item1') // Direct value, not quoted + expect(resolvedInputs.item).toEqual(['item1']) // Current loop items }) it('should resolve direct loop.index reference without quotes', () => { @@ -989,7 +1033,7 @@ describe('InputResolver', () => { environmentVariables: {}, decisions: { router: new Map(), condition: new Map() }, loopIterations: new Map(), - loopItems: new Map([['parallel-1', 'test-item']]), + loopItems: new Map([['parallel-1', ['test-item']]]), completedLoops: new Set(), executedBlocks: new Set(), activeExecutionPath: new Set(['function-1']), @@ -999,7 +1043,7 @@ describe('InputResolver', () => { const block = workflow.blocks[1] const result = resolver.resolveInputs(block, context) - expect(result.code).toBe('test-item') + expect(result.code).toEqual(['test-item']) }) it('should resolve parallel references by block name when multiple parallels exist', () => { @@ -1027,7 +1071,7 @@ describe('InputResolver', () => { { id: 'function-1', position: { x: 0, y: 0 }, - config: { tool: 'function', params: { code: '' } }, + config: { tool: 'function', params: { code: '' } }, inputs: {}, outputs: {}, metadata: { id: 'function', name: 'Function 1' }, @@ -1048,14 +1092,31 @@ describe('InputResolver', () => { }, } - const resolver = new InputResolver(workflow, {}) + // Create accessibility map + const accessibilityMap = new Map>() + const allBlockIds = workflow.blocks.map((b) => b.id) + const testBlockIds = ['test-block', 'function-1'] + const allIds = [...allBlockIds, ...testBlockIds] + + workflow.blocks.forEach((block) => { + const accessibleBlocks = new Set(allIds) + accessibilityMap.set(block.id, accessibleBlocks) + }) + + // Set up accessibility for test blocks + testBlockIds.forEach((testId) => { + const accessibleBlocks = new Set(allIds) + accessibilityMap.set(testId, accessibleBlocks) + }) + + const resolver = new InputResolver(workflow, {}, {}, undefined, accessibilityMap) const context: ExecutionContext = { workflowId: 'test', blockStates: new Map([ [ 'parallel-1', { - output: { response: { results: ['result1', 'result2'] } }, + output: { results: ['result1', 'result2'] }, executed: true, executionTime: 0, }, @@ -1063,7 +1124,7 @@ describe('InputResolver', () => { [ 'parallel-2', { - output: { response: { results: ['result3', 'result4'] } }, + output: { results: ['result3', 'result4'] }, executed: true, executionTime: 0, }, @@ -1104,7 +1165,7 @@ describe('InputResolver', () => { { id: 'function-1', position: { x: 0, y: 0 }, - config: { tool: 'function', params: { code: '' } }, + config: { tool: 'function', params: { code: '' } }, inputs: {}, outputs: {}, metadata: { id: 'function', name: 'Function 1' }, @@ -1121,14 +1182,31 @@ describe('InputResolver', () => { }, } - const resolver = new InputResolver(workflow, {}) + // Create accessibility map for second test + const accessibilityMap = new Map>() + const allBlockIds = workflow.blocks.map((b) => b.id) + const testBlockIds = ['test-block', 'function-1'] + const allIds = [...allBlockIds, ...testBlockIds] + + workflow.blocks.forEach((block) => { + const accessibleBlocks = new Set(allIds) + accessibilityMap.set(block.id, accessibleBlocks) + }) + + // Set up accessibility for test blocks + testBlockIds.forEach((testId) => { + const accessibleBlocks = new Set(allIds) + accessibilityMap.set(testId, accessibleBlocks) + }) + + const resolver = new InputResolver(workflow, {}, {}, undefined, accessibilityMap) const context: ExecutionContext = { workflowId: 'test', blockStates: new Map([ [ 'parallel-1', { - output: { response: { results: ['result1', 'result2'] } }, + output: { results: ['result1', 'result2'] }, executed: true, executionTime: 0, }, @@ -1153,4 +1231,539 @@ describe('InputResolver', () => { expect(result.code).toBe('["result1","result2"]') }) }) + + describe('Connection-Based Reference Validation', () => { + let workflowWithConnections: SerializedWorkflow + let connectionResolver: InputResolver + let contextWithConnections: ExecutionContext + + beforeEach(() => { + // Create a workflow with specific connections: Agent -> Function -> Response + workflowWithConnections = { + version: '1.0', + blocks: [ + { + id: 'starter-1', + metadata: { id: 'starter', name: 'Start' }, + position: { x: 0, y: 0 }, + config: { tool: 'starter', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'agent-1', + metadata: { id: 'agent', name: 'Agent Block' }, + position: { x: 100, y: 100 }, + config: { tool: 'agent', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'function-1', + metadata: { id: 'function', name: 'Function Block' }, + position: { x: 200, y: 200 }, + config: { tool: 'function', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'isolated-block', + metadata: { id: 'agent', name: 'Isolated Block' }, + position: { x: 300, y: 300 }, + config: { tool: 'agent', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + ], + connections: [ + { source: 'starter-1', target: 'agent-1' }, + { source: 'agent-1', target: 'function-1' }, + // Note: isolated-block has no connections + ], + loops: {}, + } + + // Create accessibility map based on connections + const accessibleBlocksMap = new Map>() + const testBlockIds = ['test-block', 'test-block-2', 'test-response-block', 'generic-block'] + + workflowWithConnections.blocks.forEach((block) => { + const accessibleBlocks = new Set() + // Add directly connected blocks (sources that connect to this block) + workflowWithConnections.connections.forEach((conn) => { + if (conn.target === block.id) { + accessibleBlocks.add(conn.source) + } + }) + // Always allow starter block access + const starterBlock = workflowWithConnections.blocks.find( + (b) => b.metadata?.id === 'starter' + ) + if (starterBlock) { + accessibleBlocks.add(starterBlock.id) + } + accessibleBlocksMap.set(block.id, accessibleBlocks) + }) + + // Set up accessibility for test blocks - they should only reference specific connected blocks + // For "test-block" - it should have connection from function-1, so it can reference function-1 and start + workflowWithConnections.connections.push({ source: 'function-1', target: 'test-block' }) + + testBlockIds.forEach((testId) => { + const accessibleBlocks = new Set() + // Add directly connected blocks (sources that connect to this test block) + workflowWithConnections.connections.forEach((conn) => { + if (conn.target === testId) { + accessibleBlocks.add(conn.source) + } + }) + // Always allow starter block access + const starterBlock = workflowWithConnections.blocks.find( + (b) => b.metadata?.id === 'starter' + ) + if (starterBlock) { + accessibleBlocks.add(starterBlock.id) + } + accessibleBlocksMap.set(testId, accessibleBlocks) + }) + + connectionResolver = new InputResolver( + workflowWithConnections, + {}, + {}, + undefined, + accessibleBlocksMap + ) + contextWithConnections = { + workflowId: 'test-workflow', + blockStates: new Map([ + ['starter-1', { output: { input: 'Hello World' }, executed: true, executionTime: 0 }], + ['agent-1', { output: { content: 'Agent response' }, executed: true, executionTime: 0 }], + [ + 'function-1', + { output: { result: 'Function result' }, executed: true, executionTime: 0 }, + ], + [ + 'isolated-block', + { output: { content: 'Isolated content' }, executed: true, executionTime: 0 }, + ], + ]), + blockLogs: [], + metadata: { duration: 0 }, + environmentVariables: {}, + decisions: { router: new Map(), condition: new Map() }, + loopIterations: new Map(), + loopItems: new Map(), + completedLoops: new Set(), + executedBlocks: new Set(), + activeExecutionPath: new Set(['starter-1', 'agent-1', 'function-1', 'isolated-block']), + workflow: workflowWithConnections, + } + }) + + it('should allow references to directly connected blocks', () => { + const functionBlock = workflowWithConnections.blocks[2] // function-1 + const testBlock: SerializedBlock = { + ...functionBlock, + config: { + tool: 'function', + params: { + code: 'return ', // function-1 can reference agent-1 (connected) + }, + }, + } + + const result = connectionResolver.resolveInputs(testBlock, contextWithConnections) + expect(result.code).toBe('return "Agent response"') + }) + + it('should reject references to unconnected blocks', () => { + // Create a new block that is added to the workflow but not connected to isolated-block + workflowWithConnections.blocks.push({ + id: 'test-block', + metadata: { id: 'function', name: 'Test Block' }, + position: { x: 500, y: 500 }, + config: { tool: 'function', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }) + + // Add a connection so test-block can reference agent-1 but not isolated-block + workflowWithConnections.connections.push({ source: 'agent-1', target: 'test-block' }) + + // Update the accessibility map for test-block to include the new connection + const testBlockAccessible = new Set() + workflowWithConnections.connections.forEach((conn) => { + if (conn.target === 'test-block') { + testBlockAccessible.add(conn.source) + } + }) + // Always allow starter block access + const starterBlock = workflowWithConnections.blocks.find((b) => b.metadata?.id === 'starter') + if (starterBlock) { + testBlockAccessible.add(starterBlock.id) + } + connectionResolver.accessibleBlocksMap?.set('test-block', testBlockAccessible) + + const testBlock: SerializedBlock = { + id: 'test-block', + metadata: { id: 'function', name: 'Test Block' }, + position: { x: 500, y: 500 }, + config: { + tool: 'function', + params: { + code: 'return ', // test-block cannot reference isolated-block (not connected) + }, + }, + inputs: {}, + outputs: {}, + enabled: true, + } + + expect(() => connectionResolver.resolveInputs(testBlock, contextWithConnections)).toThrow( + /Block "isolated-block" is not connected to this block/ + ) + }) + + it('should always allow references to starter block', () => { + const functionBlock = workflowWithConnections.blocks[2] // function-1 + const testBlock: SerializedBlock = { + ...functionBlock, + config: { + tool: 'function', + params: { + code: 'return ', // Any block can reference start + }, + }, + } + + const result = connectionResolver.resolveInputs(testBlock, contextWithConnections) + expect(result.code).toBe('return Hello World') // Should not be quoted for function blocks + }) + + it('should provide helpful error messages for unconnected blocks', () => { + // Create a test block in the workflow first + workflowWithConnections.blocks.push({ + id: 'test-block-2', + metadata: { id: 'function', name: 'Test Block 2' }, + position: { x: 600, y: 600 }, + config: { tool: 'function', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }) + + // Add a connection so test-block-2 can reference agent-1 + workflowWithConnections.connections.push({ source: 'agent-1', target: 'test-block-2' }) + + // Update the accessibility map for test-block-2 to include the new connection + const testBlock2Accessible = new Set() + workflowWithConnections.connections.forEach((conn) => { + if (conn.target === 'test-block-2') { + testBlock2Accessible.add(conn.source) + } + }) + // Always allow starter block access + const starterBlock = workflowWithConnections.blocks.find((b) => b.metadata?.id === 'starter') + if (starterBlock) { + testBlock2Accessible.add(starterBlock.id) + } + connectionResolver.accessibleBlocksMap?.set('test-block-2', testBlock2Accessible) + + const testBlock: SerializedBlock = { + id: 'test-block-2', + metadata: { id: 'function', name: 'Test Block 2' }, + position: { x: 600, y: 600 }, + config: { + tool: 'function', + params: { + code: 'return ', + }, + }, + inputs: {}, + outputs: {}, + enabled: true, + } + + expect(() => connectionResolver.resolveInputs(testBlock, contextWithConnections)).toThrow( + /Available connected blocks:.*Agent Block.*agent-1.*start/ + ) + }) + + it('should work with block names and normalized names', () => { + const functionBlock = workflowWithConnections.blocks[2] // function-1 + const testBlock: SerializedBlock = { + ...functionBlock, + config: { + tool: 'function', + params: { + nameRef: '', // Reference by actual name + normalizedRef: '', // Reference by normalized name + idRef: '', // Reference by ID + }, + }, + } + + const result = connectionResolver.resolveInputs(testBlock, contextWithConnections) + expect(result.nameRef).toBe('"Agent response"') // Should be quoted for function blocks + expect(result.normalizedRef).toBe('"Agent response"') // Should be quoted for function blocks + expect(result.idRef).toBe('"Agent response"') // Should be quoted for function blocks + }) + + it('should handle complex connection graphs', () => { + // Add a new block connected to function-1 + const extendedWorkflow = { + ...workflowWithConnections, + blocks: [ + ...workflowWithConnections.blocks, + { + id: 'response-1', + metadata: { id: 'response', name: 'Response Block' }, + position: { x: 400, y: 400 }, + config: { tool: 'response', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + ], + connections: [ + ...workflowWithConnections.connections, + { source: 'function-1', target: 'response-1' }, + ], + } + + // Create accessibility map for extended workflow + const extendedAccessibilityMap = new Map>() + const extendedTestBlockIds = [ + 'test-response-block', + 'test-block', + 'test-block-2', + 'generic-block', + ] + + extendedWorkflow.blocks.forEach((block) => { + const accessibleBlocks = new Set() + // Add directly connected blocks (sources that connect to this block) + extendedWorkflow.connections.forEach((conn) => { + if (conn.target === block.id) { + accessibleBlocks.add(conn.source) + } + }) + // Always allow starter block access + const starterBlock = extendedWorkflow.blocks.find((b) => b.metadata?.id === 'starter') + if (starterBlock) { + accessibleBlocks.add(starterBlock.id) + } + extendedAccessibilityMap.set(block.id, accessibleBlocks) + }) + + // Set up accessibility for test blocks + extendedTestBlockIds.forEach((testId) => { + const accessibleBlocks = new Set() + // Add directly connected blocks + extendedWorkflow.connections.forEach((conn) => { + if (conn.target === testId) { + accessibleBlocks.add(conn.source) + } + }) + // Always allow starter block access + const starterBlock = extendedWorkflow.blocks.find((b) => b.metadata?.id === 'starter') + if (starterBlock) { + accessibleBlocks.add(starterBlock.id) + } + extendedAccessibilityMap.set(testId, accessibleBlocks) + }) + + const extendedResolver = new InputResolver( + extendedWorkflow, + {}, + {}, + undefined, + extendedAccessibilityMap + ) + const responseBlock = extendedWorkflow.blocks[4] // response-1 + const testBlock: SerializedBlock = { + ...responseBlock, + config: { + tool: 'response', + params: { + canReferenceFunction: '', // Can reference directly connected function-1 + cannotReferenceAgent: '', // Cannot reference agent-1 (not directly connected) + }, + }, + } + + const extendedContext = { + ...contextWithConnections, + workflow: extendedWorkflow, + blockStates: new Map([ + ...contextWithConnections.blockStates, + [ + 'response-1', + { output: { message: 'Final response' }, executed: true, executionTime: 0 }, + ], + ]), + } + + // Should work for direct connection + expect(() => { + const block1 = { + ...testBlock, + config: { tool: 'response', params: { test: '' } }, + } + extendedResolver.resolveInputs(block1, extendedContext) + }).not.toThrow() + + // Should fail for indirect connection + expect(() => { + // Add the response block to the workflow so it can be validated properly + extendedWorkflow.blocks.push({ + id: 'test-response-block', + metadata: { id: 'response', name: 'Test Response Block' }, + position: { x: 500, y: 500 }, + config: { tool: 'response', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }) + extendedWorkflow.connections.push({ source: 'function-1', target: 'test-response-block' }) + + const block2 = { + id: 'test-response-block', + metadata: { id: 'response', name: 'Test Response Block' }, + position: { x: 500, y: 500 }, + config: { tool: 'response', params: { test: '' } }, + inputs: {}, + outputs: {}, + enabled: true, + } + extendedResolver.resolveInputs(block2, extendedContext) + }).toThrow(/Block "agent-1" is not connected to this block/) + }) + + it('should handle blocks in same loop referencing each other', () => { + const loopWorkflow: SerializedWorkflow = { + version: '1.0', + blocks: [ + { + id: 'starter-1', + metadata: { id: 'starter', name: 'Start' }, + position: { x: 0, y: 0 }, + config: { tool: 'starter', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'loop-1', + metadata: { id: 'loop', name: 'Loop' }, + position: { x: 100, y: 100 }, + config: { tool: '', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'function-1', + metadata: { id: 'function', name: 'Function 1' }, + position: { x: 200, y: 200 }, + config: { tool: 'function', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + { + id: 'function-2', + metadata: { id: 'function', name: 'Function 2' }, + position: { x: 300, y: 300 }, + config: { tool: 'function', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + }, + ], + connections: [{ source: 'starter-1', target: 'loop-1' }], + loops: { + 'loop-1': { + id: 'loop-1', + nodes: ['function-1', 'function-2'], // Both functions in same loop + iterations: 3, + loopType: 'for', + }, + }, + } + + // Create accessibility map for loop workflow + const loopAccessibilityMap = new Map>() + const loopTestBlockIds = ['test-block', 'test-block-2', 'generic-block'] + + loopWorkflow.blocks.forEach((block) => { + const accessibleBlocks = new Set() + // Add directly connected blocks + loopWorkflow.connections.forEach((conn) => { + if (conn.target === block.id) { + accessibleBlocks.add(conn.source) + } + }) + // Always allow starter block access + const starterBlock = loopWorkflow.blocks.find((b) => b.metadata?.id === 'starter') + if (starterBlock) { + accessibleBlocks.add(starterBlock.id) + } + // Allow blocks in same loop to reference each other + const blockLoop = Object.values(loopWorkflow.loops || {}).find((loop) => + loop.nodes.includes(block.id) + ) + if (blockLoop) { + blockLoop.nodes.forEach((nodeId) => accessibleBlocks.add(nodeId)) + } + loopAccessibilityMap.set(block.id, accessibleBlocks) + }) + + // Set up accessibility for test blocks + loopTestBlockIds.forEach((testId) => { + const accessibleBlocks = new Set() + // Add directly connected blocks + loopWorkflow.connections.forEach((conn) => { + if (conn.target === testId) { + accessibleBlocks.add(conn.source) + } + }) + // Always allow starter block access + const starterBlock = loopWorkflow.blocks.find((b) => b.metadata?.id === 'starter') + if (starterBlock) { + accessibleBlocks.add(starterBlock.id) + } + loopAccessibilityMap.set(testId, accessibleBlocks) + }) + + const loopResolver = new InputResolver(loopWorkflow, {}, {}, undefined, loopAccessibilityMap) + const testBlock: SerializedBlock = { + ...loopWorkflow.blocks[2], + config: { + tool: 'function', + params: { + code: 'return ', // function-1 can reference function-2 (same loop) + }, + }, + } + + const loopContext = { + ...contextWithConnections, + workflow: loopWorkflow, + blockStates: new Map([ + ['starter-1', { output: { input: 'Hello' }, executed: true, executionTime: 0 }], + ['function-1', { output: { result: 'Result 1' }, executed: true, executionTime: 0 }], + ['function-2', { output: { result: 'Result 2' }, executed: true, executionTime: 0 }], + ]), + } + + expect(() => loopResolver.resolveInputs(testBlock, loopContext)).not.toThrow() + }) + }) }) diff --git a/apps/sim/executor/resolver.ts b/apps/sim/executor/resolver.ts index 33f269845..fd268ea99 100644 --- a/apps/sim/executor/resolver.ts +++ b/apps/sim/executor/resolver.ts @@ -1,3 +1,4 @@ +import { BlockPathCalculator } from '@/lib/block-path-calculator' import { createLogger } from '@/lib/logs/console-logger' import { VariableManager } from '@/lib/variables/variable-manager' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' @@ -18,7 +19,8 @@ export class InputResolver { private workflow: SerializedWorkflow, private environmentVariables: Record, private workflowVariables: Record = {}, - private loopManager?: LoopManager + private loopManager?: LoopManager, + public accessibleBlocksMap?: Map> ) { // Create maps for efficient lookups this.blockById = new Map(workflow.blocks.map((block) => [block.id, block])) @@ -373,21 +375,28 @@ export class InputResolver { const path = match.slice(1, -1) const [blockRef, ...pathParts] = path.split('.') - // Skip XML-like tags that have no path parts (not a valid block reference) - if (pathParts.length === 0 || blockRef.includes(':') || blockRef.includes(' ')) { + // Skip XML-like tags (but allow block names with spaces) + if (blockRef.includes(':')) { continue } + // System references (start, loop, parallel, variable) are handled as special cases + const isSystemReference = ['start', 'loop', 'parallel', 'variable'].includes( + blockRef.toLowerCase() + ) + + // System references and regular block references are both processed + // Accessibility validation happens later in validateBlockReference + // Special case for "start" references - // This allows users to reference the starter block using - // regardless of the actual name of the starter block if (blockRef.toLowerCase() === 'start') { // Find the starter block const starterBlock = this.workflow.blocks.find((block) => block.metadata?.id === 'starter') if (starterBlock) { const blockState = context.blockStates.get(starterBlock.id) if (blockState) { - // Navigate through the path parts + // For starter block, start directly with the flattened output + // This enables direct access to and let replacementValue: any = blockState.output for (const part of pathParts) { @@ -432,7 +441,7 @@ export class InputResolver { } // For all other blocks, stringify objects else { - // Preserve full JSON structure for objects (especially for structured inputs with conversationId) + // Preserve full JSON structure for objects formattedValue = JSON.stringify(replacementValue) } } else { @@ -502,20 +511,14 @@ export class InputResolver { } } - // Standard block reference resolution - let sourceBlock = this.blockById.get(blockRef) - if (!sourceBlock) { - const normalizedRef = this.normalizeBlockName(blockRef) - sourceBlock = this.blockByNormalizedName.get(normalizedRef) + // Standard block reference resolution with connection validation + const validation = this.validateBlockReference(blockRef, currentBlock.id, context) + + if (!validation.isValid) { + throw new Error(validation.errorMessage!) } - if (!sourceBlock) { - // Provide a more helpful error message with available block names - const availableBlocks = Array.from(this.blockByNormalizedName.keys()).join(', ') - throw new Error( - `Block reference "${blockRef}" was not found. Available blocks: ${availableBlocks}. For the starter block, try using "start" or the exact block name.` - ) - } + const sourceBlock = this.blockById.get(validation.resolvedBlockId!)! if (sourceBlock.enabled === false) { throw new Error( @@ -833,6 +836,180 @@ export class InputResolver { return foundVariable ? foundVariable[1] : undefined } + /** + * Gets all blocks that the current block can reference. + * Uses pre-calculated accessible blocks if available, otherwise falls back to legacy calculation. + * + * @param currentBlockId - ID of the block requesting references + * @returns Set of accessible block IDs + */ + private getAccessibleBlocks(currentBlockId: string): Set { + // Use pre-calculated accessible blocks if available + if (this.accessibleBlocksMap?.has(currentBlockId)) { + return this.accessibleBlocksMap.get(currentBlockId)! + } + + // Fallback to legacy calculation for backward compatibility + return this.calculateAccessibleBlocksLegacy(currentBlockId) + } + + /** + * Legacy method for calculating accessible blocks (for backward compatibility). + * This method is kept for cases where pre-calculated data is not available. + * + * @param currentBlockId - ID of the block requesting references + * @returns Set of accessible block IDs + */ + private calculateAccessibleBlocksLegacy(currentBlockId: string): Set { + const accessibleBlocks = new Set() + + // Add blocks that have outgoing connections TO this block + for (const connection of this.workflow.connections) { + if (connection.target === currentBlockId) { + accessibleBlocks.add(connection.source) + } + } + + // Always allow referencing the starter block (special case) + const starterBlock = this.workflow.blocks.find((block) => block.metadata?.id === 'starter') + if (starterBlock) { + accessibleBlocks.add(starterBlock.id) + } + + // Special case: blocks in the same loop can reference each other + const currentBlockLoop = this.loopsByBlockId.get(currentBlockId) + if (currentBlockLoop) { + const loop = this.workflow.loops?.[currentBlockLoop] + if (loop) { + for (const nodeId of loop.nodes) { + accessibleBlocks.add(nodeId) + } + } + } + + // Special case: blocks in the same parallel can reference each other + for (const [parallelId, parallel] of Object.entries(this.workflow.parallels || {})) { + if (parallel.nodes.includes(currentBlockId)) { + for (const nodeId of parallel.nodes) { + accessibleBlocks.add(nodeId) + } + } + } + + return accessibleBlocks + } + + /** + * Gets block names that the current block can reference for helpful error messages. + * Uses shared utility when pre-calculated data is available. + * + * @param currentBlockId - ID of the block requesting references + * @returns Array of accessible block names and aliases + */ + private getAccessibleBlockNames(currentBlockId: string): string[] { + // Use shared utility if pre-calculated data is available + if (this.accessibleBlocksMap) { + return BlockPathCalculator.getAccessibleBlockNames( + currentBlockId, + this.workflow, + this.accessibleBlocksMap + ) + } + + // Fallback to legacy calculation + const accessibleBlockIds = this.getAccessibleBlocks(currentBlockId) + const names: string[] = [] + + for (const blockId of accessibleBlockIds) { + const block = this.blockById.get(blockId) + if (block) { + // Add both the actual name and the normalized name + if (block.metadata?.name) { + names.push(block.metadata.name) + names.push(this.normalizeBlockName(block.metadata.name)) + } + names.push(blockId) + } + } + + // Add special aliases + names.push('start') // Always allow start alias + + return [...new Set(names)] // Remove duplicates + } + + /** + * Checks if a block reference could potentially be valid without throwing errors. + * Used to filter out non-block patterns like from block reference resolution. + * + * @param blockRef - The block reference to check + * @param currentBlockId - ID of the current block + * @returns Whether this could be a valid block reference + */ + private isAccessibleBlockReference(blockRef: string, currentBlockId: string): boolean { + // Special cases that are always allowed + const specialRefs = ['start', 'loop', 'parallel'] + if (specialRefs.includes(blockRef.toLowerCase())) { + return true + } + + // Get all accessible block names for this block + const accessibleNames = this.getAccessibleBlockNames(currentBlockId) + + // Check if the reference matches any accessible block name + return accessibleNames.includes(blockRef) || accessibleNames.includes(blockRef.toLowerCase()) + } + + /** + * Validates if a block reference is accessible from the current block. + * Checks existence and connection-based access rules. + * + * @param blockRef - Name or ID of the referenced block + * @param currentBlockId - ID of the block making the reference + * @param context - Current execution context + * @returns Validation result with success status and resolved block ID or error message + */ + private validateBlockReference( + blockRef: string, + currentBlockId: string, + context: ExecutionContext + ): { isValid: boolean; resolvedBlockId?: string; errorMessage?: string } { + // Special case: 'start' is always allowed + if (blockRef.toLowerCase() === 'start') { + const starterBlock = this.workflow.blocks.find((block) => block.metadata?.id === 'starter') + return starterBlock + ? { isValid: true, resolvedBlockId: starterBlock.id } + : { isValid: false, errorMessage: 'Starter block not found in workflow' } + } + + // Check if block exists + let sourceBlock = this.blockById.get(blockRef) + if (!sourceBlock) { + const normalizedRef = this.normalizeBlockName(blockRef) + sourceBlock = this.blockByNormalizedName.get(normalizedRef) + } + + if (!sourceBlock) { + const accessibleNames = this.getAccessibleBlockNames(currentBlockId) + return { + isValid: false, + errorMessage: `Block "${blockRef}" was not found. Available connected blocks: ${accessibleNames.join(', ')}`, + } + } + + // Check if block is accessible (connected) + const accessibleBlocks = this.getAccessibleBlocks(currentBlockId) + if (!accessibleBlocks.has(sourceBlock.id)) { + const accessibleNames = this.getAccessibleBlockNames(currentBlockId) + return { + isValid: false, + errorMessage: `Block "${blockRef}" is not connected to this block. Available connected blocks: ${accessibleNames.join(', ')}`, + } + } + + return { isValid: true, resolvedBlockId: sourceBlock.id } + } + /** * Gets the items for a forEach loop. * The items can be stored directly in loop.forEachItems or may need to be evaluated. @@ -891,7 +1068,7 @@ export class InputResolver { // As a fallback, look for the most recent array or object in any block's output // This is less reliable but might help in some cases for (const [_blockId, blockState] of context.blockStates.entries()) { - const output = blockState.output?.response + const output = blockState.output if (output) { for (const [_key, value] of Object.entries(output)) { if (Array.isArray(value) && value.length > 0) { diff --git a/apps/sim/executor/types.ts b/apps/sim/executor/types.ts index b361fc3ed..ac76a3538 100644 --- a/apps/sim/executor/types.ts +++ b/apps/sim/executor/types.ts @@ -5,37 +5,37 @@ import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' * Standardized block output format that ensures compatibility with the execution engine. */ export interface NormalizedBlockOutput { - /** Primary response data from the block execution */ - response: { - [key: string]: any - content?: string // Text content from LLM responses - model?: string // Model identifier used for generation - tokens?: { - prompt?: number - completion?: number - total?: number - } - toolCalls?: { - list: any[] - count: number - } - selectedPath?: { - blockId: string - blockType?: string - blockTitle?: string - } - selectedConditionId?: string // ID of selected condition - conditionResult?: boolean // Whether condition evaluated to true - result?: any // Generic result value - stdout?: string // Standard output from function execution - executionTime?: number // Time taken to execute - data?: any // Response data from API calls - status?: number // HTTP status code - headers?: Record // HTTP headers - error?: string // Error message if block execution failed + [key: string]: any + // Content fields + content?: string // Text content from LLM responses + model?: string // Model identifier used for generation + tokens?: { + prompt?: number + completion?: number + total?: number } - error?: string // Top-level error field for easy error checking - [key: string]: any // Additional properties + toolCalls?: { + list: any[] + count: number + } + // Path selection fields + selectedPath?: { + blockId: string + blockType?: string + blockTitle?: string + } + selectedConditionId?: string // ID of selected condition + conditionResult?: boolean // Whether condition evaluated to true + // Generic result fields + result?: any // Generic result value + stdout?: string // Standard output from function execution + executionTime?: number // Time taken to execute + // API response fields + data?: any // Response data from API calls + status?: number // HTTP status code + headers?: Record // HTTP headers + // Error handling + error?: string // Error message if block execution failed } /** diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index 8fec7a7b8..885ed0e6d 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -351,7 +351,7 @@ export function useCollaborativeWorkflow() { } // Generate outputs using the same logic as the store - const outputs = resolveOutputType(blockConfig.outputs, subBlocks) + const outputs = resolveOutputType(blockConfig.outputs) const completeBlockData = { id, diff --git a/apps/sim/lib/auth/internal.ts b/apps/sim/lib/auth/internal.ts new file mode 100644 index 000000000..1f70b1af4 --- /dev/null +++ b/apps/sim/lib/auth/internal.ts @@ -0,0 +1,47 @@ +import { jwtVerify, SignJWT } from 'jose' +import { env } from '@/lib/env' + +// Create a secret key for JWT signing +const getJwtSecret = () => { + const secret = new TextEncoder().encode(env.INTERNAL_API_SECRET) + return secret +} + +/** + * Generate an internal JWT token for server-side API calls + * Token expires in 5 minutes to keep it short-lived + */ +export async function generateInternalToken(): Promise { + const secret = getJwtSecret() + + const token = await new SignJWT({ type: 'internal' }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('5m') + .setIssuer('sim-internal') + .setAudience('sim-api') + .sign(secret) + + return token +} + +/** + * Verify an internal JWT token + * Returns true if valid, false otherwise + */ +export async function verifyInternalToken(token: string): Promise { + try { + const secret = getJwtSecret() + + const { payload } = await jwtVerify(token, secret, { + issuer: 'sim-internal', + audience: 'sim-api', + }) + + // Check that it's an internal token + return payload.type === 'internal' + } catch (error) { + // Token verification failed + return false + } +} diff --git a/apps/sim/lib/block-path-calculator.ts b/apps/sim/lib/block-path-calculator.ts new file mode 100644 index 000000000..8481297e7 --- /dev/null +++ b/apps/sim/lib/block-path-calculator.ts @@ -0,0 +1,136 @@ +import type { SerializedWorkflow } from '../serializer/types' + +/** + * Shared utility for calculating block paths and accessible connections. + * Used by both frontend (useBlockConnections) and backend (InputResolver) to ensure consistency. + */ +export class BlockPathCalculator { + /** + * Finds all blocks along paths leading to the target block. + * This is a reverse traversal from the target node to find all ancestors + * along connected paths using BFS. + * + * @param edges - List of all edges in the graph + * @param targetNodeId - ID of the target block we're finding connections for + * @returns Array of unique ancestor node IDs + */ + static findAllPathNodes( + edges: Array<{ source: string; target: string }>, + targetNodeId: string + ): string[] { + // We'll use a reverse topological sort approach by tracking "distance" from target + const nodeDistances = new Map() + const visited = new Set() + const queue: [string, number][] = [[targetNodeId, 0]] // [nodeId, distance] + const pathNodes = new Set() + + // Build a reverse adjacency list for faster traversal + const reverseAdjList: Record = {} + for (const edge of edges) { + if (!reverseAdjList[edge.target]) { + reverseAdjList[edge.target] = [] + } + reverseAdjList[edge.target].push(edge.source) + } + + // BFS to find all ancestors and their shortest distance from target + while (queue.length > 0) { + const [currentNodeId, distance] = queue.shift()! + + if (visited.has(currentNodeId)) { + // If we've seen this node before, update its distance if this path is shorter + const currentDistance = nodeDistances.get(currentNodeId) || Number.POSITIVE_INFINITY + if (distance < currentDistance) { + nodeDistances.set(currentNodeId, distance) + } + continue + } + + visited.add(currentNodeId) + nodeDistances.set(currentNodeId, distance) + + // Don't add the target node itself to the results + if (currentNodeId !== targetNodeId) { + pathNodes.add(currentNodeId) + } + + // Get all incoming edges from the reverse adjacency list + const incomingNodeIds = reverseAdjList[currentNodeId] || [] + + // Add all source nodes to the queue with incremented distance + for (const sourceId of incomingNodeIds) { + queue.push([sourceId, distance + 1]) + } + } + + return Array.from(pathNodes) + } + + /** + * Calculates accessible blocks for all blocks in a workflow. + * This ensures consistent block reference resolution across frontend and backend. + * + * @param workflow - The serialized workflow + * @returns Map of block ID to Set of accessible block IDs + */ + static calculateAccessibleBlocksForWorkflow( + workflow: SerializedWorkflow + ): Map> { + const accessibleMap = new Map>() + + for (const block of workflow.blocks) { + const accessibleBlocks = new Set() + + // Find all blocks along paths leading to this block + const pathNodes = BlockPathCalculator.findAllPathNodes(workflow.connections, block.id) + pathNodes.forEach((nodeId) => accessibleBlocks.add(nodeId)) + + // Always allow referencing the starter block (special case) + const starterBlock = workflow.blocks.find((b) => b.metadata?.id === 'starter') + if (starterBlock && starterBlock.id !== block.id) { + accessibleBlocks.add(starterBlock.id) + } + + accessibleMap.set(block.id, accessibleBlocks) + } + + return accessibleMap + } + + /** + * Gets accessible block names for a specific block (for error messages). + * + * @param blockId - The block ID to get accessible names for + * @param workflow - The serialized workflow + * @param accessibleBlocksMap - Pre-calculated accessible blocks map + * @returns Array of accessible block names and aliases + */ + static getAccessibleBlockNames( + blockId: string, + workflow: SerializedWorkflow, + accessibleBlocksMap: Map> + ): string[] { + const accessibleBlockIds = accessibleBlocksMap.get(blockId) || new Set() + const names: string[] = [] + + // Create a map of block IDs to blocks for efficient lookup + const blockById = new Map(workflow.blocks.map((block) => [block.id, block])) + + for (const accessibleBlockId of accessibleBlockIds) { + const block = blockById.get(accessibleBlockId) + if (block) { + // Add both the actual name and the normalized name + if (block.metadata?.name) { + names.push(block.metadata.name) + names.push(block.metadata.name.toLowerCase().replace(/\s+/g, '')) + } + names.push(accessibleBlockId) + } + } + + // Add special aliases + names.push('start') // Always allow start alias + + return [...new Set(names)] // Remove duplicates + } +} diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index 45287305e..79247c446 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -13,6 +13,7 @@ export const env = createEnv({ BETTER_AUTH_SECRET: z.string().min(32), DISABLE_REGISTRATION: z.boolean().optional(), ENCRYPTION_KEY: z.string().min(32), + INTERNAL_API_SECRET: z.string().min(32), POSTGRES_URL: z.string().url().optional(), STRIPE_SECRET_KEY: z.string().min(1).optional(), diff --git a/apps/sim/lib/logs/execution-logger.ts b/apps/sim/lib/logs/execution-logger.ts index a726d7d21..b1086090a 100644 --- a/apps/sim/lib/logs/execution-logger.ts +++ b/apps/sim/lib/logs/execution-logger.ts @@ -115,46 +115,45 @@ export async function persistExecutionLogs( blockType: log.blockType, outputKeys: Object.keys(log.output), hasToolCalls: !!log.output.toolCalls, - hasResponse: !!log.output.response, + hasResponse: !!log.output, }) // FIRST PASS - Check if this is a no-tool scenario with tokens data not propagated // In some cases, the token data from the streaming callback doesn't properly get into // the agent block response. This ensures we capture it. if ( - log.output.response && - (!log.output.response.tokens?.completion || - log.output.response.tokens.completion === 0) && - (!log.output.response.toolCalls || - !log.output.response.toolCalls.list || - log.output.response.toolCalls.list.length === 0) + log.output && + (!log.output.tokens?.completion || log.output.tokens.completion === 0) && + (!log.output.toolCalls || + !log.output.toolCalls.list || + log.output.toolCalls.list.length === 0) ) { - // Check if output response has providerTiming - this indicates it's a streaming response - if (log.output.response.providerTiming) { + // Check if output has providerTiming - this indicates it's a streaming response + if (log.output.providerTiming) { logger.debug('Processing streaming response without tool calls for token extraction', { blockId: log.blockId, - hasTokens: !!log.output.response.tokens, - hasProviderTiming: !!log.output.response.providerTiming, + hasTokens: !!log.output.tokens, + hasProviderTiming: !!log.output.providerTiming, }) // Only for no-tool streaming cases, extract content length and estimate token count - const contentLength = log.output.response.content?.length || 0 + const contentLength = log.output.content?.length || 0 if (contentLength > 0) { // Estimate completion tokens based on content length as a fallback const estimatedCompletionTokens = Math.ceil(contentLength / 4) - const promptTokens = log.output.response.tokens?.prompt || 8 + const promptTokens = log.output.tokens?.prompt || 8 // Update the tokens object - log.output.response.tokens = { + log.output.tokens = { prompt: promptTokens, completion: estimatedCompletionTokens, total: promptTokens + estimatedCompletionTokens, } // Update cost information using the provider's cost model - const model = log.output.response.model || 'gpt-4o' + const model = log.output.model || 'gpt-4o' const costInfo = calculateCost(model, promptTokens, estimatedCompletionTokens) - log.output.response.cost = { + log.output.cost = { input: costInfo.input, output: costInfo.output, total: costInfo.total, @@ -165,7 +164,7 @@ export async function persistExecutionLogs( blockId: log.blockId, contentLength, estimatedCompletionTokens, - tokens: log.output.response.tokens, + tokens: log.output.tokens, }) } } @@ -185,74 +184,54 @@ export async function persistExecutionLogs( // Extract the executionData and use it as our primary source of information const executionData = log.output.executionData - // If executionData has output with response, use that as our response + // If executionData has output, merge it with our output // This is especially important for streaming responses where the final content // is set in the executionData structure by the executor - if (executionData.output?.response) { - log.output.response = executionData.output.response - logger.debug('Using response from executionData', { - responseKeys: Object.keys(log.output.response), - hasContent: !!log.output.response.content, - contentLength: log.output.response.content?.length || 0, - hasToolCalls: !!log.output.response.toolCalls, - hasTokens: !!log.output.response.tokens, - hasCost: !!log.output.response.cost, + if (executionData.output) { + log.output = { ...log.output, ...executionData.output } + logger.debug('Using output from executionData', { + outputKeys: Object.keys(log.output), + hasContent: !!log.output.content, + contentLength: log.output.content?.length || 0, + hasToolCalls: !!log.output.toolCalls, + hasTokens: !!log.output.tokens, + hasCost: !!log.output.cost, }) } } - // Extract tool calls and other metadata - if (log.output.response) { - const response = log.output.response - - // Process tool calls - if (response.toolCalls?.list) { - metadata = { - toolCalls: response.toolCalls.list.map((tc: any) => ({ - name: stripCustomToolPrefix(tc.name), - duration: tc.duration || 0, - startTime: tc.startTime || new Date().toISOString(), - endTime: tc.endTime || new Date().toISOString(), - status: tc.error ? 'error' : 'success', - input: tc.input || tc.arguments, - output: tc.output || tc.result, - error: tc.error, - })), - } + // Add cost information if available + if (log.output?.cost) { + const output = log.output + if (!metadata) metadata = {} + metadata.cost = { + model: output.model, + input: output.cost.input, + output: output.cost.output, + total: output.cost.total, + tokens: output.tokens, + pricing: output.cost.pricing, } - // Add cost information if available - if (response.cost) { - if (!metadata) metadata = {} - metadata.cost = { - model: response.model, - input: response.cost.input, - output: response.cost.output, - total: response.cost.total, - tokens: response.tokens, - pricing: response.cost.pricing, + // Accumulate costs for workflow-level summary + if (output.cost.total) { + totalCost += output.cost.total + totalInputCost += output.cost.input || 0 + totalOutputCost += output.cost.output || 0 + + // Track tokens + if (output.tokens) { + totalPromptTokens += output.tokens.prompt || 0 + totalCompletionTokens += output.tokens.completion || 0 + totalTokens += output.tokens.total || 0 } - // Accumulate costs for workflow-level summary - if (response.cost.total) { - totalCost += response.cost.total - totalInputCost += response.cost.input || 0 - totalOutputCost += response.cost.output || 0 - - // Track tokens - if (response.tokens) { - totalPromptTokens += response.tokens.prompt || 0 - totalCompletionTokens += response.tokens.completion || 0 - totalTokens += response.tokens.total || 0 - } - - // Track model usage - if (response.model) { - modelCounts[response.model] = (modelCounts[response.model] || 0) + 1 - // Set the most frequently used model as primary - if (!primaryModel || modelCounts[response.model] > modelCounts[primaryModel]) { - primaryModel = response.model - } + // Track model usage + if (output.model) { + modelCounts[output.model] = (modelCounts[output.model] || 0) + 1 + // Set the most frequently used model as primary + if (!primaryModel || modelCounts[output.model] > modelCounts[primaryModel]) { + primaryModel = output.model } } } @@ -342,50 +321,7 @@ export async function persistExecutionLogs( } }) } - // Case 3: Response has toolCalls - else if (log.output.response?.toolCalls) { - const toolCalls = Array.isArray(log.output.response.toolCalls) - ? log.output.response.toolCalls - : log.output.response.toolCalls.list || [] - - logger.debug('Found toolCalls in response', { - count: toolCalls.length, - }) - - // Log raw timing data for debugging - toolCalls.forEach((tc: any, idx: number) => { - logger.debug(`Response tool call ${idx} raw timing data:`, { - name: stripCustomToolPrefix(tc.name), - startTime: tc.startTime, - endTime: tc.endTime, - duration: tc.duration, - timing: tc.timing, - argumentKeys: tc.arguments ? Object.keys(tc.arguments) : undefined, - }) - }) - - toolCallData = toolCalls.map((toolCall: any) => { - // Extract timing info - try various formats that providers might use - const duration = extractDuration(toolCall) - const timing = extractTimingInfo( - toolCall, - blockStartTime ? new Date(blockStartTime) : undefined, - blockEndTime ? new Date(blockEndTime) : undefined - ) - - return { - name: toolCall.name, - duration: duration, - startTime: timing.startTime, - endTime: timing.endTime, - status: toolCall.error ? 'error' : 'success', - input: toolCall.arguments || toolCall.input, - output: toolCall.result || toolCall.output, - error: toolCall.error, - } - }) - } - // Case 4: toolCalls is an object and has a list property + // Case 3: toolCalls is an object and has a list property else if ( log.output.toolCalls && typeof log.output.toolCalls === 'object' && @@ -438,9 +374,9 @@ export async function persistExecutionLogs( } }) } - // Case 5: Look in executionData.output.response for streaming responses - else if (log.output.executionData?.output?.response?.toolCalls) { - const toolCallsObj = log.output.executionData.output.response.toolCalls + // Case 4: Look in executionData.output for streaming responses + else if (log.output.executionData?.output?.toolCalls) { + const toolCallsObj = log.output.executionData.output.toolCalls const list = Array.isArray(toolCallsObj) ? toolCallsObj : toolCallsObj.list || [] logger.debug('Found toolCalls in executionData output response', { @@ -480,9 +416,9 @@ export async function persistExecutionLogs( } }) } - // Case 6: Parse the response string for toolCalls as a last resort - else if (typeof log.output.response === 'string') { - const match = log.output.response.match(/"toolCalls"\s*:\s*({[^}]*}|(\[.*?\]))/s) + // Case 5: Parse the output string for toolCalls as a last resort + else if (typeof log.output === 'string') { + const match = log.output.match(/"toolCalls"\s*:\s*({[^}]*}|(\[.*?\]))/s) if (match) { try { const toolCallsJson = JSON.parse(`{${match[0]}}`) @@ -535,9 +471,9 @@ export async function persistExecutionLogs( } }) } catch (error) { - logger.error('Error parsing toolCalls from response string', { + logger.error('Error parsing toolCalls from output string', { error, - response: log.output.response, + output: log.output, }) } } @@ -549,7 +485,7 @@ export async function persistExecutionLogs( }) } - // Fill in missing timing information + // Fill in missing timing information and merge with existing metadata if (toolCallData.length > 0) { const getToolCalls = getToolCallTimings( toolCallData, @@ -563,12 +499,13 @@ export async function persistExecutionLogs( input: redactApiKeys(toolCall.input), })) - metadata = { - toolCalls: redactedToolCalls, - } + // Merge with existing metadata instead of overwriting + if (!metadata) metadata = {} + metadata.toolCalls = redactedToolCalls - logger.debug('Created metadata with tool calls', { + logger.debug('Added tool calls to metadata', { count: redactedToolCalls.length, + existingMetadata: Object.keys(metadata).filter((k) => k !== 'toolCalls'), }) } } @@ -580,9 +517,9 @@ export async function persistExecutionLogs( level: log.success ? 'info' : 'error', message: log.success ? `Block ${log.blockName || log.blockId} (${log.blockType || 'unknown'}): ${ - log.output?.response?.content || - log.output?.executionData?.output?.response?.content || - JSON.stringify(log.output?.response || {}) + log.output?.content || + log.output?.executionData?.output?.content || + JSON.stringify(log.output || {}) }` : `Block ${log.blockName || log.blockId} (${log.blockType || 'unknown'}): ${log.error || 'Failed'}`, duration: log.success ? `${log.durationMs}ms` : 'NA', @@ -646,8 +583,8 @@ export async function persistExecutionLogs( if (primaryModel && result.logs && result.logs.length > 0) { // Find the first agent log with pricing info for (const log of result.logs) { - if (log.output?.response?.cost?.pricing) { - workflowMetadata.cost.pricing = log.output.response.cost.pricing + if (log.output?.cost?.pricing) { + workflowMetadata.cost.pricing = log.output.cost.pricing break } } diff --git a/apps/sim/lib/logs/trace-spans.ts b/apps/sim/lib/logs/trace-spans.ts index 02ef418f9..20c08c27b 100644 --- a/apps/sim/lib/logs/trace-spans.ts +++ b/apps/sim/lib/logs/trace-spans.ts @@ -57,8 +57,8 @@ export function buildTraceSpans(result: ExecutionResult): { } // Add provider timing data if it exists - if (log.output?.response?.providerTiming) { - const providerTiming = log.output.response.providerTiming + if (log.output?.providerTiming) { + const providerTiming = log.output.providerTiming // If we have time segments, use them to create a more detailed timeline if (providerTiming.timeSegments && providerTiming.timeSegments.length > 0) { @@ -149,18 +149,18 @@ export function buildTraceSpans(result: ExecutionResult): { // Create a child span for the provider execution const providerSpan: TraceSpan = { id: `${spanId}-provider`, - name: log.output.response.model || 'AI Provider', + name: log.output.model || 'AI Provider', type: 'provider', duration: providerTiming.duration || 0, startTime: providerTiming.startTime || log.startedAt, endTime: providerTiming.endTime || log.endedAt, status: 'success', - tokens: log.output.response.tokens?.total, + tokens: log.output.tokens?.total, } // If we have model time, create a child span for just the model processing if (providerTiming.modelTime) { - const modelName = log.output.response.model || '' + const modelName = log.output.model || '' const modelSpan: TraceSpan = { id: `${spanId}-model`, name: `Model Generation${modelName ? ` (${modelName})` : ''}`, @@ -169,7 +169,7 @@ export function buildTraceSpans(result: ExecutionResult): { startTime: providerTiming.startTime, // Approximate endTime: providerTiming.endTime, // Approximate status: 'success', - tokens: log.output.response.tokens?.completion, + tokens: log.output.tokens?.completion, } if (!providerSpan.children) providerSpan.children = [] @@ -180,8 +180,8 @@ export function buildTraceSpans(result: ExecutionResult): { span.children.push(providerSpan) // When using provider timing without segments, still add tool calls if they exist - if (log.output?.response?.toolCalls?.list) { - span.toolCalls = log.output.response.toolCalls.list.map((tc: any) => ({ + if (log.output?.toolCalls?.list) { + span.toolCalls = log.output.toolCalls.list.map((tc: any) => ({ name: stripCustomToolPrefix(tc.name), duration: tc.duration || 0, startTime: tc.startTime || log.startedAt, @@ -205,15 +205,15 @@ export function buildTraceSpans(result: ExecutionResult): { // Wrap extraction in try-catch to handle unexpected toolCalls formats try { - if (log.output?.response?.toolCalls?.list) { + if (log.output?.toolCalls?.list) { // Standard format with list property - toolCallsList = log.output.response.toolCalls.list - } else if (Array.isArray(log.output?.response?.toolCalls)) { + toolCallsList = log.output.toolCalls.list + } else if (Array.isArray(log.output?.toolCalls)) { // Direct array format - toolCallsList = log.output.response.toolCalls - } else if (log.output?.executionData?.output?.response?.toolCalls) { + toolCallsList = log.output.toolCalls + } else if (log.output?.executionData?.output?.toolCalls) { // Streaming format with executionData - const tcObj = log.output.executionData.output.response.toolCalls + const tcObj = log.output.executionData.output.toolCalls toolCallsList = Array.isArray(tcObj) ? tcObj : tcObj.list || [] } diff --git a/apps/sim/lib/workflows/db-helpers.test.ts b/apps/sim/lib/workflows/db-helpers.test.ts index be9d23734..66035a06b 100644 --- a/apps/sim/lib/workflows/db-helpers.test.ts +++ b/apps/sim/lib/workflows/db-helpers.test.ts @@ -92,7 +92,7 @@ const mockBlocksFromDb = [ isWide: false, height: 150, subBlocks: { input: { id: 'input', type: 'short-input', value: 'test' } }, - outputs: { response: { type: 'string' } }, + outputs: { result: { type: 'string' } }, data: { parentId: null, extent: null, width: 350 }, parentId: null, extent: null, @@ -159,7 +159,7 @@ const mockWorkflowState: WorkflowState = { name: 'Start Block', position: { x: 100, y: 100 }, subBlocks: { input: { id: 'input', type: 'short-input', value: 'test' } }, - outputs: { response: { type: 'string' } }, + outputs: { result: { type: 'string' } }, enabled: true, horizontalHandles: true, isWide: false, @@ -268,7 +268,7 @@ describe('Database Helpers', () => { isWide: false, height: 150, subBlocks: { input: { id: 'input', type: 'short-input', value: 'test' } }, - outputs: { response: { type: 'string' } }, + outputs: { result: { type: 'string' } }, data: { parentId: null, extent: null, width: 350 }, parentId: null, extent: null, diff --git a/apps/sim/package.json b/apps/sim/package.json index 934a79e78..9e784b184 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -85,6 +85,7 @@ "groq-sdk": "^0.15.0", "input-otp": "^1.4.2", "ioredis": "^5.6.0", + "jose": "6.0.11", "jwt-decode": "^4.0.0", "lenis": "^1.2.3", "lucide-react": "^0.479.0", diff --git a/apps/sim/providers/anthropic/index.ts b/apps/sim/providers/anthropic/index.ts index 59e9c19e6..41476db91 100644 --- a/apps/sim/providers/anthropic/index.ts +++ b/apps/sim/providers/anthropic/index.ts @@ -289,31 +289,29 @@ ${fieldDescriptions} execution: { success: true, output: { - response: { - content: '', // Will be filled by streaming content in chat component - model: request.model, - tokens: tokenUsage, - toolCalls: undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - timeSegments: [ - { - type: 'model', - name: 'Streaming response', - startTime: providerStartTime, - endTime: Date.now(), - duration: Date.now() - providerStartTime, - }, - ], - }, - // Estimate token cost based on typical Claude pricing - cost: { - total: 0.0, - input: 0.0, - output: 0.0, - }, + content: '', // Will be filled by streaming content in chat component + model: request.model, + tokens: tokenUsage, + toolCalls: undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + timeSegments: [ + { + type: 'model', + name: 'Streaming response', + startTime: providerStartTime, + endTime: Date.now(), + duration: Date.now() - providerStartTime, + }, + ], + }, + // Estimate token cost based on typical Claude pricing + cost: { + total: 0.0, + input: 0.0, + output: 0.0, }, }, logs: [], // No block logs for direct streaming @@ -641,36 +639,34 @@ ${fieldDescriptions} execution: { success: true, output: { - response: { - content: '', // Will be filled by the callback - model: request.model || 'claude-3-7-sonnet-20250219', - tokens: { - prompt: tokens.prompt, - completion: tokens.completion, - total: tokens.total, - }, - toolCalls: - toolCalls.length > 0 - ? { - list: toolCalls, - count: toolCalls.length, - } - : undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - modelTime: modelTime, - toolsTime: toolsTime, - firstResponseTime: firstResponseTime, - iterations: iterationCount + 1, - timeSegments: timeSegments, - }, - cost: { - total: (tokens.total || 0) * 0.0001, // Estimate cost based on tokens - input: (tokens.prompt || 0) * 0.0001, - output: (tokens.completion || 0) * 0.0001, - }, + content: '', // Will be filled by the callback + model: request.model || 'claude-3-7-sonnet-20250219', + tokens: { + prompt: tokens.prompt, + completion: tokens.completion, + total: tokens.total, + }, + toolCalls: + toolCalls.length > 0 + ? { + list: toolCalls, + count: toolCalls.length, + } + : undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + modelTime: modelTime, + toolsTime: toolsTime, + firstResponseTime: firstResponseTime, + iterations: iterationCount + 1, + timeSegments: timeSegments, + }, + cost: { + total: (tokens.total || 0) * 0.0001, // Estimate cost based on tokens + input: (tokens.prompt || 0) * 0.0001, + output: (tokens.completion || 0) * 0.0001, }, }, logs: [], // No block logs at provider level diff --git a/apps/sim/providers/azure-openai/index.ts b/apps/sim/providers/azure-openai/index.ts index 1c7dfa660..91639241d 100644 --- a/apps/sim/providers/azure-openai/index.ts +++ b/apps/sim/providers/azure-openai/index.ts @@ -212,22 +212,22 @@ export const azureOpenAIProvider: ProviderConfig = { stream: createReadableStreamFromAzureOpenAIStream(streamResponse, (content, usage) => { // Update the execution data with the final content and token usage _streamContent = content - streamingResult.execution.output.response.content = content + streamingResult.execution.output.content = content // Update the timing information with the actual completion time const streamEndTime = Date.now() const streamEndTimeISO = new Date(streamEndTime).toISOString() - if (streamingResult.execution.output.response.providerTiming) { - streamingResult.execution.output.response.providerTiming.endTime = streamEndTimeISO - streamingResult.execution.output.response.providerTiming.duration = + if (streamingResult.execution.output.providerTiming) { + streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO + streamingResult.execution.output.providerTiming.duration = streamEndTime - providerStartTime // Update the time segment as well - if (streamingResult.execution.output.response.providerTiming.timeSegments?.[0]) { - streamingResult.execution.output.response.providerTiming.timeSegments[0].endTime = + if (streamingResult.execution.output.providerTiming.timeSegments?.[0]) { + streamingResult.execution.output.providerTiming.timeSegments[0].endTime = streamEndTime - streamingResult.execution.output.response.providerTiming.timeSegments[0].duration = + streamingResult.execution.output.providerTiming.timeSegments[0].duration = streamEndTime - providerStartTime } } @@ -240,34 +240,32 @@ export const azureOpenAIProvider: ProviderConfig = { total: usage.total_tokens || tokenUsage.total, } - streamingResult.execution.output.response.tokens = newTokens + streamingResult.execution.output.tokens = newTokens } // We don't need to estimate tokens here as execution-logger.ts will handle that }), execution: { success: true, output: { - response: { - content: '', // Will be filled by the stream completion callback - model: request.model, - tokens: tokenUsage, - toolCalls: undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - timeSegments: [ - { - type: 'model', - name: 'Streaming response', - startTime: providerStartTime, - endTime: Date.now(), - duration: Date.now() - providerStartTime, - }, - ], - }, - // Cost will be calculated in execution-logger.ts + content: '', // Will be filled by the stream completion callback + model: request.model, + tokens: tokenUsage, + toolCalls: undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + timeSegments: [ + { + type: 'model', + name: 'Streaming response', + startTime: providerStartTime, + endTime: Date.now(), + duration: Date.now() - providerStartTime, + }, + ], }, + // Cost will be calculated in execution-logger.ts }, logs: [], // No block logs for direct streaming metadata: { @@ -527,7 +525,7 @@ export const azureOpenAIProvider: ProviderConfig = { stream: createReadableStreamFromAzureOpenAIStream(streamResponse, (content, usage) => { // Update the execution data with the final content and token usage _streamContent = content - streamingResult.execution.output.response.content = content + streamingResult.execution.output.content = content // Update token usage if available from the stream if (usage) { @@ -537,39 +535,37 @@ export const azureOpenAIProvider: ProviderConfig = { total: usage.total_tokens || tokens.total, } - streamingResult.execution.output.response.tokens = newTokens + streamingResult.execution.output.tokens = newTokens } }), execution: { success: true, output: { - response: { - content: '', // Will be filled by the callback - model: request.model, - tokens: { - prompt: tokens.prompt, - completion: tokens.completion, - total: tokens.total, - }, - toolCalls: - toolCalls.length > 0 - ? { - list: toolCalls, - count: toolCalls.length, - } - : undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - modelTime: modelTime, - toolsTime: toolsTime, - firstResponseTime: firstResponseTime, - iterations: iterationCount + 1, - timeSegments: timeSegments, - }, - // Cost will be calculated in execution-logger.ts + content: '', // Will be filled by the callback + model: request.model, + tokens: { + prompt: tokens.prompt, + completion: tokens.completion, + total: tokens.total, }, + toolCalls: + toolCalls.length > 0 + ? { + list: toolCalls, + count: toolCalls.length, + } + : undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + modelTime: modelTime, + toolsTime: toolsTime, + firstResponseTime: firstResponseTime, + iterations: iterationCount + 1, + timeSegments: timeSegments, + }, + // Cost will be calculated in execution-logger.ts }, logs: [], // No block logs at provider level metadata: { diff --git a/apps/sim/providers/cerebras/index.ts b/apps/sim/providers/cerebras/index.ts index d1cd664ee..5fdca4c74 100644 --- a/apps/sim/providers/cerebras/index.ts +++ b/apps/sim/providers/cerebras/index.ts @@ -157,31 +157,29 @@ export const cerebrasProvider: ProviderConfig = { execution: { success: true, output: { - response: { - content: '', // Will be filled by streaming content in chat component - model: request.model || 'cerebras/llama-3.3-70b', - tokens: tokenUsage, - toolCalls: undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - timeSegments: [ - { - type: 'model', - name: 'Streaming response', - startTime: providerStartTime, - endTime: Date.now(), - duration: Date.now() - providerStartTime, - }, - ], - }, - // Estimate token cost - cost: { - total: 0.0, - input: 0.0, - output: 0.0, - }, + content: '', // Will be filled by streaming content in chat component + model: request.model || 'cerebras/llama-3.3-70b', + tokens: tokenUsage, + toolCalls: undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + timeSegments: [ + { + type: 'model', + name: 'Streaming response', + startTime: providerStartTime, + endTime: Date.now(), + duration: Date.now() - providerStartTime, + }, + ], + }, + // Estimate token cost + cost: { + total: 0.0, + input: 0.0, + output: 0.0, }, }, logs: [], // No block logs for direct streaming @@ -461,36 +459,34 @@ export const cerebrasProvider: ProviderConfig = { execution: { success: true, output: { - response: { - content: '', // Will be filled by the callback - model: request.model || 'cerebras/llama-3.3-70b', - tokens: { - prompt: tokens.prompt, - completion: tokens.completion, - total: tokens.total, - }, - toolCalls: - toolCalls.length > 0 - ? { - list: toolCalls, - count: toolCalls.length, - } - : undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - modelTime: modelTime, - toolsTime: toolsTime, - firstResponseTime: firstResponseTime, - iterations: iterationCount + 1, - timeSegments: timeSegments, - }, - cost: { - total: (tokens.total || 0) * 0.0001, - input: (tokens.prompt || 0) * 0.0001, - output: (tokens.completion || 0) * 0.0001, - }, + content: '', // Will be filled by the callback + model: request.model || 'cerebras/llama-3.3-70b', + tokens: { + prompt: tokens.prompt, + completion: tokens.completion, + total: tokens.total, + }, + toolCalls: + toolCalls.length > 0 + ? { + list: toolCalls, + count: toolCalls.length, + } + : undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + modelTime: modelTime, + toolsTime: toolsTime, + firstResponseTime: firstResponseTime, + iterations: iterationCount + 1, + timeSegments: timeSegments, + }, + cost: { + total: (tokens.total || 0) * 0.0001, + input: (tokens.prompt || 0) * 0.0001, + output: (tokens.completion || 0) * 0.0001, }, }, logs: [], // No block logs at provider level diff --git a/apps/sim/providers/deepseek/index.ts b/apps/sim/providers/deepseek/index.ts index 6b18551ba..d91b7008d 100644 --- a/apps/sim/providers/deepseek/index.ts +++ b/apps/sim/providers/deepseek/index.ts @@ -151,31 +151,29 @@ export const deepseekProvider: ProviderConfig = { execution: { success: true, output: { - response: { - content: '', // Will be filled by streaming content in chat component - model: request.model || 'deepseek-chat', - tokens: tokenUsage, - toolCalls: undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - timeSegments: [ - { - type: 'model', - name: 'Streaming response', - startTime: providerStartTime, - endTime: Date.now(), - duration: Date.now() - providerStartTime, - }, - ], - }, - // Estimate token cost - cost: { - total: 0.0, - input: 0.0, - output: 0.0, - }, + content: '', // Will be filled by streaming content in chat component + model: request.model || 'deepseek-chat', + tokens: tokenUsage, + toolCalls: undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + timeSegments: [ + { + type: 'model', + name: 'Streaming response', + startTime: providerStartTime, + endTime: Date.now(), + duration: Date.now() - providerStartTime, + }, + ], + }, + // Estimate token cost + cost: { + total: 0.0, + input: 0.0, + output: 0.0, }, }, logs: [], // No block logs for direct streaming @@ -461,36 +459,34 @@ export const deepseekProvider: ProviderConfig = { execution: { success: true, output: { - response: { - content: '', // Will be filled by the callback - model: request.model || 'deepseek-chat', - tokens: { - prompt: tokens.prompt, - completion: tokens.completion, - total: tokens.total, - }, - toolCalls: - toolCalls.length > 0 - ? { - list: toolCalls, - count: toolCalls.length, - } - : undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - modelTime: modelTime, - toolsTime: toolsTime, - firstResponseTime: firstResponseTime, - iterations: iterationCount + 1, - timeSegments: timeSegments, - }, - cost: { - total: (tokens.total || 0) * 0.0001, - input: (tokens.prompt || 0) * 0.0001, - output: (tokens.completion || 0) * 0.0001, - }, + content: '', // Will be filled by the callback + model: request.model || 'deepseek-chat', + tokens: { + prompt: tokens.prompt, + completion: tokens.completion, + total: tokens.total, + }, + toolCalls: + toolCalls.length > 0 + ? { + list: toolCalls, + count: toolCalls.length, + } + : undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + modelTime: modelTime, + toolsTime: toolsTime, + firstResponseTime: firstResponseTime, + iterations: iterationCount + 1, + timeSegments: timeSegments, + }, + cost: { + total: (tokens.total || 0) * 0.0001, + input: (tokens.prompt || 0) * 0.0001, + output: (tokens.completion || 0) * 0.0001, }, }, logs: [], // No block logs at provider level diff --git a/apps/sim/providers/google/index.ts b/apps/sim/providers/google/index.ts index 594799e33..f2413d27a 100644 --- a/apps/sim/providers/google/index.ts +++ b/apps/sim/providers/google/index.ts @@ -215,36 +215,34 @@ export const googleProvider: ProviderConfig = { execution: { success: true, output: { - response: { - content: '', - model: request.model, - tokens: { - prompt: 0, - completion: 0, - total: 0, - }, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: firstResponseTime, - modelTime: firstResponseTime, - toolsTime: 0, - firstResponseTime, - iterations: 1, - timeSegments: [ - { - type: 'model', - name: 'Initial streaming response', - startTime: initialCallTime, - endTime: initialCallTime + firstResponseTime, - duration: firstResponseTime, - }, - ], - cost: { - total: 0.0, // Initial estimate, updated as tokens are processed - input: 0.0, - output: 0.0, + content: '', + model: request.model, + tokens: { + prompt: 0, + completion: 0, + total: 0, + }, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: firstResponseTime, + modelTime: firstResponseTime, + toolsTime: 0, + firstResponseTime, + iterations: 1, + timeSegments: [ + { + type: 'model', + name: 'Initial streaming response', + startTime: initialCallTime, + endTime: initialCallTime + firstResponseTime, + duration: firstResponseTime, }, + ], + cost: { + total: 0.0, // Initial estimate, updated as tokens are processed + input: 0.0, + output: 0.0, }, }, }, @@ -527,33 +525,31 @@ export const googleProvider: ProviderConfig = { execution: { success: true, output: { - response: { - content: '', - model: request.model, - tokens, - toolCalls: - toolCalls.length > 0 - ? { - list: toolCalls, - count: toolCalls.length, - } - : undefined, - toolResults, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - modelTime, - toolsTime, - firstResponseTime, - iterations: iterationCount + 1, - timeSegments, - }, - cost: { - total: (tokens.total || 0) * 0.0001, // Estimate cost based on tokens - input: (tokens.prompt || 0) * 0.0001, - output: (tokens.completion || 0) * 0.0001, - }, + content: '', + model: request.model, + tokens, + toolCalls: + toolCalls.length > 0 + ? { + list: toolCalls, + count: toolCalls.length, + } + : undefined, + toolResults, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + modelTime, + toolsTime, + firstResponseTime, + iterations: iterationCount + 1, + timeSegments, + }, + cost: { + total: (tokens.total || 0) * 0.0001, // Estimate cost based on tokens + input: (tokens.prompt || 0) * 0.0001, + output: (tokens.completion || 0) * 0.0001, }, }, logs: [], diff --git a/apps/sim/providers/groq/index.ts b/apps/sim/providers/groq/index.ts index b67f7fdc3..d928924ea 100644 --- a/apps/sim/providers/groq/index.ts +++ b/apps/sim/providers/groq/index.ts @@ -153,30 +153,28 @@ export const groqProvider: ProviderConfig = { execution: { success: true, output: { - response: { - content: '', // Will be filled by streaming content in chat component - model: request.model || 'groq/meta-llama/llama-4-scout-17b-16e-instruct', - tokens: tokenUsage, - toolCalls: undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - timeSegments: [ - { - type: 'model', - name: 'Streaming response', - startTime: providerStartTime, - endTime: Date.now(), - duration: Date.now() - providerStartTime, - }, - ], - }, - cost: { - total: 0.0, - input: 0.0, - output: 0.0, - }, + content: '', // Will be filled by streaming content in chat component + model: request.model || 'groq/meta-llama/llama-4-scout-17b-16e-instruct', + tokens: tokenUsage, + toolCalls: undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + timeSegments: [ + { + type: 'model', + name: 'Streaming response', + startTime: providerStartTime, + endTime: Date.now(), + duration: Date.now() - providerStartTime, + }, + ], + }, + cost: { + total: 0.0, + input: 0.0, + output: 0.0, }, }, logs: [], // No block logs for direct streaming @@ -380,36 +378,34 @@ export const groqProvider: ProviderConfig = { execution: { success: true, output: { - response: { - content: '', // Will be filled by the callback - model: request.model || 'groq/meta-llama/llama-4-scout-17b-16e-instruct', - tokens: { - prompt: tokens.prompt, - completion: tokens.completion, - total: tokens.total, - }, - toolCalls: - toolCalls.length > 0 - ? { - list: toolCalls, - count: toolCalls.length, - } - : undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - modelTime: modelTime, - toolsTime: toolsTime, - firstResponseTime: firstResponseTime, - iterations: iterationCount + 1, - timeSegments: timeSegments, - }, - cost: { - total: (tokens.total || 0) * 0.0001, - input: (tokens.prompt || 0) * 0.0001, - output: (tokens.completion || 0) * 0.0001, - }, + content: '', // Will be filled by the callback + model: request.model || 'groq/meta-llama/llama-4-scout-17b-16e-instruct', + tokens: { + prompt: tokens.prompt, + completion: tokens.completion, + total: tokens.total, + }, + toolCalls: + toolCalls.length > 0 + ? { + list: toolCalls, + count: toolCalls.length, + } + : undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + modelTime: modelTime, + toolsTime: toolsTime, + firstResponseTime: firstResponseTime, + iterations: iterationCount + 1, + timeSegments: timeSegments, + }, + cost: { + total: (tokens.total || 0) * 0.0001, + input: (tokens.prompt || 0) * 0.0001, + output: (tokens.completion || 0) * 0.0001, }, }, logs: [], // No block logs at provider level diff --git a/apps/sim/providers/ollama/index.ts b/apps/sim/providers/ollama/index.ts index e71c1de42..845830786 100644 --- a/apps/sim/providers/ollama/index.ts +++ b/apps/sim/providers/ollama/index.ts @@ -53,7 +53,6 @@ export const ollamaProvider: ProviderConfig = { }) const startTime = Date.now() - const _timeSegments: TimeSegment[] = [] try { // Prepare messages array @@ -126,9 +125,6 @@ export const ollamaProvider: ProviderConfig = { } } - // Track the original tool_choice for forced tool tracking - const _originalToolChoice = payload.tool_choice - let currentResponse = await ollama.chat.completions.create(payload) const firstResponseTime = Date.now() - startTime diff --git a/apps/sim/providers/openai/index.ts b/apps/sim/providers/openai/index.ts index 9c6624574..43452c8b7 100644 --- a/apps/sim/providers/openai/index.ts +++ b/apps/sim/providers/openai/index.ts @@ -194,22 +194,22 @@ export const openaiProvider: ProviderConfig = { stream: createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => { // Update the execution data with the final content and token usage _streamContent = content - streamingResult.execution.output.response.content = content + streamingResult.execution.output.content = content // Update the timing information with the actual completion time const streamEndTime = Date.now() const streamEndTimeISO = new Date(streamEndTime).toISOString() - if (streamingResult.execution.output.response.providerTiming) { - streamingResult.execution.output.response.providerTiming.endTime = streamEndTimeISO - streamingResult.execution.output.response.providerTiming.duration = + if (streamingResult.execution.output.providerTiming) { + streamingResult.execution.output.providerTiming.endTime = streamEndTimeISO + streamingResult.execution.output.providerTiming.duration = streamEndTime - providerStartTime // Update the time segment as well - if (streamingResult.execution.output.response.providerTiming.timeSegments?.[0]) { - streamingResult.execution.output.response.providerTiming.timeSegments[0].endTime = + if (streamingResult.execution.output.providerTiming.timeSegments?.[0]) { + streamingResult.execution.output.providerTiming.timeSegments[0].endTime = streamEndTime - streamingResult.execution.output.response.providerTiming.timeSegments[0].duration = + streamingResult.execution.output.providerTiming.timeSegments[0].duration = streamEndTime - providerStartTime } } @@ -222,34 +222,32 @@ export const openaiProvider: ProviderConfig = { total: usage.total_tokens || tokenUsage.total, } - streamingResult.execution.output.response.tokens = newTokens + streamingResult.execution.output.tokens = newTokens } // We don't need to estimate tokens here as execution-logger.ts will handle that }), execution: { success: true, output: { - response: { - content: '', // Will be filled by the stream completion callback - model: request.model, - tokens: tokenUsage, - toolCalls: undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - timeSegments: [ - { - type: 'model', - name: 'Streaming response', - startTime: providerStartTime, - endTime: Date.now(), - duration: Date.now() - providerStartTime, - }, - ], - }, - // Cost will be calculated in execution-logger.ts + content: '', // Will be filled by the stream completion callback + model: request.model, + tokens: tokenUsage, + toolCalls: undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + timeSegments: [ + { + type: 'model', + name: 'Streaming response', + startTime: providerStartTime, + endTime: Date.now(), + duration: Date.now() - providerStartTime, + }, + ], }, + // Cost will be calculated in execution-logger.ts }, logs: [], // No block logs for direct streaming metadata: { @@ -509,7 +507,7 @@ export const openaiProvider: ProviderConfig = { stream: createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => { // Update the execution data with the final content and token usage _streamContent = content - streamingResult.execution.output.response.content = content + streamingResult.execution.output.content = content // Update token usage if available from the stream if (usage) { @@ -519,39 +517,37 @@ export const openaiProvider: ProviderConfig = { total: usage.total_tokens || tokens.total, } - streamingResult.execution.output.response.tokens = newTokens + streamingResult.execution.output.tokens = newTokens } }), execution: { success: true, output: { - response: { - content: '', // Will be filled by the callback - model: request.model, - tokens: { - prompt: tokens.prompt, - completion: tokens.completion, - total: tokens.total, - }, - toolCalls: - toolCalls.length > 0 - ? { - list: toolCalls, - count: toolCalls.length, - } - : undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - modelTime: modelTime, - toolsTime: toolsTime, - firstResponseTime: firstResponseTime, - iterations: iterationCount + 1, - timeSegments: timeSegments, - }, - // Cost will be calculated in execution-logger.ts + content: '', // Will be filled by the callback + model: request.model, + tokens: { + prompt: tokens.prompt, + completion: tokens.completion, + total: tokens.total, }, + toolCalls: + toolCalls.length > 0 + ? { + list: toolCalls, + count: toolCalls.length, + } + : undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + modelTime: modelTime, + toolsTime: toolsTime, + firstResponseTime: firstResponseTime, + iterations: iterationCount + 1, + timeSegments: timeSegments, + }, + // Cost will be calculated in execution-logger.ts }, logs: [], // No block logs at provider level metadata: { diff --git a/apps/sim/providers/xai/index.ts b/apps/sim/providers/xai/index.ts index a4f2f9be9..957d49574 100644 --- a/apps/sim/providers/xai/index.ts +++ b/apps/sim/providers/xai/index.ts @@ -169,31 +169,29 @@ export const xAIProvider: ProviderConfig = { execution: { success: true, output: { - response: { - content: '', // Will be filled by streaming content in chat component - model: request.model || 'grok-3-latest', - tokens: tokenUsage, - toolCalls: undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - timeSegments: [ - { - type: 'model', - name: 'Streaming response', - startTime: providerStartTime, - endTime: Date.now(), - duration: Date.now() - providerStartTime, - }, - ], - }, - // Estimate token cost - cost: { - total: 0.0, - input: 0.0, - output: 0.0, - }, + content: '', // Will be filled by streaming content in chat component + model: request.model || 'grok-3-latest', + tokens: tokenUsage, + toolCalls: undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + timeSegments: [ + { + type: 'model', + name: 'Streaming response', + startTime: providerStartTime, + endTime: Date.now(), + duration: Date.now() - providerStartTime, + }, + ], + }, + // Estimate token cost + cost: { + total: 0.0, + input: 0.0, + output: 0.0, }, }, logs: [], // No block logs for direct streaming @@ -514,36 +512,34 @@ export const xAIProvider: ProviderConfig = { execution: { success: true, output: { - response: { - content: '', // Will be filled by the callback - model: request.model || 'grok-3-latest', - tokens: { - prompt: tokens.prompt, - completion: tokens.completion, - total: tokens.total, - }, - toolCalls: - toolCalls.length > 0 - ? { - list: toolCalls, - count: toolCalls.length, - } - : undefined, - providerTiming: { - startTime: providerStartTimeISO, - endTime: new Date().toISOString(), - duration: Date.now() - providerStartTime, - modelTime: modelTime, - toolsTime: toolsTime, - firstResponseTime: firstResponseTime, - iterations: iterationCount + 1, - timeSegments: timeSegments, - }, - cost: { - total: (tokens.total || 0) * 0.0001, - input: (tokens.prompt || 0) * 0.0001, - output: (tokens.completion || 0) * 0.0001, - }, + content: '', // Will be filled by the callback + model: request.model || 'grok-3-latest', + tokens: { + prompt: tokens.prompt, + completion: tokens.completion, + total: tokens.total, + }, + toolCalls: + toolCalls.length > 0 + ? { + list: toolCalls, + count: toolCalls.length, + } + : undefined, + providerTiming: { + startTime: providerStartTimeISO, + endTime: new Date().toISOString(), + duration: Date.now() - providerStartTime, + modelTime: modelTime, + toolsTime: toolsTime, + firstResponseTime: firstResponseTime, + iterations: iterationCount + 1, + timeSegments: timeSegments, + }, + cost: { + total: (tokens.total || 0) * 0.0001, + input: (tokens.prompt || 0) * 0.0001, + output: (tokens.completion || 0) * 0.0001, }, }, logs: [], // No block logs at provider level diff --git a/apps/sim/stores/execution/store.ts b/apps/sim/stores/execution/store.ts index ab09c84fa..5261bacba 100644 --- a/apps/sim/stores/execution/store.ts +++ b/apps/sim/stores/execution/store.ts @@ -1,14 +1,65 @@ import { create } from 'zustand' -import { type ExecutionActions, type ExecutionState, initialState } from './types' +import { useGeneralStore } from '@/stores/settings/general/store' +import { + type ExecutionActions, + type ExecutionState, + initialState, + type PanToBlockCallback, + type SetPanToBlockCallback, +} from './types' -export const useExecutionStore = create()((set) => ({ +// Global callback for panning to active blocks +let panToBlockCallback: PanToBlockCallback | null = null + +export const setPanToBlockCallback: SetPanToBlockCallback = (callback) => { + panToBlockCallback = callback +} + +export const useExecutionStore = create()((set, get) => ({ ...initialState, - setActiveBlocks: (blockIds) => set({ activeBlockIds: new Set(blockIds) }), - setIsExecuting: (isExecuting) => set({ isExecuting }), + setActiveBlocks: (blockIds) => { + set({ activeBlockIds: new Set(blockIds) }) + + // Pan to the first active block if auto-pan is enabled and we have a callback and blocks are active + const { autoPanDisabled } = get() + const isAutoPanEnabled = useGeneralStore.getState().isAutoPanEnabled + + if (panToBlockCallback && !autoPanDisabled && isAutoPanEnabled && blockIds.size > 0) { + const firstActiveBlockId = Array.from(blockIds)[0] + panToBlockCallback(firstActiveBlockId) + } + }, + + setPendingBlocks: (pendingBlocks) => { + set({ pendingBlocks }) + + // Pan to the first pending block if auto-pan is enabled, we have a callback, blocks are pending, and we're in debug mode + const { isDebugging, autoPanDisabled } = get() + const isAutoPanEnabled = useGeneralStore.getState().isAutoPanEnabled + + if ( + panToBlockCallback && + !autoPanDisabled && + isAutoPanEnabled && + pendingBlocks.length > 0 && + isDebugging + ) { + const firstPendingBlockId = pendingBlocks[0] + panToBlockCallback(firstPendingBlockId) + } + }, + + setIsExecuting: (isExecuting) => { + set({ isExecuting }) + // Reset auto-pan disabled state when starting execution + if (isExecuting) { + set({ autoPanDisabled: false }) + } + }, setIsDebugging: (isDebugging) => set({ isDebugging }), - setPendingBlocks: (pendingBlocks) => set({ pendingBlocks }), setExecutor: (executor) => set({ executor }), setDebugContext: (debugContext) => set({ debugContext }), + setAutoPanDisabled: (disabled) => set({ autoPanDisabled: disabled }), reset: () => set(initialState), })) diff --git a/apps/sim/stores/execution/types.ts b/apps/sim/stores/execution/types.ts index 6e387678e..48578d445 100644 --- a/apps/sim/stores/execution/types.ts +++ b/apps/sim/stores/execution/types.ts @@ -8,6 +8,7 @@ export interface ExecutionState { pendingBlocks: string[] executor: Executor | null debugContext: ExecutionContext | null + autoPanDisabled: boolean } export interface ExecutionActions { @@ -17,6 +18,7 @@ export interface ExecutionActions { setPendingBlocks: (blockIds: string[]) => void setExecutor: (executor: Executor | null) => void setDebugContext: (context: ExecutionContext | null) => void + setAutoPanDisabled: (disabled: boolean) => void reset: () => void } @@ -27,4 +29,9 @@ export const initialState: ExecutionState = { pendingBlocks: [], executor: null, debugContext: null, + autoPanDisabled: false, } + +// Types for panning functionality +export type PanToBlockCallback = (blockId: string) => void +export type SetPanToBlockCallback = (callback: PanToBlockCallback | null) => void diff --git a/apps/sim/stores/index.ts b/apps/sim/stores/index.ts index 502d71a7a..ec368e0e3 100644 --- a/apps/sim/stores/index.ts +++ b/apps/sim/stores/index.ts @@ -250,5 +250,3 @@ export const logAllStores = () => { return state } - -// Removed sync managers - Socket.IO handles real-time sync diff --git a/apps/sim/stores/panel/console/store.test.ts b/apps/sim/stores/panel/console/store.test.ts index ebdbe636f..975ac8593 100644 --- a/apps/sim/stores/panel/console/store.test.ts +++ b/apps/sim/stores/panel/console/store.test.ts @@ -29,7 +29,7 @@ describe('Console Store', () => { blockName: 'Test Block', blockType: 'agent', success: true, - output: { response: { content: 'Test output' } }, + output: { content: 'Test output' }, durationMs: 100, startedAt: '2023-01-01T00:00:00.000Z', endedAt: '2023-01-01T00:00:01.000Z', @@ -78,7 +78,7 @@ describe('Console Store', () => { blockName: 'Test Block', blockType: 'agent', success: true, - output: { response: { content: 'Initial content' } }, + output: { content: 'Initial content' }, durationMs: 100, startedAt: '2023-01-01T00:00:00.000Z', endedAt: '2023-01-01T00:00:01.000Z', @@ -92,7 +92,7 @@ describe('Console Store', () => { const state = useConsoleStore.getState() expect(state.entries).toHaveLength(1) - expect(state.entries[0].output?.response?.content).toBe('Updated content') + expect(state.entries[0].output?.content).toBe('Updated content') }) it('should update console entry with object update', () => { @@ -111,7 +111,7 @@ describe('Console Store', () => { const state = useConsoleStore.getState() const entry = state.entries[0] - expect(entry.output?.response?.content).toBe('New content') + expect(entry.output?.content).toBe('New content') expect(entry.success).toBe(false) expect(entry.error).toBe('Update error') expect(entry.durationMs).toBe(200) @@ -123,10 +123,8 @@ describe('Console Store', () => { const update: ConsoleUpdate = { output: { - response: { - content: 'Direct output update', - status: 200, - }, + content: 'Direct output update', + status: 200, }, } @@ -135,8 +133,8 @@ describe('Console Store', () => { const state = useConsoleStore.getState() const entry = state.entries[0] - expect(entry.output?.response?.content).toBe('Direct output update') - expect(entry.output?.response?.status).toBe(200) + expect(entry.output?.content).toBe('Direct output update') + expect(entry.output?.status).toBe(200) }) it('should not update non-matching block IDs', () => { @@ -145,7 +143,7 @@ describe('Console Store', () => { store.updateConsole('non-existent-block', 'Should not update') const newState = useConsoleStore.getState() - expect(newState.entries[0].output?.response?.content).toBe('Initial content') + expect(newState.entries[0].output?.content).toBe('Initial content') }) it('should handle partial updates correctly', () => { @@ -156,14 +154,14 @@ describe('Console Store', () => { let state = useConsoleStore.getState() expect(state.entries[0].success).toBe(false) - expect(state.entries[0].output?.response?.content).toBe('Initial content') // Should remain unchanged + expect(state.entries[0].output?.content).toBe('Initial content') // Should remain unchanged // Then update only content store.updateConsole('block-123', { content: 'Partial update' }) state = useConsoleStore.getState() expect(state.entries[0].success).toBe(false) // Should remain false - expect(state.entries[0].output?.response?.content).toBe('Partial update') + expect(state.entries[0].output?.content).toBe('Partial update') }) }) @@ -178,7 +176,7 @@ describe('Console Store', () => { blockName: 'Block 1', blockType: 'agent', success: true, - output: { response: {} }, + output: {}, startedAt: '2023-01-01T00:00:00.000Z', endedAt: '2023-01-01T00:00:01.000Z', }) @@ -189,7 +187,7 @@ describe('Console Store', () => { blockName: 'Block 2', blockType: 'api', success: true, - output: { response: {} }, + output: {}, startedAt: '2023-01-01T00:00:00.000Z', endedAt: '2023-01-01T00:00:01.000Z', }) @@ -230,7 +228,7 @@ describe('Console Store', () => { blockName: 'Block 1', blockType: 'agent', success: true, - output: { response: {} }, + output: {}, startedAt: '2023-01-01T00:00:00.000Z', endedAt: '2023-01-01T00:00:01.000Z', }) @@ -241,7 +239,7 @@ describe('Console Store', () => { blockName: 'Block 2', blockType: 'api', success: true, - output: { response: {} }, + output: {}, startedAt: '2023-01-01T00:00:00.000Z', endedAt: '2023-01-01T00:00:01.000Z', }) @@ -252,7 +250,7 @@ describe('Console Store', () => { blockName: 'Block 3', blockType: 'function', success: false, - output: { response: {} }, + output: {}, error: 'Test error', startedAt: '2023-01-01T00:00:00.000Z', endedAt: '2023-01-01T00:00:01.000Z', diff --git a/apps/sim/stores/panel/console/store.ts b/apps/sim/stores/panel/console/store.ts index b315e0a96..396666823 100644 --- a/apps/sim/stores/panel/console/store.ts +++ b/apps/sim/stores/panel/console/store.ts @@ -14,15 +14,11 @@ const updateBlockOutput = ( existingOutput: NormalizedBlockOutput | undefined, contentUpdate: string ): NormalizedBlockOutput => { - const defaultOutput: NormalizedBlockOutput = { response: {} } - const baseOutput = existingOutput || defaultOutput + const baseOutput = existingOutput || {} return { ...baseOutput, - response: { - ...baseOutput.response, - content: contentUpdate, - }, + content: contentUpdate, } } @@ -198,14 +194,10 @@ export const useConsoleStore = create()( } if (update.output !== undefined) { - const existingOutput = entry.output || { response: {} } + const existingOutput = entry.output || {} updatedEntry.output = { ...existingOutput, ...update.output, - response: { - ...(existingOutput.response || {}), - ...(update.output.response || {}), - }, } } diff --git a/apps/sim/stores/settings/general/store.ts b/apps/sim/stores/settings/general/store.ts index 7a526f9c3..573611e81 100644 --- a/apps/sim/stores/settings/general/store.ts +++ b/apps/sim/stores/settings/general/store.ts @@ -19,6 +19,7 @@ export const useGeneralStore = create()( isAutoConnectEnabled: true, isDebugModeEnabled: false, isAutoFillEnvVarsEnabled: true, + isAutoPanEnabled: true, theme: 'system', telemetryEnabled: true, telemetryNotifiedUser: false, @@ -44,6 +45,12 @@ export const useGeneralStore = create()( get().updateSetting('autoFillEnvVars', newValue) }, + toggleAutoPan: () => { + const newValue = !get().isAutoPanEnabled + set({ isAutoPanEnabled: newValue }) + get().updateSetting('autoPan', newValue) + }, + setTheme: (theme) => { set({ theme }) get().updateSetting('theme', theme) @@ -96,6 +103,7 @@ export const useGeneralStore = create()( isAutoConnectEnabled: data.autoConnect, isDebugModeEnabled: data.debugMode, isAutoFillEnvVarsEnabled: data.autoFillEnvVars, + isAutoPanEnabled: data.autoPan ?? true, // Default to true if undefined theme: data.theme, telemetryEnabled: data.telemetryEnabled, telemetryNotifiedUser: data.telemetryNotifiedUser, diff --git a/apps/sim/stores/settings/general/types.ts b/apps/sim/stores/settings/general/types.ts index 665f7eab8..2a6e6e1bb 100644 --- a/apps/sim/stores/settings/general/types.ts +++ b/apps/sim/stores/settings/general/types.ts @@ -2,6 +2,7 @@ export interface General { isAutoConnectEnabled: boolean isDebugModeEnabled: boolean isAutoFillEnvVarsEnabled: boolean + isAutoPanEnabled: boolean theme: 'system' | 'light' | 'dark' telemetryEnabled: boolean telemetryNotifiedUser: boolean @@ -13,6 +14,7 @@ export interface GeneralActions { toggleAutoConnect: () => void toggleDebugMode: () => void toggleAutoFillEnvVars: () => void + toggleAutoPan: () => void setTheme: (theme: 'system' | 'light' | 'dark') => void setTelemetryEnabled: (enabled: boolean) => void setTelemetryNotifiedUser: (notified: boolean) => void @@ -27,6 +29,7 @@ export type UserSettings = { debugMode: boolean autoConnect: boolean autoFillEnvVars: boolean + autoPan: boolean telemetryEnabled: boolean telemetryNotifiedUser: boolean } diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts index fee3d6da3..5fada790b 100644 --- a/apps/sim/stores/workflows/registry/store.ts +++ b/apps/sim/stores/workflows/registry/store.ts @@ -96,8 +96,6 @@ async function fetchWorkflowsFromDB(workspaceId?: string): Promise { apiKey, } = workflow - // No need to filter by workspace since we're already fetching for specific workspace - // Add to registry registryWorkflows[id] = { id, diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 989461514..96e266429 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -143,7 +143,7 @@ export const useWorkflowStore = create()( } }) - const outputs = resolveOutputType(blockConfig.outputs, subBlocks) + const outputs = resolveOutputType(blockConfig.outputs) const newState = { blocks: { diff --git a/apps/sim/stores/workflows/workflow/utils.test.ts b/apps/sim/stores/workflows/workflow/utils.test.ts index faaa1fb70..96ceb4131 100644 --- a/apps/sim/stores/workflows/workflow/utils.test.ts +++ b/apps/sim/stores/workflows/workflow/utils.test.ts @@ -67,7 +67,7 @@ describe('convertLoopBlockToLoop', () => { data: { loopType: 'forEach', count: 5, - collection: '', + collection: '', }, }, } @@ -75,7 +75,7 @@ describe('convertLoopBlockToLoop', () => { const result = convertLoopBlockToLoop('loop1', blocks) expect(result).toBeDefined() - expect(result?.forEachItems).toBe('') + expect(result?.forEachItems).toBe('') }) test('should handle empty collection', () => { diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 4a7df371e..89539c74a 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -15,6 +15,7 @@ export async function executeTool( // Capture start time for precise timing const startTime = new Date() const startTimeISO = startTime.toISOString() + const requestId = crypto.randomUUID().slice(0, 8) try { let tool: ToolConfig | undefined @@ -23,9 +24,15 @@ export async function executeTool( if (toolId.startsWith('custom_')) { const workflowId = params._context?.workflowId tool = await getToolAsync(toolId, workflowId) + if (!tool) { + logger.error(`[${requestId}] Custom tool not found: ${toolId}`) + } } else { // For built-in tools, use the synchronous version tool = getTool(toolId) + if (!tool) { + logger.error(`[${requestId}] Built-in tool not found: ${toolId}`) + } } // Ensure context is preserved if it exists @@ -41,7 +48,6 @@ export async function executeTool( // If we have a credential parameter, fetch the access token if (contextParams.credential) { - logger.info(`[executeTool] Credential found for ${toolId}, fetching access token.`) try { const baseUrl = env.NEXT_PUBLIC_APP_URL if (!baseUrl) { @@ -60,9 +66,6 @@ export async function executeTool( const workflowId = contextParams.workflowId || contextParams._context?.workflowId if (workflowId) { tokenPayload.workflowId = workflowId - logger.info( - `[executeTool] Added workflowId ${workflowId} to token payload for ${toolId}` - ) } } @@ -75,19 +78,23 @@ export async function executeTool( if (!response.ok) { const errorText = await response.text() - logger.error('[executeTool] Token fetch failed:', response.status, errorText) + logger.error(`[${requestId}] Token fetch failed for ${toolId}:`, { + status: response.status, + error: errorText, + }) throw new Error(`Failed to fetch access token: ${response.status} ${errorText}`) } const data = await response.json() contextParams.accessToken = data.accessToken - logger.info(`[executeTool] Successfully fetched access token for ${toolId}`) // Clean up params we don't need to pass to the actual tool contextParams.credential = undefined if (contextParams.workflowId) contextParams.workflowId = undefined - } catch (error) { - logger.error('[executeTool] Error fetching access token:', { error }) + } catch (error: any) { + logger.error(`[${requestId}] Error fetching access token for ${toolId}:`, { + error: error instanceof Error ? error.message : String(error), + }) // Re-throw the error to fail the tool execution if token fetching fails throw new Error( `Failed to obtain credential for tool ${toolId}: ${error instanceof Error ? error.message : String(error)}` @@ -122,7 +129,9 @@ export async function executeTool( }, } } catch (error) { - logger.error(`Error in post-processing for tool ${toolId}:`, { error }) + logger.error(`[${requestId}] Post-processing error for ${toolId}:`, { + error: error instanceof Error ? error.message : String(error), + }) return { ...directResult, timing: { @@ -144,8 +153,10 @@ export async function executeTool( } } // If directExecution returns undefined, fall back to API route - } catch (error) { - logger.warn(`Direct execution failed for tool ${toolId}, falling back to API:`, error) + } catch (error: any) { + logger.warn(`[${requestId}] Direct execution failed for ${toolId}, falling back to API:`, { + error: error instanceof Error ? error.message : String(error), + }) // Fall back to API route if direct execution fails } } @@ -172,7 +183,9 @@ export async function executeTool( }, } } catch (error) { - logger.error(`Error in post-processing for tool ${toolId}:`, { error }) + logger.error(`[${requestId}] Post-processing error for ${toolId}:`, { + error: error instanceof Error ? error.message : String(error), + }) // Return original result if post-processing fails // Still include timing data const endTime = new Date() @@ -204,7 +217,6 @@ export async function executeTool( } // For external APIs, use the proxy - logger.info(`[executeTool] Using handleProxyRequest for toolId=${toolId}`) const result = await handleProxyRequest(toolId, contextParams) // Apply post-processing if available and not skipped @@ -225,7 +237,9 @@ export async function executeTool( }, } } catch (error) { - logger.error(`Error in post-processing for tool ${toolId}:`, { error }) + logger.error(`[${requestId}] Post-processing error for ${toolId}:`, { + error: error instanceof Error ? error.message : String(error), + }) // Return original result if post-processing fails, but include timing data const endTime = new Date() const endTimeISO = endTime.toISOString() @@ -254,7 +268,10 @@ export async function executeTool( }, } } catch (error: any) { - logger.error(`Error executing tool ${toolId}:`, { error }) + logger.error(`[${requestId}] Error executing tool ${toolId}:`, { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }) // Process the error to ensure we have a useful message let errorMessage = 'Unknown error occurred' @@ -305,8 +322,8 @@ export async function executeTool( errorMessage = error.message } - if (error.cause) { - errorMessage = `${errorMessage} (${error.cause})` + if ((error as any).cause) { + errorMessage = `${errorMessage} (${(error as any).cause})` } } } @@ -336,6 +353,8 @@ async function handleInternalRequest( tool: ToolConfig, params: Record ): Promise { + const requestId = crypto.randomUUID().slice(0, 8) + // Format the request parameters const requestParams = formatRequestParams(tool, params) @@ -354,7 +373,10 @@ async function handleInternalRequest( try { validateClientSideParams(requestBody.params, requestBody.schema) } catch (validationError) { - logger.error(`Custom tool params validation failed: ${validationError}`) + logger.error(`[${requestId}] Custom tool validation failed for ${toolId}:`, { + error: + validationError instanceof Error ? validationError.message : String(validationError), + }) throw validationError } } @@ -373,9 +395,15 @@ async function handleInternalRequest( let errorData try { errorData = await response.json() - logger.error(`Error response data: ${JSON.stringify(errorData)}`) + logger.error(`[${requestId}] Internal API error for ${toolId}:`, { + status: response.status, + errorData, + }) } catch (e) { - logger.error(`Failed to parse error response: ${e}`) + logger.error(`[${requestId}] Failed to parse error response for ${toolId}:`, { + status: response.status, + statusText: response.statusText, + }) throw new Error(response.statusText || `Request failed with status ${response.status}`) } @@ -394,7 +422,9 @@ async function handleInternalRequest( const data = await tool.transformResponse(response, params) return data } catch (transformError) { - logger.error(`Error in tool.transformResponse: ${transformError}`) + logger.error(`[${requestId}] Transform response error for ${toolId}:`, { + error: transformError instanceof Error ? transformError.message : String(transformError), + }) throw transformError } } @@ -408,12 +438,14 @@ async function handleInternalRequest( error: undefined, } } catch (jsonError) { - logger.error(`Error parsing JSON response: ${jsonError}`) + logger.error(`[${requestId}] JSON parse error for ${toolId}:`, { + error: jsonError instanceof Error ? jsonError.message : String(jsonError), + }) throw new Error(`Failed to parse response from ${toolId}: ${jsonError}`) } } catch (error: any) { - logger.error(`Error executing internal tool ${toolId}:`, { - error: error.stack || error.message || error, + logger.error(`[${requestId}] Internal request error for ${toolId}:`, { + error: error instanceof Error ? error.message : String(error), }) // Use the tool's error transformer if available @@ -460,8 +492,8 @@ async function handleInternalRequest( error: 'Unknown error', } } catch (transformError) { - logger.error(`Error transforming error for tool ${toolId}:`, { - transformError, + logger.error(`[${requestId}] Error transform failed for ${toolId}:`, { + error: transformError instanceof Error ? transformError.message : String(transformError), }) return { success: false, @@ -545,13 +577,16 @@ async function handleProxyRequest( toolId: string, params: Record ): Promise { - logger.info(`[handleProxyRequest] Entry: toolId=${toolId}`) + const requestId = crypto.randomUUID().slice(0, 8) + const baseUrl = env.NEXT_PUBLIC_APP_URL if (!baseUrl) { + logger.error(`[${requestId}] NEXT_PUBLIC_APP_URL not set`) throw new Error('NEXT_PUBLIC_APP_URL environment variable is not set') } const proxyUrl = new URL('/api/proxy', baseUrl).toString() + try { const response = await fetch(proxyUrl, { method: 'POST', @@ -561,8 +596,13 @@ async function handleProxyRequest( if (!response.ok) { const errorText = await response.text() + logger.error(`[${requestId}] Proxy request failed for ${toolId}:`, { + status: response.status, + statusText: response.statusText, + error: errorText.substring(0, 200), // Limit error text length + }) + let errorMessage = `HTTP error ${response.status}: ${response.statusText}` - let errorDetails = { status: response.status, statusText: response.statusText } try { // Try to parse as JSON for more details @@ -573,47 +613,28 @@ async function handleProxyRequest( ? errorJson.error : `API Error: ${response.status} ${response.statusText}` } - errorDetails = { ...errorDetails, ...errorJson } - } catch { + } catch (parseError) { // If not JSON, use the raw text - if (errorText && errorText !== 'undefined (undefined)') { - errorMessage = `${errorMessage} - ${errorText}` + if (errorText) { + errorMessage = `${errorMessage}: ${errorText}` } } - return { - success: false, - output: errorDetails, - error: errorMessage, - } + throw new Error(errorMessage) } + // Parse the successful response const result = await response.json() - - if (!result.success) { - return { - success: false, - output: result.output || {}, - error: result.error || `API request to ${toolId} failed with no error message`, - } - } - return result } catch (error: any) { - // Handle network or other fetch errors - logger.error(`Error in proxy request for tool ${toolId}:`, { error }) - - const errorMessage = - error instanceof Error - ? error.message - : typeof error === 'string' - ? error - : `Unknown error in API request to ${toolId}` + logger.error(`[${requestId}] Proxy request error for ${toolId}:`, { + error: error instanceof Error ? error.message : String(error), + }) return { success: false, - output: { originalError: error }, - error: errorMessage, + output: {}, + error: error.message || 'Proxy request failed', } } } diff --git a/apps/sim/tsconfig.json b/apps/sim/tsconfig.json index 58e1fd44c..6a94bcd93 100644 --- a/apps/sim/tsconfig.json +++ b/apps/sim/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "target": "es2020", + "target": "es2022", "module": "esnext", "moduleResolution": "bundler", - "lib": ["es2020", "dom", "dom.iterable"], + "lib": ["es2022", "dom", "dom.iterable"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, diff --git a/apps/sim/vitest.setup.ts b/apps/sim/vitest.setup.ts index e0d2679eb..a99e99c29 100644 --- a/apps/sim/vitest.setup.ts +++ b/apps/sim/vitest.setup.ts @@ -50,7 +50,6 @@ vi.mock('@/blocks/registry', () => ({ })), getAllBlocks: vi.fn(() => ({})), })) - const originalConsoleError = console.error const originalConsoleWarn = console.warn diff --git a/bun.lock b/bun.lock index 2caa45c2f..ac1f83834 100644 --- a/bun.lock +++ b/bun.lock @@ -115,6 +115,7 @@ "groq-sdk": "^0.15.0", "input-otp": "^1.4.2", "ioredis": "^5.6.0", + "jose": "6.0.11", "jwt-decode": "^4.0.0", "lenis": "^1.2.3", "lucide-react": "^0.479.0", @@ -173,7 +174,7 @@ }, "packages/cli": { "name": "simstudio", - "version": "0.1.18", + "version": "0.1.19", "bin": { "simstudio": "dist/index.js", }, @@ -2131,7 +2132,7 @@ "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], + "jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="], "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], @@ -3405,6 +3406,8 @@ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "better-auth/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], + "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],