Skip to content

Commit de00178

Browse files
fix(tables): pass enriched query schema to agents
1 parent 5dbe95e commit de00178

18 files changed

Lines changed: 385 additions & 57 deletions

apps/sim/executor/handlers/agent/agent-handler.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { SIM_AUTO_MODEL_ID } from '@/providers/models'
2626
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
2727
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
2828
import { executeTool } from '@/tools'
29+
import { ToolSchemaEnrichmentError } from '@/tools/params'
2930

3031
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
3132

@@ -282,6 +283,24 @@ describe('AgentBlockHandler', () => {
282283
expect(result).toEqual(expectedOutput)
283284
})
284285

286+
it('fails fast when a configured tool schema cannot be enriched', async () => {
287+
const error = new ToolSchemaEnrichmentError(
288+
'table_query_rows',
289+
new Error('table metadata unavailable')
290+
)
291+
mockTransformBlockTool.mockRejectedValueOnce(error)
292+
293+
await expect(
294+
handler.execute(mockContext, mockBlock, {
295+
model: 'gpt-4o',
296+
userPrompt: 'Query the table',
297+
apiKey: 'test-api-key',
298+
tools: [{ type: 'table', operation: 'query_rows', usageControl: 'auto' }],
299+
})
300+
).rejects.toBe(error)
301+
expect(mockExecuteProviderRequest).not.toHaveBeenCalled()
302+
})
303+
285304
it('reports a sim-auto run under the sim-auto identity, not the model that served it', async () => {
286305
mockExecuteProviderRequest.mockResolvedValue({
287306
content: 'Mocked response content',

apps/sim/executor/handlers/agent/agent-handler.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { toError } from '@sim/utils/errors'
55
import { sleep } from '@sim/utils/helpers'
66
import { truncate } from '@sim/utils/string'
77
import { and, eq, inArray, isNull } from 'drizzle-orm'
8+
import { isDev } from '@/lib/core/config/env-flags'
89
import { normalizeStringRecord, normalizeWorkflowVariables } from '@/lib/core/utils/records'
910
import { createMcpToolId } from '@/lib/mcp/utils'
1011
import {
@@ -65,7 +66,7 @@ import {
6566
import { isAutoModel, SIM_AUTO_MODEL_ID } from '@/providers/models'
6667
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
6768
import type { SerializedBlock } from '@/serializer/types'
68-
import { filterSchemaForLLM, type ToolSchema } from '@/tools/params'
69+
import { filterSchemaForLLM, type ToolSchema, ToolSchemaEnrichmentError } from '@/tools/params'
6970
import { getTool } from '@/tools/utils'
7071
import { getToolAsync } from '@/tools/utils.server'
7172

@@ -182,6 +183,32 @@ export class AgentBlockHandler implements BlockHandler {
182183
streaming: streamingConfig.shouldUseStreaming ?? false,
183184
})
184185

186+
if (isDev) {
187+
const tableQueryRowsV1Tools = formattedTools.filter((tool) => {
188+
const toolId = tool?.id
189+
if (typeof toolId !== 'string') return false
190+
191+
const isTableQueryRows =
192+
toolId === 'table_query_rows' || toolId.startsWith('table_query_rows_')
193+
const isV2 = toolId === 'table_query_rows_v2' || toolId.startsWith('table_query_rows_v2_')
194+
return isTableQueryRows && !isV2
195+
})
196+
197+
if (tableQueryRowsV1Tools.length > 0) {
198+
logger.info('Passing table_query_rows v1 tool schema to agent provider', {
199+
blockId: block.id,
200+
executionId: ctx.executionId,
201+
providerId,
202+
model,
203+
tools: tableQueryRowsV1Tools.map((tool) => ({
204+
id: tool.id,
205+
description: tool.description,
206+
parameters: tool.parameters,
207+
})),
208+
})
209+
}
210+
}
211+
185212
const result = await this.executeProviderRequest(ctx, providerRequest, block, responseFormat)
186213

187214
if (autoRouting && autoRouting.billableRoutingCost > 0) {
@@ -454,6 +481,7 @@ export class AgentBlockHandler implements BlockHandler {
454481
}
455482
return this.transformBlockTool(ctx, tool, canonicalModes, toolIndex)
456483
} catch (error) {
484+
if (error instanceof ToolSchemaEnrichmentError) throw error
457485
logger.error(`[AgentHandler] Error creating tool:`, { tool, error })
458486
return null
459487
}
@@ -783,6 +811,12 @@ export class AgentBlockHandler implements BlockHandler {
783811
}),
784812
getTool,
785813
canonicalModes,
814+
enrichmentContext: {
815+
workflowId: ctx.workflowId,
816+
workspaceId: ctx.workspaceId,
817+
executionId: ctx.executionId,
818+
userId: ctx.userId,
819+
},
786820
toolIndex,
787821
resolveCustomBlockBinding: (blockType: string) =>
788822
resolveCustomBlockToolBinding(blockType, ctx.workspaceId),

apps/sim/executor/handlers/pi/sim-tools.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ vi.mock('@/tools/utils.server', () => ({ getToolAsync: vi.fn() }))
1515

1616
import { buildSimToolSpecs } from '@/executor/handlers/pi/sim-tools'
1717
import type { ExecutionContext } from '@/executor/types'
18+
import { ToolSchemaEnrichmentError } from '@/tools/params'
1819

1920
const ctx = { workspaceId: 'ws-1' } as ExecutionContext
2021

@@ -54,6 +55,18 @@ describe('buildSimToolSpecs', () => {
5455
expect(mockTransformBlockTool).not.toHaveBeenCalled()
5556
})
5657

58+
it('fails fast when a tool schema cannot be enriched', async () => {
59+
const error = new ToolSchemaEnrichmentError(
60+
'table_query_rows',
61+
new Error('table metadata unavailable')
62+
)
63+
mockTransformBlockTool.mockRejectedValueOnce(error)
64+
65+
await expect(
66+
buildSimToolSpecs(ctx, [{ type: 'table', operation: 'query_rows', usageControl: 'auto' }])
67+
).rejects.toBe(error)
68+
})
69+
5770
it('forwards a trusted _context that an LLM-supplied _context cannot override', async () => {
5871
mockTransformBlockTool.mockResolvedValue({
5972
id: 'exa_search',

apps/sim/executor/handlers/pi/sim-tools.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { ExecutionContext } from '@/executor/types'
1717
import { transformBlockTool } from '@/providers/utils'
1818
import { executeTool } from '@/tools'
1919
import { mergeToolParameters } from '@/tools/merge-params'
20+
import { ToolSchemaEnrichmentError } from '@/tools/params'
2021
import type { ToolResponse } from '@/tools/types'
2122
import { getTool } from '@/tools/utils'
2223
import { getToolAsync } from '@/tools/utils.server'
@@ -54,6 +55,12 @@ export async function buildSimToolSpecs(
5455
getAllBlocks,
5556
getTool,
5657
getToolAsync,
58+
enrichmentContext: {
59+
workflowId: ctx.workflowId,
60+
workspaceId: ctx.workspaceId,
61+
executionId: ctx.executionId,
62+
userId: ctx.userId,
63+
},
5764
resolveCustomBlockBinding: (blockType: string) =>
5865
resolveCustomBlockToolBinding(blockType, ctx.workspaceId),
5966
})
@@ -103,6 +110,7 @@ export async function buildSimToolSpecs(
103110
},
104111
})
105112
} catch (error) {
113+
if (error instanceof ToolSchemaEnrichmentError) throw error
106114
logger.warn('Failed to adapt Sim tool for Pi', {
107115
type: tool.type,
108116
error: getErrorMessage(error),

apps/sim/providers/utils.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,6 +1622,78 @@ describe('transformBlockTool multi-instance unique IDs', () => {
16221622
expect(result?.id).toBe('table_query_rows_tbl_abc')
16231623
})
16241624

1625+
it('resolves the canonical table id before enriching the LLM tool schema', async () => {
1626+
const enrichTool = vi.fn(
1627+
async (
1628+
tableId: string,
1629+
schema: {
1630+
type: 'object'
1631+
properties: Record<string, unknown>
1632+
required: string[]
1633+
}
1634+
) => ({
1635+
description: `Query rows from ${tableId}`,
1636+
parameters: {
1637+
...schema,
1638+
properties: {
1639+
...schema.properties,
1640+
customer_name: { type: 'string' },
1641+
},
1642+
},
1643+
})
1644+
)
1645+
const result = await transformBlockTool(
1646+
{
1647+
type: 'table',
1648+
operation: 'query_rows',
1649+
params: { tableSelector: 'tbl_abc' },
1650+
},
1651+
{
1652+
selectedOperation: 'query_rows',
1653+
getAllBlocks,
1654+
enrichmentContext: {
1655+
workspaceId: 'workspace-1',
1656+
userId: 'user-1',
1657+
},
1658+
getTool: (id: string) => ({
1659+
id,
1660+
name: 'Query Rows',
1661+
description: 'Query table rows',
1662+
params: {
1663+
tableId: { type: 'string', required: true, visibility: 'user-only' },
1664+
filter: { type: 'object', visibility: 'user-or-llm' },
1665+
},
1666+
toolEnrichment: {
1667+
dependsOn: 'tableId',
1668+
enrichTool,
1669+
},
1670+
}),
1671+
}
1672+
)
1673+
1674+
expect(enrichTool).toHaveBeenCalledWith(
1675+
'tbl_abc',
1676+
expect.objectContaining({
1677+
properties: expect.objectContaining({ filter: expect.any(Object) }),
1678+
}),
1679+
'Query table rows',
1680+
{
1681+
workspaceId: 'workspace-1',
1682+
userId: 'user-1',
1683+
}
1684+
)
1685+
expect(result).toMatchObject({
1686+
id: 'table_query_rows_tbl_abc',
1687+
description: 'Query rows from tbl_abc',
1688+
params: { tableSelector: 'tbl_abc' },
1689+
parameters: {
1690+
properties: {
1691+
customer_name: { type: 'string' },
1692+
},
1693+
},
1694+
})
1695+
})
1696+
16251697
it('appends the table id resolved from the advanced manual input', async () => {
16261698
const result = await transformTable(
16271699
{ manualTableId: 'tbl_xyz' },

apps/sim/providers/utils.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type OpenAI from 'openai'
55
import type { BillingAttributionSnapshot } from '@/lib/billing/core/billing-attribution'
66
import { formatCreditCost } from '@/lib/billing/credits/conversion'
77
import { env } from '@/lib/core/config/env'
8-
import { getBlacklistedProvidersFromEnv, isHosted } from '@/lib/core/config/env-flags'
8+
import { getBlacklistedProvidersFromEnv, isDev, isHosted } from '@/lib/core/config/env-flags'
99
import {
1010
normalizeRecord,
1111
normalizeStringRecord,
@@ -53,6 +53,7 @@ import {
5353
import type { ProviderId, ProviderToolConfig } from '@/providers/types'
5454
import { useProvidersStore } from '@/stores/providers/store'
5555
import { mergeToolParameters } from '@/tools/merge-params'
56+
import type { WorkflowToolExecutionContext } from '@/tools/types'
5657

5758
const logger = createLogger('ProviderUtils')
5859

@@ -629,6 +630,7 @@ export async function transformBlockTool(
629630
getTool: (toolId: string) => any
630631
getToolAsync?: (toolId: string) => Promise<any>
631632
canonicalModes?: Record<string, 'basic' | 'advanced'>
633+
enrichmentContext?: WorkflowToolExecutionContext
632634
/**
633635
* Server-only resolver for a custom (deploy-as-block) tool's binding (bound
634636
* workflow + input schema), org-scoped to the consumer. Injected as a dependency
@@ -646,8 +648,15 @@ export async function transformBlockTool(
646648
toolIndex?: number
647649
}
648650
): Promise<ProviderToolConfig | null> {
649-
const { selectedOperation, getAllBlocks, getTool, getToolAsync, canonicalModes, toolIndex } =
650-
options
651+
const {
652+
selectedOperation,
653+
getAllBlocks,
654+
getTool,
655+
getToolAsync,
656+
canonicalModes,
657+
enrichmentContext,
658+
toolIndex,
659+
} = options
651660
const scopedCanonicalModes = scopeCanonicalModesForTool(canonicalModes, toolIndex, block.type)
652661

653662
const blockDef = getAllBlocks().find((b: any) => b.type === block.type)
@@ -755,12 +764,6 @@ export async function transformBlockTool(
755764

756765
const userProvidedParams = block.params || {}
757766

758-
const {
759-
schema: llmSchema,
760-
enrichedDescription,
761-
modelBlockedParams,
762-
} = await createLLMToolSchema(toolConfig, userProvidedParams)
763-
764767
const canonicalGroups: CanonicalGroup[] = blockDef?.subBlocks
765768
? Object.values(buildCanonicalIndex(blockDef.subBlocks).groupsById).filter(isCanonicalPair)
766769
: []
@@ -771,6 +774,12 @@ export async function transformBlockTool(
771774
scopedCanonicalModes
772775
)
773776

777+
const {
778+
schema: llmSchema,
779+
enrichedDescription,
780+
modelBlockedParams,
781+
} = await createLLMToolSchema(toolConfig, resolvedResourceParams, enrichmentContext)
782+
774783
let uniqueToolId = toolConfig.id
775784
let toolName = toolConfig.name
776785
let toolDescription = enrichedDescription || toolConfig.description
@@ -857,6 +866,16 @@ export async function transformBlockTool(
857866
}
858867
: undefined
859868

869+
if (isDev && toolConfig.id === 'table_query_rows') {
870+
logger.info('Prepared table_query_rows v1 tool schema', {
871+
toolId: uniqueToolId,
872+
tableId: resolvedResourceParams.tableId,
873+
enrichmentApplied: Boolean(enrichedDescription),
874+
description: toolDescription,
875+
parameters: llmSchema,
876+
})
877+
}
878+
860879
return {
861880
id: uniqueToolId,
862881
name: toolName,

apps/sim/tools/params.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
isPasswordParameter,
1313
type ToolParameterConfig,
1414
type ToolSchema,
15+
ToolSchemaEnrichmentError,
1516
type ValidationResult,
1617
validateToolParameters,
1718
} from '@/tools/params'
@@ -130,6 +131,27 @@ describe('Tool Parameters Utils', () => {
130131
expect(schema.required).not.toContain('apiKey') // user-only, never required for LLM
131132
expect(schema.required).toContain('message') // user-or-llm + required: true
132133
})
134+
135+
it('wraps tool enrichment failures so execution boundaries can fail fast', async () => {
136+
const cause = new Error('table metadata unavailable')
137+
const toolConfig = {
138+
...mockToolConfig,
139+
toolEnrichment: {
140+
dependsOn: 'tableId',
141+
enrichTool: vi.fn().mockRejectedValue(cause),
142+
},
143+
}
144+
145+
const error = await createLLMToolSchema(toolConfig, { tableId: 'tbl_123' }).catch(
146+
(caught) => caught
147+
)
148+
149+
expect(error).toBeInstanceOf(ToolSchemaEnrichmentError)
150+
expect(error).toMatchObject({
151+
message: 'Failed to enrich schema for tool "test_tool"',
152+
cause,
153+
})
154+
})
133155
})
134156

135157
describe('createUserToolSchema', () => {

0 commit comments

Comments
 (0)