Skip to content

Commit 732ae04

Browse files
committed
fix(agent): resolve canonical resource id when building unique tool ids
Multi-instance agent tools (table, knowledge base, workflow) get a unique id by suffixing the resource id (e.g. table_query_rows_<tableId>). The suffix code read the canonical param key (tableId/knowledgeBaseId) directly from stored params, but selector subblocks persist their value under the subblock id (tableSelector/knowledgeBaseSelector). Tools stored in that state never got a suffix, so two of them collapsed to the same name. Resolve the canonical resource id from the source subblock first — mirroring the execution-time paramsTransform — so the unique id is derived the same way the tool actually runs. Non-destructive; no-op when the canonical key is already present.
1 parent 39418f9 commit 732ae04

2 files changed

Lines changed: 113 additions & 11 deletions

File tree

apps/sim/providers/utils.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
supportsThinking,
3939
supportsToolUsageControl,
4040
supportsVerbosity,
41+
transformBlockTool,
4142
updateOllamaProviderModels,
4243
} from '@/providers/utils'
4344

@@ -1514,3 +1515,64 @@ describe('Provider/Model Blacklist', () => {
15141515
})
15151516
})
15161517
})
1518+
1519+
describe('transformBlockTool multi-instance unique IDs', () => {
1520+
const tableBlockDef = {
1521+
type: 'table',
1522+
inputs: {},
1523+
subBlocks: [
1524+
{ id: 'operation', type: 'dropdown' },
1525+
{ id: 'tableSelector', type: 'table-selector', canonicalParamId: 'tableId', mode: 'basic' },
1526+
{
1527+
id: 'manualTableId',
1528+
type: 'short-input',
1529+
canonicalParamId: 'tableId',
1530+
mode: 'advanced',
1531+
},
1532+
],
1533+
tools: {
1534+
access: ['table_query_rows', 'table_insert_row'],
1535+
config: { tool: () => 'table_query_rows' },
1536+
},
1537+
}
1538+
1539+
const getAllBlocks = () => [tableBlockDef]
1540+
const getTool = (id: string) => ({
1541+
id,
1542+
name: 'Query Rows',
1543+
description: 'Query table rows',
1544+
params: {},
1545+
})
1546+
1547+
const transformTable = (
1548+
params: Record<string, unknown>,
1549+
canonicalModes?: Record<string, 'basic' | 'advanced'>
1550+
) =>
1551+
transformBlockTool(
1552+
{ type: 'table', operation: 'query_rows', params },
1553+
{ selectedOperation: 'query_rows', getAllBlocks, getTool, canonicalModes }
1554+
)
1555+
1556+
it('appends the table id when stored under the basic selector subblock key', async () => {
1557+
const result = await transformTable({ tableSelector: 'tbl_abc' })
1558+
expect(result?.id).toBe('table_query_rows_tbl_abc')
1559+
})
1560+
1561+
it('appends the table id resolved from the advanced manual input', async () => {
1562+
const result = await transformTable(
1563+
{ manualTableId: 'tbl_xyz' },
1564+
{ 'table:tableId': 'advanced' }
1565+
)
1566+
expect(result?.id).toBe('table_query_rows_tbl_xyz')
1567+
})
1568+
1569+
it('appends the canonical table id when already present in params', async () => {
1570+
const result = await transformTable({ tableId: 'tbl_direct' })
1571+
expect(result?.id).toBe('table_query_rows_tbl_direct')
1572+
})
1573+
1574+
it('falls back to the base tool id when no table is selected', async () => {
1575+
const result = await transformTable({})
1576+
expect(result?.id).toBe('table_query_rows')
1577+
})
1578+
})

apps/sim/providers/utils.ts

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,39 @@ export function extractAndParseJSON(content: string): any {
473473
}
474474
}
475475

