diff --git a/apps/docs/content/docs/blocks/workflow.mdx b/apps/docs/content/docs/blocks/workflow.mdx index f45e0ce417..3724cb7e47 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 85f8bbcc06..ec3f2f2b67 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}}' }, }, diff --git a/apps/sim/app/api/chat/[subdomain]/route.test.ts b/apps/sim/app/api/chat/[subdomain]/route.test.ts index 6837b1a0ae..538c28428c 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 80d00861e8..325629420e 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 038a3d7948..dbf50d5411 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 49aabdaec5..71e8ff6f81 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 5f116bc9c5..f0962d1ba1 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/user/settings/route.ts b/apps/sim/app/api/user/settings/route.ts index 5204aa730d..3515f9afa8 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 a0a6fc6021..ce1765e074 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 449e86b708..3ea517f142 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 07805d36b5..dce39e5bab 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 5c8879a07e..119dd23e52 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/chat/[subdomain]/chat-client.tsx b/apps/sim/app/chat/[subdomain]/chat-client.tsx index 697bc00bda..23833e8140 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 9bb91ad2a4..6ce33a6752 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 c3642e0d3e..71b24b790e 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/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 b9f17f5702..740153f7b4 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) - - if (starterFields.length > 0) { - return ( -
    - {starterFields.map((field) => renderConnectionCard(connection, field))} -
    - ) - } - } - - // 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', - } - - return renderConnectionCard(connection, field) - }) - : renderConnectionCard(connection)} -
    - ) - })} -
    - ) + // Generate all connection cards - one per block, not per output field + const connectionCards: React.ReactNode[] = [] + + sortedConnections.forEach((connection) => { + connectionCards.push(renderConnectionCard(connection)) + }) + + // 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
    {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 9812ed5b5d..a69b0c7458 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 2108a0404a..fca7b12870 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 31e3994107..620fe14e2c 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 28844be075..f0e7b71b7a 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 f49a77299c..776f29944b 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 c2a2b9c595..676c69d3ea 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 f6fd424eb7..68dbaca299 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 bd87f186ef..502585c33a 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 e713b32216..33bc2feabd 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 646cfd3bae..092ce98aaa 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 944d5c7537..91601094d0 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 b3c0d13faa..c6bfd743cb 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 38862fee73..9206c2a6c6 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 39f7cd4490..61fe71c5fd 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 a10dfd4094..218629145f 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 6e7ad1b973..754d3ea2d8 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 97d10b8ac6..bee1f381eb 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 20c0ab17e5..3eb3213cf3 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 124f1054ec..f8706924aa 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 06fc0b7843..db57259689 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 a5e0476130..d7be4e6043 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 6224a7b19a..4c8b3b18db 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 8c3b7997c5..02f3cd2ad8 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 27e9784477..7c012cf2bd 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 1ad6e708da..6ca9ccabdc 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 9ae2ed9ee8..4d8c959d23 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 5814d8db17..a8e0346b0f 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 63cea43547..2f6cffba4f 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 a884e73314..0275dd150b 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 e0493868dd..3ec546d1f6 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 394f90d5f0..080497f9ef 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 f4eacc5c89..8e25458114 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 34408d4205..ed984776ae 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 38a64ee4eb..e85bf07978 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 946769624e..59c3ff9704 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 4eb2939a8e..9cd294d366 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 ae2809a491..9296e75496 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 49adde5f42..8b13b32538 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 87c8149c07..8f3596cdfb 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 16eefd4dae..7a67bd9c1a 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 4c2c824739..67d0b57323 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 2c6fb5ce72..3035b3d5ad 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 0b12f4abcc..fea14bf697 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 f4eb9700c2..b901f4eed1 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 4720c6cb23..3e6ba92d8d 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 56f1951537..20221f331d 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 83c34ac1e7..a5d4e86bdc 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 6b298bab8b..92a94b1e7c 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 4fb5a338f5..c87c5e683a 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 fa08c00b98..bd66fe4e92 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 f7f8022347..bd8ddd368c 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 0dff75af3e..1777366898 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 8d5949e8ae..5f2f885983 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 f0f682555c..b33e08d83f 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 d2321f02f0..0e1180abfa 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 a6139c64c8..63231c96ff 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 f8361c7e1a..6579588ba4 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 67fbcf6ea1..6d08dd02b1 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 7cc7d3fe68..a870311e26 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 8037561b1f..53279a48e6 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 d00f0bdb0b..47485883c9 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 854bc814c7..fac3ae58e3 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 6c9db2e30b..6fac151831 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 d40baf3b8e..5ff45080d4 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 ea2ff6f37e..36ceb018ad 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 6d3c7a2e13..13dc29b5e8 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 f21fc8cd54..839970e95d 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 cb3c15b9f0..035ab9534d 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 } +} - // 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 [] +// 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) + } } - 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'] - } + const blockConfig = getBlock(sourceBlock.type) + if (!blockConfig) { + return { tags: [], variableInfoMap: {}, blockTagGroups: [] } + } - // Return fields from input format - return inputFormatValue.map((field: any) => `response.input.${field.name}`) - } - } catch (e) { - logger.error('Error parsing input format:', { e }) - } + 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}`) + } + + const blockTagGroups: BlockTagGroup[] = [ + { + blockName, + blockId: activeSourceBlockId, + blockType: sourceBlock.type, + tags: blockTags, + distance: 0, + }, + ] - return ['response.input'] + return { + tags: blockTags, + variableInfoMap: {}, + blockTagGroups, } + } + + // Create serialized workflow for BlockPathCalculator + const serializer = new Serializer() + const serializedWorkflow = serializer.serializeWorkflow(blocks, edges, loops, parallels) - if ('type' in obj && typeof obj.type === 'string') { - return [prefix] + // 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) } - return Object.entries(obj).flatMap(([key, value]) => { - const newPrefix = prefix ? `${prefix}.${key}` : key - return getOutputPaths(value, newPrefix, isStarterBlock) - }) + 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]) + } + } } - // Variables as tags - format as variable.{variableName} + // 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 - const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() + for (const accessibleBlockId of accessibleBlockIds) { + const accessibleBlock = blocks[accessibleBlockId] + if (!accessibleBlock) continue - // 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 }) - } - } + const blockConfig = getBlock(accessibleBlock.type) + if (!blockConfig) continue - // Then check for response format - try { - const responseFormatValue = useSubBlockStore - .getState() - .getValue(activeSourceBlockId, 'responseFormat') - if (responseFormatValue) { - const responseFormat = - typeof responseFormatValue === 'string' - ? JSON.parse(responseFormatValue) - : responseFormatValue - - 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}`)], - } - } - - // Find parallel and loop blocks connected via end-source handles - const endSourceConnections: ConnectedBlock[] = [] - - // 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' }, - ], - }, - }) - } - } - - // 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 blockName = accessibleBlock.name || accessibleBlock.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}`) - } + // 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}`) } - // 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 [] - } - } + blockTagGroups.push({ + blockName, + blockId: accessibleBlockId, + blockType: accessibleBlock.type, + tags: blockTags, + distance: blockDistances[accessibleBlockId] || 0, + }) - // Fall back to default outputs if no response format - const sourceBlock = blocks[connection.id] - if (!sourceBlock) return [] + allBlockTags.push(...blockTags) + } - const outputPaths = getOutputPaths(sourceBlock.outputs, '', sourceBlock.type === 'starter') - return outputPaths.map((path) => `${normalizedBlockName}.${path}`) - }) + // Sort block groups by distance (closest first) + blockTagGroups.sort((a, b) => a.distance - b.distance) - return { tags: [...variableTags, ...loopTags, ...parallelTags, ...sourceTags], variableInfoMap } - }, [ - blocks, - incomingConnections, - blockId, - activeSourceBlockId, - workflowVariables, - loops, - parallels, - ]) + return { + tags: [...variableTags, ...loopTags, ...parallelTags, ...allBlockTags], + variableInfoMap, + blockTagGroups, + } + }, [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) + + return { + variableTags: varTags, + loopTags: loopTags, + parallelTags: parTags, + filteredBlockTagGroups, + } + }, [filteredTags, blockTagGroups, searchTerm]) - // Create ordered tags array that matches the display order for keyboard navigation + // 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 - - // Get block name from tag (first part before the dot) - const blockName = tag.split('.')[0] - - // 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 ( - - ) - })} -
    + {filteredBlockTagGroups.map((group) => { + // Get block color from configuration + const blockConfig = getBlock(group.blockType) + const blockColor = blockConfig?.bgColor || '#2F55FF' + + 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 + + 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 914496982f..553a045d00 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/schema.ts b/apps/sim/db/schema.ts index 376a8b4eb8..9a6587582b 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 a37187ae56..6a058981be 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 61ce054d36..973da3f0c6 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 404bf092da..b2e0235b87 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 3bae100434..aeb2fae04d 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 32d258df11..54927db80d 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 89740c8ca1..31c7df53d1 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 040184ecd4..9d7e91cbc5 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 2d90ffc00f..3f479fdf2e 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 cd436c457d..096f3ca6ad 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 642a7b1346..8d41c5bd7d 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 d61fc2ee21..f40f5be94a 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 8fcda9009b..d895093b41 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 75109192ae..34a74842a0 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 b4e392cae5..0c45b11b17 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 7e1ef005e7..3c7e7a13ea 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 5e071d7518..5795355aba 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 fa69b3189b..3eaa01ea37 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 f133cf99bd..9aaf17b8a5 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 2ad38cf717..435e28c448 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 40a88781e2..66fb3cd426 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 69c6a6f2e0..dd2a1ff98b 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 8aa2625736..3459b782d7 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -71,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) { @@ -159,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) { @@ -167,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 || [], @@ -200,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 e60c7f894a..afb1e4e3e2 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' } }) - - 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' }, - }) + // Test error handling functionality + const extractErrorMessage = (executor as any).extractErrorMessage.bind(executor) - 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 f12606656c..fc4ad0c035 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.prototype.hasOwnProperty.call(this.workflowInput, 'input') && + Object.prototype.hasOwnProperty.call(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.prototype.hasOwnProperty.call(this.workflowInput, 'input') && + Object.prototype.hasOwnProperty.call(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 7dd43350d2..3de1e4923c 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 85673e79ac..b12b13981e 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 fdfdf2867e..51d4c84e53 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 0530c694cc..a9d3598ff7 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 de08c93faf..1c33fafb86 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 4e35393b95..4fa1f6c48f 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 fd3d0db58e..520dbc13d7 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 33f269845c..82af7bf498 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, + private 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,21 +511,15 @@ 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 (!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.` - ) + if (!validation.isValid) { + throw new Error(validation.errorMessage!) } + const sourceBlock = this.blockById.get(validation.resolvedBlockId!)! + if (sourceBlock.enabled === false) { throw new Error( `Block "${sourceBlock.metadata?.name || sourceBlock.id}" is disabled, and block "${currentBlock.metadata?.name || currentBlock.id}" depends on it.` @@ -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 b361fc3edd..ac76a35380 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 + } + toolCalls?: { + list: any[] + count: number + } + // Path selection fields + selectedPath?: { + blockId: string + blockType?: string + blockTitle?: string } - error?: string // Top-level error field for easy error checking - [key: string]: any // Additional properties + 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 8fec7a7b82..885ed0e6d6 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/block-path-calculator.ts b/apps/sim/lib/block-path-calculator.ts new file mode 100644 index 0000000000..8481297e75 --- /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/logs/execution-logger.ts b/apps/sim/lib/logs/execution-logger.ts index a726d7d210..b1086090a6 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 02ef418f95..20c08c27ba 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 be9d237344..66035a06b7 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/providers/anthropic/index.ts b/apps/sim/providers/anthropic/index.ts index 59e9c19e6d..41476db917 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 1c7dfa660d..91639241d2 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 d1cd664eeb..5fdca4c748 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 6b18551ba3..d91b7008d6 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 594799e339..f2413d27a5 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 b67f7fdc3d..d928924eab 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 e71c1de420..8458307868 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 9c6624574b..43452c8b7b 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 a4f2f9be9f..957d49574f 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 ab09c84fae..5261bacbab 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 6e387678e2..48578d4458 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 502d71a7ab..ec368e0e30 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 ebdbe636f4..975ac85936 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 b315e0a96c..396666823a 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 7a526f9c3e..573611e81e 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 665f7eab8e..2a6e6e1bbf 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 fee3d6da31..5fada790b0 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 9894615149..96e2664294 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 faaa1fb703..96ceb41319 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/vitest.setup.ts b/apps/sim/vitest.setup.ts index e0d2679eb4..a99e99c295 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