updated agent handler

This commit is contained in:
Waleed Latif
2026-01-24 01:22:02 -08:00
parent a45c019c7c
commit b1bcd9a796
2 changed files with 230 additions and 514 deletions

View File

@@ -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) => {

View File

@@ -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<BlockOutput | StreamingExecution> {
// 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<BlockOutput | StreamingExecution> {
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<StreamingExecution> {
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
*/