476+
/**
477+
* Resolves canonical pair ids (e.g. `tableId`, `knowledgeBaseId`) from a tool's
478+
* raw params, filling them in from their basic/advanced selector subblock source
479+
* values when the canonical key isn't already present.
480+
*
481+
* Selector subblocks persist their value under the subblock id (e.g.
482+
* `tableSelector`), not the canonical id, so any lookup that keys off the
483+
* canonical id — like the unique-tool-id suffix below — must resolve it first.
484+
* Mode selection mirrors {@link transformBlockTool}'s execution-time
485+
* `paramsTransform` so the resolved id matches the params the tool actually runs
486+
* with.
487+
*
488+
* @returns The params with canonical resource ids resolved (non-destructive)
489+
*/
490+
function resolveCanonicalResourceParams(
491+
params: Record<string, any>,
492+
canonicalGroups: CanonicalGroup[],
493+
blockType: string,
494+
canonicalModes?: Record<string, 'basic' | 'advanced'>
495+
): Record<string, any> {
496+
if (canonicalGroups.length === 0) return params
497+
const resolved = { ...params }
498+
for (const group of canonicalGroups) {
499+
const existing = resolved[group.canonicalId]
500+
if (existing !== undefined && existing !== null && existing !== '') continue
501+
const { basicValue, advancedValue } = getCanonicalValues(group, params)
502+
const pairMode = canonicalModes?.[`${blockType}:${group.canonicalId}`] ?? 'basic'
503+
const chosen = pairMode === 'advanced' ? advancedValue : basicValue
504+
if (chosen !== undefined) resolved[group.canonicalId] = chosen
505+
}
506+
return resolved
507+
}
508+
476509
/**
477510
* Transforms a block tool into a provider tool config with operation selection
478511
*
@@ -549,14 +582,25 @@ export async function transformBlockTool(
549582
userProvidedParams
550583
)
551584

585+
const canonicalGroups: CanonicalGroup[] = blockDef?.subBlocks
586+
? Object.values(buildCanonicalIndex(blockDef.subBlocks).groupsById).filter(isCanonicalPair)
587+
: []
588+
589+
const resolvedResourceParams = resolveCanonicalResourceParams(
590+
userProvidedParams,
591+
canonicalGroups,
592+
block.type,
593+
canonicalModes
594+
)
595+
552596
let uniqueToolId = toolConfig.id
553597
let toolName = toolConfig.name
554598
let toolDescription = enrichedDescription || toolConfig.description
555599

556-
if (toolId === 'workflow_executor' && userProvidedParams.workflowId) {
557-
uniqueToolId = `${toolConfig.id}_${userProvidedParams.workflowId}`
600+
if (toolId === 'workflow_executor' && resolvedResourceParams.workflowId) {
601+
uniqueToolId = `${toolConfig.id}_${resolvedResourceParams.workflowId}`
558602

559-
const workflowMetadata = await fetchWorkflowMetadata(userProvidedParams.workflowId)
603+
const workflowMetadata = await fetchWorkflowMetadata(resolvedResourceParams.workflowId)
560604
if (workflowMetadata) {
561605
toolName = workflowMetadata.name || toolConfig.name
562606
if (
@@ -566,21 +610,17 @@ export async function transformBlockTool(
566610
toolDescription = workflowMetadata.description
567611
}
568612
}
569-
} else if (toolId.startsWith('knowledge_') && userProvidedParams.knowledgeBaseId) {
570-
uniqueToolId = `${toolConfig.id}_${userProvidedParams.knowledgeBaseId}`
571-
} else if (toolId.startsWith('table_') && userProvidedParams.tableId) {
572-
uniqueToolId = `${toolConfig.id}_${userProvidedParams.tableId}`
613+
} else if (toolId.startsWith('knowledge_') && resolvedResourceParams.knowledgeBaseId) {
614+
uniqueToolId = `${toolConfig.id}_${resolvedResourceParams.knowledgeBaseId}`
615+
} else if (toolId.startsWith('table_') && resolvedResourceParams.tableId) {
616+
uniqueToolId = `${toolConfig.id}_${resolvedResourceParams.tableId}`
573617
}
574618

575619
const blockParamsFn = blockDef?.tools?.config?.params as
576620
| ((p: Record<string, any>) => Record<string, any>)
577621
| undefined
578622
const blockInputDefs = blockDef?.inputs as Record<string, any> | undefined
579623

580-
const canonicalGroups: CanonicalGroup[] = blockDef?.subBlocks
581-
? Object.values(buildCanonicalIndex(blockDef.subBlocks).groupsById).filter(isCanonicalPair)
582-
: []
583-
584624
const needsTransform = blockParamsFn || blockInputDefs || canonicalGroups.length > 0
585625
const paramsTransform = needsTransform
586626
? (params: Record<string, any>): Record<string, any> => {

0 commit comments

Comments
 (0)