Skip to content

Commit e35303d

Browse files
fix(tables): pass enriched query schema to agents
1 parent 117fe31 commit e35303d

18 files changed

Lines changed: 349 additions & 56 deletions

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

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

3334
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
3435

@@ -289,6 +290,24 @@ describe('AgentBlockHandler', () => {
289290
expect(result).toEqual(expectedOutput)
290291
})
291292

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

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ import {
6767
import { isAutoModel, SIM_AUTO_MODEL_ID } from '@/providers/models'
6868
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
6969
import type { SerializedBlock } from '@/serializer/types'
70-
import { filterSchemaForLLM, type ToolSchema } from '@/tools/params'
70+
import { filterSchemaForLLM, type ToolSchema, ToolSchemaEnrichmentError } from '@/tools/params'
7171
import { getTool } from '@/tools/utils'
7272
import { getToolAsync } from '@/tools/utils.server'
7373

@@ -526,6 +526,7 @@ export class AgentBlockHandler implements BlockHandler {
526526
}
527527
return this.transformBlockTool(ctx, tool, canonicalModes, toolIndex)
528528
} catch (error) {
529+
if (error instanceof ToolSchemaEnrichmentError) throw error
529530
logger.error(
530531
'[AgentHandler] Error creating tool',
531532
projectAgentDiagnosticMetadata(
@@ -952,6 +953,12 @@ export class AgentBlockHandler implements BlockHandler {
952953
}),
953954
getTool,
954955
canonicalModes,
956+
enrichmentContext: {
957+
workflowId: ctx.workflowId,
958+
workspaceId: ctx.workspaceId,
959+
executionId: ctx.executionId,
960+
userId: ctx.userId,
961+
},
955962
toolIndex,
956963
resolveCustomBlockBinding: (blockType: string) =>
957964
resolveCustomBlockToolBinding(blockType, ctx.workspaceId),

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ vi.mock('@/tools/utils.server', () => ({ getToolAsync: vi.fn() }))
1616
import { buildSimToolSpecs } from '@/executor/handlers/pi/sim-tools'
1717
import type { ExecutionContext } from '@/executor/types'
1818
import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
19+
import { ToolSchemaEnrichmentError } from '@/tools/params'
1920

2021
function executionContext(registry: ResolvedSecretTraceRegistry | undefined): ExecutionContext {
2122
return {
@@ -76,6 +77,20 @@ describe('buildSimToolSpecs', () => {
7677
expect(mockTransformBlockTool).not.toHaveBeenCalled()
7778
})
7879

80+
it('fails fast when a tool schema cannot be enriched', async () => {
81+
const error = new ToolSchemaEnrichmentError(
82+
'table_query_rows',
83+
new Error('table metadata unavailable')
84+
)
85+
mockTransformBlockTool.mockRejectedValueOnce(error)
86+
87+
await expect(
88+
buildSimToolSpecs(completeExecutionContext(), [
89+
{ type: 'table', operation: 'query_rows', usageControl: 'auto' },
90+
])
91+
).rejects.toBe(error)
92+
})
93+
7994
it('forwards a trusted _context that an LLM-supplied _context cannot override', async () => {
8095
mockTransformBlockTool.mockResolvedValue({
8196
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
@@ -22,6 +22,7 @@ import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secr
2222
import { transformBlockTool } from '@/providers/utils'
2323
import { executeTool } from '@/tools'
2424
import { mergeToolParameters } from '@/tools/merge-params'
25+
import { ToolSchemaEnrichmentError } from '@/tools/params'
2526
import type { ToolResponse } from '@/tools/types'
2627
import { getTool } from '@/tools/utils'
2728
import { getToolAsync } from '@/tools/utils.server'
@@ -97,6 +98,12 @@ export async function buildSimToolSpecs(
9798
getAllBlocks,
9899
getTool,
99100
getToolAsync,
101+
enrichmentContext: {
102+
workflowId: ctx.workflowId,
103+
workspaceId: ctx.workspaceId,
104+
executionId: ctx.executionId,
105+
userId: ctx.userId,
106+
},
100107
resolveCustomBlockBinding: (blockType: string) =>
101108
resolveCustomBlockToolBinding(blockType, ctx.workspaceId),
102109
})
@@ -171,6 +178,7 @@ export async function buildSimToolSpecs(
171178
},
172179
})
173180
} catch (error) {
181+
if (error instanceof ToolSchemaEnrichmentError) throw error
174182
logger.warn('Failed to adapt Sim tool for Pi', {
175183
type: tool.type,
176184
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: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

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', () => {

apps/sim/tools/params.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
ParameterVisibility,
2626
ToolConfig,
2727
ToolParameterItemSchema,
28+
WorkflowToolExecutionContext,
2829
} from '@/tools/types'
2930

3031
const logger = createLogger('ToolsParams')
@@ -155,6 +156,13 @@ export interface LLMToolSchemaResult {
155156
modelBlockedParams?: string[]
156157
}
157158

159+
export class ToolSchemaEnrichmentError extends Error {
160+
constructor(toolId: string, cause: unknown) {
161+
super(`Failed to enrich schema for tool "${toolId}"`, { cause })
162+
this.name = 'ToolSchemaEnrichmentError'
163+
}
164+
}
165+
158166
export interface ValidationResult {
159167
valid: boolean
160168
missingParams: string[]
@@ -630,7 +638,8 @@ export function createUserToolSchema(
630638

631639
export async function createLLMToolSchema(
632640
toolConfig: ToolConfig,
633-
userProvidedParams: Record<string, unknown>
641+
userProvidedParams: Record<string, unknown>,
642+
enrichmentContext: WorkflowToolExecutionContext = {}
634643
): Promise<LLMToolSchemaResult> {
635644
const schema: ToolSchema = {
636645
type: 'object',
@@ -704,11 +713,17 @@ export async function createLLMToolSchema(
704713
if (toolConfig.toolEnrichment) {
705714
const dependencyValue = userProvidedParams[toolConfig.toolEnrichment.dependsOn] as string
706715
if (dependencyValue) {
707-
const enriched = await toolConfig.toolEnrichment.enrichTool(
708-
dependencyValue,
709-
schema,
710-
toolConfig.description
711-
)
716+
let enriched
717+
try {
718+
enriched = await toolConfig.toolEnrichment.enrichTool(
719+
dependencyValue,
720+
schema,
721+
toolConfig.description,
722+
enrichmentContext
723+
)
724+
} catch (error) {
725+
throw new ToolSchemaEnrichmentError(toolConfig.id, error)
726+
}
712727
if (enriched) {
713728
return {
714729
schema: enriched.parameters as ToolSchema,

0 commit comments

Comments
 (0)