From b1bcd9a796b46075a32f9260ab2da2f083d2c33b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 24 Jan 2026 01:22:02 -0800 Subject: [PATCH] updated agent handler --- .../handlers/agent/agent-handler.test.ts | 548 ++++++------------ .../executor/handlers/agent/agent-handler.ts | 196 ++----- 2 files changed, 230 insertions(+), 514 deletions(-) diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index a30f1a045..c583555a2 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -144,25 +144,22 @@ describe('AgentBlockHandler', () => { } mockGetProviderFromModel.mockReturnValue('mock-provider') - mockFetch.mockImplementation(() => { + mockExecuteProviderRequest.mockResolvedValue({ + content: 'Mocked response content', + model: 'mock-model', + tokens: { input: 10, output: 20, total: 30 }, + toolCalls: [], + cost: 0.001, + timing: { total: 100 }, + }) + + mockFetch.mockImplementation((url: string) => { return Promise.resolve({ ok: true, headers: { - get: (name: string) => { - if (name === 'Content-Type') return 'application/json' - if (name === 'X-Execution-Data') return null - return null - }, + get: () => null, }, - json: () => - Promise.resolve({ - content: 'Mocked response content', - model: 'mock-model', - tokens: { input: 10, output: 20, total: 30 }, - toolCalls: [], - cost: 0.001, - timing: { total: 100 }, - }), + json: () => Promise.resolve({}), }) }) @@ -244,7 +241,7 @@ describe('AgentBlockHandler', () => { const result = await handler.execute(mockContext, mockBlock, inputs) expect(mockGetProviderFromModel).toHaveBeenCalledWith('gpt-4o') - expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) + expect(mockExecuteProviderRequest).toHaveBeenCalled() expect(result).toEqual(expectedOutput) }) @@ -263,34 +260,21 @@ describe('AgentBlockHandler', () => { return result }) - mockFetch.mockImplementationOnce(() => { - return Promise.resolve({ - ok: true, - headers: { - get: (name: string) => { - if (name === 'Content-Type') return 'application/json' - if (name === 'X-Execution-Data') return null - return null - }, + mockExecuteProviderRequest.mockResolvedValueOnce({ + content: 'Using tools to respond', + model: 'mock-model', + tokens: { input: 10, output: 20, total: 30 }, + toolCalls: [ + { + name: 'auto_tool', + arguments: { input: 'test input for auto tool' }, }, - json: () => - Promise.resolve({ - content: 'Using tools to respond', - model: 'mock-model', - tokens: { input: 10, output: 20, total: 30 }, - toolCalls: [ - { - name: 'auto_tool', - arguments: { input: 'test input for auto tool' }, - }, - { - name: 'force_tool', - arguments: { input: 'test input for force tool' }, - }, - ], - timing: { total: 100 }, - }), - }) + { + name: 'force_tool', + arguments: { input: 'test input for force tool' }, + }, + ], + timing: { total: 100 }, }) const inputs = { @@ -403,8 +387,8 @@ describe('AgentBlockHandler', () => { expect.any(Object) // execution context ) - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] expect(requestBody.tools.length).toBe(2) }) @@ -443,8 +427,8 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] expect(requestBody.tools.length).toBe(2) @@ -488,8 +472,8 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] expect(requestBody.tools[0].usageControl).toBe('auto') expect(requestBody.tools[1].usageControl).toBe('force') @@ -553,8 +537,8 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] expect(requestBody.tools.length).toBe(2) @@ -583,7 +567,7 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) + expect(mockExecuteProviderRequest).toHaveBeenCalled() }) it('should execute with standard block tools', async () => { @@ -625,7 +609,7 @@ describe('AgentBlockHandler', () => { inputs.tools[0], expect.objectContaining({ selectedOperation: 'analyze' }) ) - expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) + expect(mockExecuteProviderRequest).toHaveBeenCalled() expect(result).toEqual(expectedOutput) }) @@ -676,30 +660,17 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) + expect(mockExecuteProviderRequest).toHaveBeenCalled() }) it('should handle responseFormat with valid JSON', async () => { - mockFetch.mockImplementationOnce(() => { - return Promise.resolve({ - ok: true, - headers: { - get: (name: string) => { - if (name === 'Content-Type') return 'application/json' - if (name === 'X-Execution-Data') return null - return null - }, - }, - json: () => - Promise.resolve({ - content: '{"result": "Success", "score": 0.95}', - model: 'mock-model', - tokens: { input: 10, output: 20, total: 30 }, - timing: { total: 100 }, - toolCalls: [], - cost: undefined, - }), - }) + mockExecuteProviderRequest.mockResolvedValueOnce({ + content: '{"result": "Success", "score": 0.95}', + model: 'mock-model', + tokens: { input: 10, output: 20, total: 30 }, + timing: { total: 100 }, + toolCalls: [], + cost: undefined, }) const inputs = { @@ -723,24 +694,11 @@ describe('AgentBlockHandler', () => { }) it('should handle responseFormat when it is an empty string', async () => { - mockFetch.mockImplementationOnce(() => { - return Promise.resolve({ - ok: true, - headers: { - get: (name: string) => { - if (name === 'Content-Type') return 'application/json' - if (name === 'X-Execution-Data') return null - return null - }, - }, - json: () => - Promise.resolve({ - content: 'Regular text response', - model: 'mock-model', - tokens: { input: 10, output: 20, total: 30 }, - timing: { total: 100 }, - }), - }) + mockExecuteProviderRequest.mockResolvedValueOnce({ + content: 'Regular text response', + model: 'mock-model', + tokens: { input: 10, output: 20, total: 30 }, + timing: { total: 100 }, }) const inputs = { @@ -763,26 +721,13 @@ describe('AgentBlockHandler', () => { }) it('should handle invalid JSON in responseFormat gracefully', async () => { - mockFetch.mockImplementationOnce(() => { - return Promise.resolve({ - ok: true, - headers: { - get: (name: string) => { - if (name === 'Content-Type') return 'application/json' - if (name === 'X-Execution-Data') return null - return null - }, - }, - json: () => - Promise.resolve({ - content: 'Regular text response', - model: 'mock-model', - tokens: { input: 10, output: 20, total: 30 }, - timing: { total: 100 }, - toolCalls: [], - cost: undefined, - }), - }) + mockExecuteProviderRequest.mockResolvedValueOnce({ + content: 'Regular text response', + model: 'mock-model', + tokens: { input: 10, output: 20, total: 30 }, + timing: { total: 100 }, + toolCalls: [], + cost: undefined, }) const inputs = { @@ -806,26 +751,13 @@ describe('AgentBlockHandler', () => { }) it('should handle variable references in responseFormat gracefully', async () => { - mockFetch.mockImplementationOnce(() => { - return Promise.resolve({ - ok: true, - headers: { - get: (name: string) => { - if (name === 'Content-Type') return 'application/json' - if (name === 'X-Execution-Data') return null - return null - }, - }, - json: () => - Promise.resolve({ - content: 'Regular text response', - model: 'mock-model', - tokens: { input: 10, output: 20, total: 30 }, - timing: { total: 100 }, - toolCalls: [], - cost: undefined, - }), - }) + mockExecuteProviderRequest.mockResolvedValueOnce({ + content: 'Regular text response', + model: 'mock-model', + tokens: { input: 10, output: 20, total: 30 }, + timing: { total: 100 }, + toolCalls: [], + cost: undefined, }) const inputs = { @@ -856,7 +788,7 @@ describe('AgentBlockHandler', () => { } mockGetProviderFromModel.mockReturnValue('openai') - mockFetch.mockRejectedValue(new Error('Provider API Error')) + mockExecuteProviderRequest.mockRejectedValueOnce(new Error('Provider API Error')) await expect(handler.execute(mockContext, mockBlock, inputs)).rejects.toThrow( 'Provider API Error' @@ -870,30 +802,17 @@ describe('AgentBlockHandler', () => { }, }) - mockFetch.mockImplementationOnce(() => { - return Promise.resolve({ - ok: true, - headers: { - get: (name: string) => { - if (name === 'Content-Type') return 'application/json' - if (name === 'X-Execution-Data') return null - return null - }, + mockExecuteProviderRequest.mockResolvedValueOnce({ + stream: mockStreamBody, + execution: { + success: true, + output: {}, + logs: [], + metadata: { + duration: 0, + startTime: new Date().toISOString(), }, - json: () => - Promise.resolve({ - stream: mockStreamBody, - execution: { - success: true, - output: {}, - logs: [], - metadata: { - duration: 0, - startTime: new Date().toISOString(), - }, - }, - }), - }) + }, }) const inputs = { @@ -947,22 +866,9 @@ describe('AgentBlockHandler', () => { }, } - mockFetch.mockImplementationOnce(() => { - return Promise.resolve({ - ok: true, - headers: { - get: (name: string) => { - if (name === 'Content-Type') return 'application/json' - if (name === 'X-Execution-Data') return JSON.stringify(mockExecutionData) - return null - }, - }, - json: () => - Promise.resolve({ - stream: mockStreamBody, - execution: mockExecutionData, - }), - }) + mockExecuteProviderRequest.mockResolvedValueOnce({ + stream: mockStreamBody, + execution: mockExecutionData, }) const inputs = { @@ -996,30 +902,21 @@ describe('AgentBlockHandler', () => { }, }) - mockFetch.mockImplementationOnce(() => { - return Promise.resolve({ - ok: true, - headers: { - get: (name: string) => (name === 'Content-Type' ? 'application/json' : null), + mockExecuteProviderRequest.mockResolvedValueOnce({ + stream: {}, // Serialized stream placeholder + execution: { + success: true, + output: { + content: 'Test streaming content', + model: 'gpt-4o', + tokens: { input: 10, output: 5, total: 15 }, }, - json: () => - Promise.resolve({ - stream: {}, // Serialized stream placeholder - execution: { - success: true, - output: { - content: 'Test streaming content', - model: 'gpt-4o', - tokens: { input: 10, output: 5, total: 15 }, - }, - logs: [], - metadata: { - startTime: new Date().toISOString(), - duration: 150, - }, - }, - }), - }) + logs: [], + metadata: { + startTime: new Date().toISOString(), + duration: 150, + }, + }, }) const inputs = { @@ -1060,8 +957,8 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] // Verify messages were built correctly expect(requestBody.messages).toBeDefined() @@ -1110,8 +1007,8 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] // Verify messages were built correctly expect(requestBody.messages).toBeDefined() @@ -1149,8 +1046,8 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] // Verify messages were built correctly expect(requestBody.messages).toBeDefined() @@ -1181,8 +1078,8 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] // Verify messages were built correctly // Agent system (1) + legacy memories (3) + user from messages (1) = 5 @@ -1225,8 +1122,8 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] // Verify messages were built correctly expect(requestBody.messages).toBeDefined() @@ -1268,8 +1165,8 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] // Verify messages were built correctly expect(requestBody.messages).toBeDefined() @@ -1310,8 +1207,8 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] // Verify user prompt content was extracted correctly expect(requestBody.messages).toBeDefined() @@ -1337,15 +1234,14 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) + expect(mockExecuteProviderRequest).toHaveBeenCalled() - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] - // Check that Azure parameters are included in the request expect(requestBody.azureEndpoint).toBe('https://my-azure-resource.openai.azure.com') expect(requestBody.azureApiVersion).toBe('2024-07-01-preview') - expect(requestBody.provider).toBe('azure-openai') + expect(providerCall[0]).toBe('azure-openai') expect(requestBody.model).toBe('azure/gpt-4o') expect(requestBody.apiKey).toBe('test-azure-api-key') }) @@ -1365,15 +1261,14 @@ describe('AgentBlockHandler', () => { await handler.execute(mockContext, mockBlock, inputs) - expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) + expect(mockExecuteProviderRequest).toHaveBeenCalled() - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] - // Check that GPT-5 parameters are included in the request expect(requestBody.reasoningEffort).toBe('minimal') expect(requestBody.verbosity).toBe('high') - expect(requestBody.provider).toBe('openai') + expect(providerCall[0]).toBe('openai') expect(requestBody.model).toBe('gpt-5') expect(requestBody.apiKey).toBe('test-api-key') }) @@ -1385,22 +1280,20 @@ describe('AgentBlockHandler', () => { userPrompt: 'Hello!', apiKey: 'test-api-key', temperature: 0.7, - // No reasoningEffort or verbosity provided } mockGetProviderFromModel.mockReturnValue('openai') await handler.execute(mockContext, mockBlock, inputs) - expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) + expect(mockExecuteProviderRequest).toHaveBeenCalled() - const fetchCall = mockFetch.mock.calls[0] - const requestBody = JSON.parse(fetchCall[1].body) + const providerCall = mockExecuteProviderRequest.mock.calls[0] + const requestBody = providerCall[1] - // Check that GPT-5 parameters are undefined when not provided expect(requestBody.reasoningEffort).toBeUndefined() expect(requestBody.verbosity).toBeUndefined() - expect(requestBody.provider).toBe('openai') + expect(providerCall[0]).toBe('openai') expect(requestBody.model).toBe('gpt-5') }) @@ -1422,42 +1315,29 @@ describe('AgentBlockHandler', () => { return Promise.resolve({ success: false, error: 'Unknown tool' }) }) - mockFetch.mockImplementationOnce(() => { - return Promise.resolve({ - ok: true, - headers: { - get: (name: string) => { - if (name === 'Content-Type') return 'application/json' - if (name === 'X-Execution-Data') return null - return null + mockExecuteProviderRequest.mockResolvedValueOnce({ + content: 'I will use MCP tools to help you.', + model: 'gpt-4o', + tokens: { input: 15, output: 25, total: 40 }, + toolCalls: [ + { + name: 'mcp-server1-list_files', + arguments: { path: '/tmp' }, + result: { + success: true, + output: { content: [{ type: 'text', text: 'Files listed' }] }, }, }, - json: () => - Promise.resolve({ - content: 'I will use MCP tools to help you.', - model: 'gpt-4o', - tokens: { input: 15, output: 25, total: 40 }, - toolCalls: [ - { - name: 'mcp-server1-list_files', - arguments: { path: '/tmp' }, - result: { - success: true, - output: { content: [{ type: 'text', text: 'Files listed' }] }, - }, - }, - { - name: 'mcp-server2-search', - arguments: { query: 'test', limit: 5 }, - result: { - success: true, - output: { content: [{ type: 'text', text: 'Search results' }] }, - }, - }, - ], - timing: { total: 150 }, - }), - }) + { + name: 'mcp-server2-search', + arguments: { query: 'test', limit: 5 }, + result: { + success: true, + output: { content: [{ type: 'text', text: 'Search results' }] }, + }, + }, + ], + timing: { total: 150 }, }) const inputs = { @@ -1533,34 +1413,21 @@ describe('AgentBlockHandler', () => { return Promise.resolve({ success: false, error: 'Unknown tool' }) }) - mockFetch.mockImplementationOnce(() => { - return Promise.resolve({ - ok: true, - headers: { - get: (name: string) => { - if (name === 'Content-Type') return 'application/json' - if (name === 'X-Execution-Data') return null - return null + mockExecuteProviderRequest.mockResolvedValueOnce({ + content: 'Let me try to use this tool.', + model: 'gpt-4o', + tokens: { input: 10, output: 15, total: 25 }, + toolCalls: [ + { + name: 'mcp-server1-failing_tool', + arguments: { param: 'value' }, + result: { + success: false, + error: 'MCP server connection failed', }, }, - json: () => - Promise.resolve({ - content: 'Let me try to use this tool.', - model: 'gpt-4o', - tokens: { input: 10, output: 15, total: 25 }, - toolCalls: [ - { - name: 'mcp-server1-failing_tool', - arguments: { param: 'value' }, - result: { - success: false, - error: 'MCP server connection failed', - }, - }, - ], - timing: { total: 100 }, - }), - }) + ], + timing: { total: 100 }, }) const inputs = { @@ -1638,25 +1505,12 @@ describe('AgentBlockHandler', () => { mockGetProviderFromModel.mockReturnValue('openai') - mockFetch.mockImplementationOnce(() => { - return Promise.resolve({ - ok: true, - headers: { - get: (name: string) => { - if (name === 'Content-Type') return 'application/json' - if (name === 'X-Execution-Data') return null - return null - }, - }, - json: () => - Promise.resolve({ - content: 'Used MCP tools successfully', - model: 'gpt-4o', - tokens: { input: 20, output: 30, total: 50 }, - toolCalls: [], - timing: { total: 200 }, - }), - }) + mockExecuteProviderRequest.mockResolvedValueOnce({ + content: 'Used MCP tools successfully', + model: 'gpt-4o', + tokens: { input: 20, output: 30, total: 50 }, + toolCalls: [], + timing: { total: 200 }, }) mockTransformBlockTool.mockImplementation((tool: any) => ({ @@ -1669,11 +1523,9 @@ describe('AgentBlockHandler', () => { const result = await handler.execute(mockContext, mockBlock, inputs) - // Verify that the agent executed successfully with MCP tools expect(result).toBeDefined() - expect(mockFetch).toHaveBeenCalled() + expect(mockExecuteProviderRequest).toHaveBeenCalled() - // Verify the agent returns the expected response format expect((result as any).content).toBe('Used MCP tools successfully') expect((result as any).model).toBe('gpt-4o') }) @@ -1691,21 +1543,12 @@ describe('AgentBlockHandler', () => { return Promise.resolve({ success: false, error: 'Unknown tool' }) }) - mockFetch.mockImplementationOnce(() => { - return Promise.resolve({ - ok: true, - headers: { - get: (name: string) => (name === 'Content-Type' ? 'application/json' : null), - }, - json: () => - Promise.resolve({ - content: 'Using MCP tool', - model: 'gpt-4o', - tokens: { input: 10, output: 10, total: 20 }, - toolCalls: [{ name: 'mcp-test-tool', arguments: {} }], - timing: { total: 50 }, - }), - }) + mockExecuteProviderRequest.mockResolvedValueOnce({ + content: 'Using MCP tool', + model: 'gpt-4o', + tokens: { input: 10, output: 10, total: 20 }, + toolCalls: [{ name: 'mcp-test-tool', arguments: {} }], + timing: { total: 50 }, }) const inputs = { @@ -1815,37 +1658,26 @@ describe('AgentBlockHandler', () => { const discoveryCalls = fetchCalls.filter((c) => c.url.includes('/api/mcp/tools/discover')) expect(discoveryCalls.length).toBe(0) - const providerCalls = fetchCalls.filter((c) => c.url.includes('/api/providers')) - expect(providerCalls.length).toBe(1) + expect(mockExecuteProviderRequest).toHaveBeenCalled() }) it('should pass toolSchema to execution endpoint when using cached schema', async () => { let executionCall: any = null - mockFetch.mockImplementation((url: string, options: any) => { - if (url.includes('/api/providers')) { - return Promise.resolve({ - ok: true, - headers: { - get: (name: string) => (name === 'Content-Type' ? 'application/json' : null), - }, - json: () => - Promise.resolve({ - content: 'Tool executed', - model: 'gpt-4o', - tokens: { input: 10, output: 10, total: 20 }, - toolCalls: [ - { - name: 'search_files', - arguments: { query: 'test' }, - result: { success: true, output: {} }, - }, - ], - timing: { total: 50 }, - }), - }) - } + mockExecuteProviderRequest.mockResolvedValueOnce({ + content: 'Tool executed', + model: 'gpt-4o', + tokens: { input: 10, output: 10, total: 20 }, + toolCalls: [ + { + name: 'search_files', + arguments: JSON.stringify({ query: 'test' }), + }, + ], + timing: { total: 50 }, + }) + mockFetch.mockImplementation((url: string, options: any) => { if (url.includes('/api/mcp/tools/execute')) { executionCall = { url, body: JSON.parse(options.body) } return Promise.resolve({ @@ -1898,13 +1730,11 @@ describe('AgentBlockHandler', () => { await handler.execute(contextWithWorkspace, mockBlock, inputs) - const providerCalls = mockFetch.mock.calls.filter((c: any) => c[0].includes('/api/providers')) - expect(providerCalls.length).toBe(1) - - const providerRequestBody = JSON.parse(providerCalls[0][1].body) - expect(providerRequestBody.tools).toBeDefined() - expect(providerRequestBody.tools.length).toBe(1) - expect(providerRequestBody.tools[0].name).toBe('search_files') + expect(mockExecuteProviderRequest).toHaveBeenCalled() + const providerCallArgs = mockExecuteProviderRequest.mock.calls[0] + expect(providerCallArgs[1].tools).toBeDefined() + expect(providerCallArgs[1].tools.length).toBe(1) + expect(providerCallArgs[1].tools[0].name).toBe('search_files') }) it('should handle multiple MCP tools from the same server efficiently', async () => { @@ -1987,14 +1817,12 @@ describe('AgentBlockHandler', () => { const discoveryCalls = fetchCalls.filter((c) => c.url.includes('/api/mcp/tools/discover')) expect(discoveryCalls.length).toBe(0) - const providerCalls = fetchCalls.filter((c) => c.url.includes('/api/providers')) - expect(providerCalls.length).toBe(1) - - const providerRequestBody = JSON.parse(providerCalls[0].options.body) - expect(providerRequestBody.tools.length).toBe(3) + expect(mockExecuteProviderRequest).toHaveBeenCalled() + const providerCallArgs = mockExecuteProviderRequest.mock.calls[0] + expect(providerCallArgs[1].tools.length).toBe(3) }) - it('should should fallback to discovery for MCP tools without cached schema', async () => { + it('should fallback to discovery for MCP tools without cached schema', async () => { const fetchCalls: any[] = [] mockFetch.mockImplementation((url: string, options: any) => { diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 9cbd6692a..6c0d19fc3 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -6,14 +6,7 @@ import { createMcpToolId } from '@/lib/mcp/utils' import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { getAllBlocks } from '@/blocks' import type { BlockOutput } from '@/blocks/types' -import { - AGENT, - BlockType, - DEFAULTS, - HTTP, - REFERENCE, - stripCustomToolPrefix, -} from '@/executor/constants' +import { AGENT, BlockType, DEFAULTS, REFERENCE, stripCustomToolPrefix } from '@/executor/constants' import { memoryService } from '@/executor/handlers/agent/memory' import type { AgentInputs, @@ -23,7 +16,7 @@ import type { } from '@/executor/handlers/agent/types' import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/executor/types' import { collectBlockData } from '@/executor/utils/block-data' -import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http' +import { buildAPIUrl, buildAuthHeaders } from '@/executor/utils/http' import { stringifyJSON } from '@/executor/utils/json' import { validateBlockType, @@ -52,11 +45,9 @@ export class AgentBlockHandler implements BlockHandler { block: SerializedBlock, inputs: AgentInputs ): Promise { - // Filter out unavailable MCP tools early so they don't appear in logs/inputs const filteredTools = await this.filterUnavailableMcpTools(ctx, inputs.tools || []) const filteredInputs = { ...inputs, tools: filteredTools } - // Validate tool permissions before processing await this.validateToolPermissions(ctx, filteredInputs.tools || []) const responseFormat = this.parseResponseFormat(filteredInputs.responseFormat) @@ -80,13 +71,7 @@ export class AgentBlockHandler implements BlockHandler { streaming: streamingConfig.shouldUseStreaming ?? false, }) - const result = await this.executeProviderRequest( - ctx, - providerRequest, - block, - responseFormat, - filteredInputs - ) + const result = await this.executeProviderRequest(ctx, providerRequest, block, responseFormat) if (this.isStreamingExecution(result)) { if (filteredInputs.memoryType && filteredInputs.memoryType !== 'none') { @@ -996,157 +981,60 @@ export class AgentBlockHandler implements BlockHandler { ctx: ExecutionContext, providerRequest: any, block: SerializedBlock, - responseFormat: any, - inputs: AgentInputs + responseFormat: any ): Promise { const providerId = providerRequest.provider const model = providerRequest.model const providerStartTime = Date.now() try { - const isBrowser = typeof window !== 'undefined' + let finalApiKey: string | undefined = providerRequest.apiKey - if (!isBrowser) { - return this.executeServerSide( - ctx, - providerRequest, - providerId, - model, - block, - responseFormat, - providerStartTime + if (providerId === 'vertex' && providerRequest.vertexCredential) { + finalApiKey = await this.resolveVertexCredential( + providerRequest.vertexCredential, + ctx.workflowId ) } - return this.executeBrowserSide( - ctx, - providerRequest, - block, - responseFormat, - providerStartTime, - inputs - ) + + const { blockData, blockNameMapping } = collectBlockData(ctx) + + const response = await executeProviderRequest(providerId, { + model, + systemPrompt: 'systemPrompt' in providerRequest ? providerRequest.systemPrompt : undefined, + context: 'context' in providerRequest ? providerRequest.context : undefined, + tools: providerRequest.tools, + temperature: providerRequest.temperature, + maxTokens: providerRequest.maxTokens, + apiKey: finalApiKey, + azureEndpoint: providerRequest.azureEndpoint, + azureApiVersion: providerRequest.azureApiVersion, + vertexProject: providerRequest.vertexProject, + vertexLocation: providerRequest.vertexLocation, + bedrockAccessKeyId: providerRequest.bedrockAccessKeyId, + bedrockSecretKey: providerRequest.bedrockSecretKey, + bedrockRegion: providerRequest.bedrockRegion, + responseFormat: providerRequest.responseFormat, + workflowId: providerRequest.workflowId, + workspaceId: ctx.workspaceId, + stream: providerRequest.stream, + messages: 'messages' in providerRequest ? providerRequest.messages : undefined, + environmentVariables: ctx.environmentVariables || {}, + workflowVariables: ctx.workflowVariables || {}, + blockData, + blockNameMapping, + isDeployedContext: ctx.isDeployedContext, + reasoningEffort: providerRequest.reasoningEffort, + verbosity: providerRequest.verbosity, + }) + + return this.processProviderResponse(response, block, responseFormat) } catch (error) { this.handleExecutionError(error, providerStartTime, providerId, model, ctx, block) throw error } } - private async executeServerSide( - ctx: ExecutionContext, - providerRequest: any, - providerId: string, - model: string, - block: SerializedBlock, - responseFormat: any, - providerStartTime: number - ) { - let finalApiKey: string | undefined = providerRequest.apiKey - - if (providerId === 'vertex' && providerRequest.vertexCredential) { - finalApiKey = await this.resolveVertexCredential( - providerRequest.vertexCredential, - ctx.workflowId - ) - } - - const { blockData, blockNameMapping } = collectBlockData(ctx) - - const response = await executeProviderRequest(providerId, { - model, - systemPrompt: 'systemPrompt' in providerRequest ? providerRequest.systemPrompt : undefined, - context: 'context' in providerRequest ? providerRequest.context : undefined, - tools: providerRequest.tools, - temperature: providerRequest.temperature, - maxTokens: providerRequest.maxTokens, - apiKey: finalApiKey, - azureEndpoint: providerRequest.azureEndpoint, - azureApiVersion: providerRequest.azureApiVersion, - vertexProject: providerRequest.vertexProject, - vertexLocation: providerRequest.vertexLocation, - bedrockAccessKeyId: providerRequest.bedrockAccessKeyId, - bedrockSecretKey: providerRequest.bedrockSecretKey, - bedrockRegion: providerRequest.bedrockRegion, - responseFormat: providerRequest.responseFormat, - workflowId: providerRequest.workflowId, - workspaceId: ctx.workspaceId, - stream: providerRequest.stream, - messages: 'messages' in providerRequest ? providerRequest.messages : undefined, - environmentVariables: ctx.environmentVariables || {}, - workflowVariables: ctx.workflowVariables || {}, - blockData, - blockNameMapping, - isDeployedContext: ctx.isDeployedContext, - }) - - return this.processProviderResponse(response, block, responseFormat) - } - - private async executeBrowserSide( - ctx: ExecutionContext, - providerRequest: any, - block: SerializedBlock, - responseFormat: any, - providerStartTime: number, - inputs: AgentInputs - ) { - const url = buildAPIUrl('/api/providers') - const response = await fetch(url.toString(), { - method: 'POST', - headers: { 'Content-Type': HTTP.CONTENT_TYPE.JSON }, - body: stringifyJSON(providerRequest), - signal: AbortSignal.timeout(AGENT.REQUEST_TIMEOUT), - }) - - if (!response.ok) { - const errorMessage = await extractAPIErrorMessage(response) - throw new Error(errorMessage) - } - - const contentType = response.headers.get('Content-Type') - if (contentType?.includes(HTTP.CONTENT_TYPE.EVENT_STREAM)) { - return this.handleStreamingResponse(response, block, ctx, inputs) - } - - const result = await response.json() - return this.processProviderResponse(result, block, responseFormat) - } - - private async handleStreamingResponse( - response: Response, - block: SerializedBlock, - _ctx?: ExecutionContext, - _inputs?: AgentInputs - ): Promise { - const executionDataHeader = response.headers.get('X-Execution-Data') - - if (executionDataHeader) { - try { - const executionData = JSON.parse(executionDataHeader) - return { - stream: response.body!, - execution: { - success: executionData.success, - output: executionData.output || {}, - error: executionData.error, - logs: [], - metadata: executionData.metadata || { - duration: DEFAULTS.EXECUTION_TIME, - startTime: new Date().toISOString(), - }, - isStreaming: true, - blockId: block.id, - blockName: block.metadata?.name, - blockType: block.metadata?.id, - } as any, - } - } catch (error) { - logger.error('Failed to parse execution data from header:', error) - } - } - - return this.createMinimalStreamingExecution(response.body!) - } - /** * Resolves a Vertex AI OAuth credential to an access token */