import { createLogger } from '@sim/logger' import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format' import { buildCanonicalIndex, type CanonicalModeOverrides, evaluateSubBlockCondition, isCanonicalPair, isSubBlockFeatureEnabled, isSubBlockHidden, isTriggerModeSubBlock, resolveCanonicalMode, type SubBlockCondition, } from '@/lib/workflows/subblocks/visibility' import { isCustomBlockType, RESERVED_PARAMS } from '@/blocks/custom/build-config' import type { BlockConfig as AppBlockConfig, SubBlockConfig as BlockSubBlockConfig, GenerationType, } from '@/blocks/types' import { isNonEmpty } from '@/tools/merge-params' import { getToolMetadata, type ToolMetadata } from '@/tools/metadata' import { safeAssign } from '@/tools/safe-assign' import type { OAuthConfig, ParameterVisibility, ToolConfig, ToolParameterItemSchema, WorkflowToolExecutionContext, } from '@/tools/types' const logger = createLogger('ToolsParams') type ToolParamDefinition = ToolConfig['params'][string] // ============================================================================ // Tag/Value Parsing Utilities // ============================================================================ interface Option { label: string value: string } interface ComponentCondition { field: string value: string | number | boolean | Array not?: boolean } interface UIComponentConfig { type: string options?: Option[] placeholder?: string password?: boolean condition?: ComponentCondition title?: string value?: unknown serviceId?: string selectorKey?: BlockSubBlockConfig['selectorKey'] requiredScopes?: string[] mimeType?: string columns?: string[] min?: number max?: number step?: number integer?: boolean language?: string generationType?: string acceptedTypes?: string[] multiple?: boolean multiSelect?: boolean maxSize?: number dependsOn?: string[] | { all?: string[]; any?: string[] } /** Canonical parameter ID if this is part of a canonical group */ canonicalParamId?: string /** The mode of the source subblock (basic/advanced/both) */ mode?: 'basic' | 'advanced' | 'both' | 'trigger' | 'trigger-advanced' /** The actual subblock ID this config was derived from */ actualSubBlockId?: string /** Wand configuration for AI assistance */ wandConfig?: { enabled: boolean prompt: string generationType?: GenerationType placeholder?: string maintainHistory?: boolean } } interface SubBlockConfig { id: string type: string title?: string options?: Option[] placeholder?: string password?: boolean condition?: ComponentCondition value?: unknown serviceId?: string requiredScopes?: string[] mimeType?: string columns?: string[] min?: number max?: number step?: number integer?: boolean language?: string generationType?: string acceptedTypes?: string[] multiple?: boolean maxSize?: number dependsOn?: string[] } type ToolInputBlockConfig = Pick interface SchemaProperty { type: string description?: string items?: ToolParameterItemSchema properties?: Record required?: string[] } export interface ToolSchema { type: 'object' properties: Record required: string[] } export interface UserToolSchemaOptions { surface?: 'default' | 'copilot' /** * Set when the deployment provides hosted API keys for tools with a * `hosting` config. For unconditionally hosted tools the key param then * stays in the schema only as an optional bring-your-own-key override * instead of a required argument — the executor injects the hosted key * server-side after validation, and the key value itself is never exposed * to the model or the mothership. Tools with a conditional * `hosting.enabled` predicate keep the key required, since injection only * happens for configurations that satisfy the predicate (mirrors the VFS * `conditional_hosted_or_byok` auth mode). */ hostedKeySupport?: boolean } export interface LLMToolSchemaResult { schema: ToolSchema enrichedDescription?: string /** * Params the model is never allowed to supply, because the tool declares them * `user-only` or `hidden`. Omitting them from {@link schema} is not enough on * its own — nothing stops a model from emitting an undeclared key, and the * merge downstream seeds from the model's args — so the names travel with the * schema for `prepareToolExecution` to strip. */ modelBlockedParams?: string[] } export class ToolSchemaEnrichmentError extends Error { constructor(toolId: string, cause: unknown) { super(`Failed to enrich schema for tool "${toolId}"`, { cause }) this.name = 'ToolSchemaEnrichmentError' } } export interface ValidationResult { valid: boolean missingParams: string[] } export interface ToolParameterConfig { id: string type: string required?: boolean // Required for tool execution visibility?: ParameterVisibility // Controls who can/must provide this parameter userProvided?: boolean // User filled this parameter description?: string default?: unknown // UI component information from block config uiComponent?: UIComponentConfig } export interface ToolWithParameters { toolConfig: ToolMetadata allParameters: ToolParameterConfig[] userInputParameters: ToolParameterConfig[] // Parameters shown to user requiredParameters: ToolParameterConfig[] // Must be filled by user or LLM optionalParameters: ToolParameterConfig[] // Nice to have, shown to user } let blockConfigCache: Record | null = null function getBlockConfigurations(): Record { if (!blockConfigCache) { try { const { getAllBlocks } = require('@/blocks') const allBlocks = getAllBlocks() blockConfigCache = {} allBlocks.forEach((block: AppBlockConfig) => { blockConfigCache![block.type] = block }) } catch (error) { logger.warn('Could not load block configuration:', error) blockConfigCache = {} } } return blockConfigCache } /** * Gets the correct tool ID for a block operation. * * Pass `blockOverride` (a fresh, overlay-aware config) for custom (deploy-as-block) * blocks — the module `getBlockConfigurations()` cache can miss async-hydrated * custom blocks, which would return `undefined` here and make "add tool" silently * no-op. */ export function getToolIdForOperation( blockType: string, operation?: string, blockOverride?: Pick ): string | undefined { const block = blockOverride ?? getBlockConfigurations()[blockType] if (!block?.tools?.access) return undefined if (block.tools.access.length === 1) { return block.tools.access[0] } if (operation && block.tools.config?.tool) { try { return block.tools.config.tool({ operation }) } catch (error) { logger.error('Error selecting tool for operation:', error) } } if (operation && block.tools.access.includes(operation)) { return operation } return block.tools.access[0] } function resolveSubBlockForParam( paramId: string, subBlocks: BlockSubBlockConfig[], valuesWithOperation: Record, paramType: string ): BlockSubBlockConfig | undefined { const blockSubBlocks = subBlocks // First pass: find subblock with matching condition let fallbackMatch: BlockSubBlockConfig | undefined for (const sb of blockSubBlocks) { const matches = sb.id === paramId || sb.canonicalParamId === paramId if (!matches) continue // Remember first match as fallback (for condition-based filtering in UI) if (!fallbackMatch) fallbackMatch = sb if ( !sb.condition || evaluateSubBlockCondition(sb.condition as SubBlockCondition, valuesWithOperation) ) { return sb } } // Return fallback so its condition can be used for UI filtering if (fallbackMatch) return fallbackMatch // Check if boolean param is part of a checkbox-list if (paramType === 'boolean') { return blockSubBlocks.find( (sb) => sb.type === 'checkbox-list' && Array.isArray(sb.options) && (sb.options as Array<{ id?: string }>).some((opt) => opt.id === paramId) ) } return undefined } /** Map a custom-block field sub-block type to a tool-parameter type. */ function customFieldParamType(subBlockType: string): string { switch (subBlockType) { case 'switch': return 'boolean' case 'file-upload': return 'file[]' case 'code': return 'json' default: return 'string' } } /** * Gets all parameters for a tool, categorized by their usage * Also includes UI component information from block configurations */ export function getToolParametersConfig( toolId: string, blockType?: string, currentValues?: Record, blockConfigOverride?: Pick ): ToolWithParameters | null { try { const toolConfig = getToolMetadata(toolId) if (!toolConfig) { logger.warn(`Tool not found: ${toolId}`) return null } // Validate that toolConfig has required properties if (!toolConfig.params || typeof toolConfig.params !== 'object') { logger.warn(`Tool ${toolId} has invalid params configuration`) return null } // Custom (deploy-as-block) blocks resolve to `workflow_executor`, but their // editable inputs are their own per-field sub-blocks — not the generic // workflowId/inputMapping. Surface those so the tool panel renders the block's // real fields (and never the workflow-executor fields as "uncovered" params). // MUST run before the `workflow_executor` branch below. Read subBlocks from the // fresh, overlay-aware `blockConfigOverride` — the module `getBlockConfigurations` // cache can miss async-hydrated custom blocks. if (blockType && isCustomBlockType(blockType)) { const blockConfig = blockConfigOverride ?? getBlockConfigurations()[blockType] const fieldSubBlocks = ( (blockConfig?.subBlocks as BlockSubBlockConfig[] | undefined) ?? [] ).filter((sb) => !sb.hidden && !RESERVED_PARAMS.has(sb.id)) const parameters: ToolParameterConfig[] = fieldSubBlocks.map((sb) => ({ id: sb.id, type: customFieldParamType(sb.type), required: sb.required === true, visibility: 'user-or-llm', description: sb.description, uiComponent: { type: sb.type, title: sb.title, placeholder: sb.placeholder, language: sb.language, multiple: sb.multiple, }, })) return { toolConfig, allParameters: parameters, userInputParameters: parameters, requiredParameters: parameters.filter((param) => param.required), optionalParameters: parameters.filter((param) => !param.required), } } // Special handling for workflow_executor tool if (toolId === 'workflow_executor') { const parameters: ToolParameterConfig[] = [ { id: 'workflowId', type: 'string', required: true, visibility: 'user-only', description: 'The ID of the workflow to execute', uiComponent: { type: 'workflow-selector', placeholder: 'Select workflow to execute', selectorKey: 'sim.workflows', }, }, { id: 'inputMapping', type: 'object', required: false, visibility: 'user-or-llm', description: 'Map inputs to the selected workflow', uiComponent: { type: 'workflow-input-mapper', title: 'Workflow Inputs', condition: { field: 'workflowId', value: '', not: true, // Show when workflowId is not empty }, dependsOn: ['workflowId'], }, }, ] return { toolConfig, allParameters: parameters, userInputParameters: parameters.filter( (param) => param.visibility === 'user-or-llm' || param.visibility === 'user-only' ), requiredParameters: parameters.filter((param) => param.required), optionalParameters: parameters.filter( (param) => param.visibility === 'user-only' && !param.required ), } } // Get block configuration for UI component information let blockConfig: ToolInputBlockConfig | null = null if (blockType) { const blockConfigs = getBlockConfigurations() blockConfig = blockConfigs[blockType] || null } // Build values for condition evaluation // Operation should come from currentValues if provided, otherwise extract from toolId const values = currentValues || {} const valuesWithOperation = { ...values } if (valuesWithOperation.operation === undefined) { // Fallback: extract operation from tool ID (e.g., 'slack_message' -> 'message') const parts = toolId.split('_') valuesWithOperation.operation = parts.length >= 3 ? parts.slice(2).join('_') : parts[parts.length - 1] } // Convert tool params to our standard format with UI component info const allParameters: ToolParameterConfig[] = Object.entries(toolConfig.params).map( ([paramId, param]) => { const toolParam: ToolParameterConfig = { id: paramId, type: param.type, required: param.required ?? false, visibility: param.visibility ?? (param.required ? 'user-or-llm' : 'user-only'), description: param.description, default: param.default, } if (blockConfig) { const subBlock = resolveSubBlockForParam( paramId, blockConfig.subBlocks || [], valuesWithOperation, param.type ) if (subBlock) { if (isSubBlockHidden(subBlock)) { toolParam.visibility = 'hidden' } toolParam.uiComponent = { type: subBlock.type, options: subBlock.options as Option[] | undefined, placeholder: subBlock.placeholder, password: subBlock.password, condition: subBlock.condition as ComponentCondition | undefined, title: subBlock.title, value: subBlock.value, serviceId: subBlock.serviceId, selectorKey: subBlock.selectorKey, requiredScopes: subBlock.requiredScopes, mimeType: subBlock.mimeType, columns: subBlock.columns, min: subBlock.min, max: subBlock.max, step: subBlock.step, integer: subBlock.integer, language: subBlock.language, generationType: subBlock.generationType, acceptedTypes: subBlock.acceptedTypes ? [subBlock.acceptedTypes] : undefined, multiple: subBlock.multiple, maxSize: subBlock.maxSize, dependsOn: subBlock.dependsOn, canonicalParamId: subBlock.canonicalParamId, mode: subBlock.mode, actualSubBlockId: subBlock.id, wandConfig: subBlock.wandConfig, } } } return toolParam } ) // Parameters that should be shown to the user for input const userInputParameters = allParameters.filter( (param) => param.visibility === 'user-or-llm' || param.visibility === 'user-only' ) // Parameters that are required (must be filled by user or LLM) const requiredParameters = allParameters.filter((param) => param.required) // Parameters that are optional but can be provided by user const optionalParameters = allParameters.filter( (param) => param.visibility === 'user-only' && !param.required ) return { toolConfig, allParameters, userInputParameters, requiredParameters, optionalParameters, } } catch (error) { logger.error('Error getting tool parameters config:', error) return null } } /** * Creates a tool schema for LLM with user-provided parameters excluded */ function buildParameterSchema( toolId: string, paramId: string, param: ToolParamDefinition, options: UserToolSchemaOptions = {} ): SchemaProperty { const surface = options.surface ?? 'default' if (surface === 'copilot' && (param.type === 'file' || param.type === 'file[]')) { return buildCopilotFileParameterSchema(param) } let schemaType = param.type if (schemaType === 'json' || schemaType === 'any') { schemaType = 'object' } const propertySchema: SchemaProperty = { type: schemaType, description: param.description || '', } if (param.type === 'array' && param.items) { propertySchema.items = { ...param.items, ...(param.items.properties && { properties: { ...param.items.properties }, }), } } else if (param.items) { logger.warn(`items property ignored for non-array param "${paramId}" in tool "${toolId}"`) } return propertySchema } function buildCopilotFileParameterSchema(param: ToolParamDefinition): SchemaProperty { const baseDescription = param.description || (param.type === 'file' ? 'A file object for tool execution.' : 'An array of file objects for tool execution.') const resolutionDescription = 'For copilot and mothership tool calls, prefer passing canonical workspace file IDs such as "wf_123". The runtime will resolve them into full file objects before tool execution.' const fileObjectSchema: SchemaProperty = { type: 'object', description: `${baseDescription} ${resolutionDescription}`, properties: { id: { type: 'string', description: 'Canonical workspace file ID.' }, name: { type: 'string', description: 'File name.' }, url: { type: 'string', description: 'File URL or serve path.' }, size: { type: 'number', description: 'File size in bytes.' }, type: { type: 'string', description: 'MIME type.' }, key: { type: 'string', description: 'Internal storage key.' }, context: { type: 'string', description: 'Optional file context.' }, base64: { type: 'string', description: 'Optional base64-encoded file contents.' }, }, required: ['id', 'name', 'url', 'size', 'type', 'key'], } if (param.type === 'file') { return fileObjectSchema } return { type: 'array', description: `${baseDescription} ${resolutionDescription}`, items: { type: 'object', description: 'A file object.', properties: fileObjectSchema.properties, }, } } export function createUserToolSchema( toolConfig: ToolConfig, options: UserToolSchemaOptions = {} ): ToolSchema { const surface = options.surface ?? 'default' const hostedApiKeyParam = options.hostedKeySupport && toolConfig.hosting && !toolConfig.hosting.enabled ? toolConfig.hosting.apiKeyParam : undefined const schema: ToolSchema = { type: 'object', properties: {}, required: [], } for (const [paramId, param] of Object.entries(toolConfig.params)) { if (!param) continue const visibility = param.visibility ?? 'user-or-llm' if (visibility === 'hidden') { continue } const propertySchema = buildParameterSchema(toolConfig.id, paramId, param, options) if (paramId === hostedApiKeyParam) { propertySchema.description = [ propertySchema.description, 'Optional: Sim provides a hosted key for this tool. Omit this parameter unless intentionally overriding with your own key.', ] .filter(Boolean) .join(' ') } schema.properties[paramId] = propertySchema if (param.required && paramId !== hostedApiKeyParam) { schema.required.push(paramId) } } if (toolConfig.oauth?.required && surface === 'copilot') { schema.properties.credentialId = { type: 'string', description: 'Credential ID to use for this OAuth tool call. Required for Copilot/Superagent execution. Get valid IDs from environment/credentials.json.', } schema.required.push('credentialId') } return schema } export async function createLLMToolSchema( toolConfig: ToolConfig, userProvidedParams: Record, enrichmentContext: WorkflowToolExecutionContext = {} ): Promise { const schema: ToolSchema = { type: 'object', properties: {}, required: [], } // Derived from the declarations rather than from which branch below skipped a // param: the loop's `continue`s also skip params the user simply filled in, // and those are not off-limits to the model. const modelBlockedParams = Object.entries(toolConfig.params) .filter(([, param]) => param.visibility === 'user-only' || param.visibility === 'hidden') .map(([paramId]) => paramId) for (const [paramId, param] of Object.entries(toolConfig.params)) { const enrichmentConfig = toolConfig.schemaEnrichment?.[paramId] const isWorkflowInputMapping = toolConfig.id === 'workflow_executor' && paramId === 'inputMapping' if (enrichmentConfig) { const dependencyValue = userProvidedParams[enrichmentConfig.dependsOn] as string if (!dependencyValue) { continue } const propertySchema = buildParameterSchema(toolConfig.id, paramId, param) const enrichedSchema = await enrichmentConfig.enrichSchema(dependencyValue, enrichmentContext) if (enrichedSchema) { safeAssign(propertySchema, enrichedSchema as Record) schema.properties[paramId] = propertySchema if (param.required) { schema.required.push(paramId) } } continue } if (!isWorkflowInputMapping) { if (isNonEmpty(userProvidedParams[paramId])) { continue } if (param.visibility === 'user-only') { continue } if (param.visibility === 'hidden') { continue } } const propertySchema = buildParameterSchema(toolConfig.id, paramId, param) if (isWorkflowInputMapping) { const workflowId = userProvidedParams.workflowId as string if (workflowId) { await applyDynamicSchemaForWorkflow(propertySchema, workflowId) } } schema.properties[paramId] = propertySchema if ((param.visibility === 'user-or-llm' || param.visibility === 'llm-only') && param.required) { schema.required.push(paramId) } } if (toolConfig.toolEnrichment) { const dependencyValue = userProvidedParams[toolConfig.toolEnrichment.dependsOn] as string if (dependencyValue) { let enriched try { enriched = await toolConfig.toolEnrichment.enrichTool( dependencyValue, schema, toolConfig.description, enrichmentContext ) } catch (error) { throw new ToolSchemaEnrichmentError(toolConfig.id, error) } if (enriched) { return { schema: enriched.parameters as ToolSchema, enrichedDescription: enriched.description, modelBlockedParams, } } } } return { schema, modelBlockedParams } } /** * Apply dynamic schema enrichment for workflow_executor's inputMapping parameter */ async function applyDynamicSchemaForWorkflow( propertySchema: SchemaProperty, workflowId: string ): Promise { try { const workflowInputFields = await fetchWorkflowInputFields(workflowId) if (workflowInputFields && workflowInputFields.length > 0) { propertySchema.type = 'object' propertySchema.properties = {} propertySchema.required = [] // Convert workflow input fields to JSON schema properties for (const field of workflowInputFields) { propertySchema.properties[field.name] = { type: field.type || 'string', description: field.description || `Input field: ${field.name}`, } propertySchema.required.push(field.name) } // Update description to be more specific propertySchema.description = `Input values for the workflow. Required fields: ${workflowInputFields.map((f) => f.name).join(', ')}` } } catch (error) { logger.error('Failed to fetch workflow input fields for LLM schema:', error) } } /** * Fetches workflow input fields from the API. */ async function fetchWorkflowInputFields( workflowId: string ): Promise> { try { const { buildAuthHeaders, buildAPIUrl } = await import('@/executor/utils/http') const headers = await buildAuthHeaders() const url = buildAPIUrl(`/api/workflows/${workflowId}`) const response = await fetch(url.toString(), { headers }) if (!response.ok) { throw new Error('Failed to fetch workflow') } const { data } = await response.json() return extractInputFieldsFromBlocks(data?.state?.blocks) } catch (error) { logger.error('Error fetching workflow input fields:', error) return [] } } /** * Creates a complete tool schema for execution with all parameters */ export function createExecutionToolSchema(toolConfig: ToolConfig): ToolSchema { const schema: ToolSchema = { type: 'object', properties: {}, required: [], } Object.entries(toolConfig.params).forEach(([paramId, param]) => { const propertySchema: SchemaProperty = { type: param.type === 'json' ? 'object' : param.type, description: param.description || '', } // Include items property for arrays if (param.type === 'array' && param.items) { propertySchema.items = { ...param.items, ...(param.items.properties && { properties: { ...param.items.properties }, }), } } else if (param.items) { logger.warn( `items property ignored for non-array param "${paramId}" in tool "${toolConfig.id}"` ) } schema.properties[paramId] = propertySchema if (param.required) { schema.required.push(paramId) } }) return schema } interface FilterableToolSchema { properties?: Record required?: string[] } /** Filters user-provided parameters from any object-shaped tool schema sent to an LLM. */ export function filterSchemaForLLM( originalSchema: T, userProvidedParams: Record ): T { if (!originalSchema || !originalSchema.properties) { return originalSchema } const filteredProperties = { ...originalSchema.properties } const filteredRequired = [...(originalSchema.required || [])] // Remove user-provided parameters from the schema Object.keys(userProvidedParams).forEach((paramKey) => { if (isNonEmpty(userProvidedParams[paramKey])) { delete filteredProperties[paramKey] const reqIndex = filteredRequired.indexOf(paramKey) if (reqIndex > -1) { filteredRequired.splice(reqIndex, 1) } } }) return Object.assign({}, originalSchema, { properties: filteredProperties, required: filteredRequired, }) } /** * Validates that all required parameters are provided */ export function validateToolParameters( toolConfig: ToolConfig, finalParams: Record ): ValidationResult { const requiredParams = Object.entries(toolConfig.params) .filter(([_, param]) => param.required) .map(([paramId]) => paramId) const missingParams = requiredParams.filter( (paramId) => finalParams[paramId] === undefined || finalParams[paramId] === null || finalParams[paramId] === '' ) return { valid: missingParams.length === 0, missingParams, } } /** * Helper to check if a parameter should be treated as a password field */ export function isPasswordParameter(paramId: string): boolean { const passwordFields = [ 'password', 'apiKey', 'token', 'secret', 'key', 'credential', 'accessToken', 'refreshToken', 'botToken', 'authToken', ] return passwordFields.some((field) => paramId.toLowerCase().includes(field.toLowerCase())) } /** * Formats parameter IDs into human-readable labels */ export function formatParameterLabel(paramId: string): string { // Special cases if (paramId === 'apiKey') return 'API Key' if (paramId === 'apiVersion') return 'API Version' if (paramId === 'accessToken') return 'Access Token' if (paramId === 'refreshToken') return 'Refresh Token' if (paramId === 'botToken') return 'Bot Token' // Handle underscore and hyphen separated words if (paramId.includes('_') || paramId.includes('-')) { return paramId .split(/[-_]/) .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' ') } // Handle single character parameters if (paramId.length === 1) return paramId.toUpperCase() // Handle camelCase if (/[A-Z]/.test(paramId)) { const result = paramId.replace(/([A-Z])/g, ' $1') return ( result.charAt(0).toUpperCase() + result .slice(1) .replace(/ Api/g, ' API') .replace(/ Id/g, ' ID') .replace(/ Url/g, ' URL') .replace(/ Uri/g, ' URI') .replace(/ Ui/g, ' UI') ) } // Simple case - just capitalize first letter return paramId.charAt(0).toUpperCase() + paramId.slice(1) } /** * SubBlock IDs that control tool routing, not user-facing parameters. * Excluded from tool-input rendering unless they have an explicit paramVisibility set. */ const STRUCTURAL_SUBBLOCK_IDS = new Set(['operation']) /** * SubBlock types that represent auth/credential inputs handled separately * by the tool-input OAuth credential selector. */ const AUTH_SUBBLOCK_TYPES = new Set(['oauth-input']) /** * SubBlock types that should never appear in tool-input context. */ const EXCLUDED_SUBBLOCK_TYPES = new Set([ 'tool-input', 'skill-input', 'condition-input', 'eval-input', 'webhook-config', 'schedule-info', 'input-format', 'response-format', 'mcp-server-selector', 'mcp-tool-selector', 'mcp-dynamic-args', 'input-mapping', 'variables-input', 'messages-input', 'router-input', 'text', ]) export interface SubBlocksForToolInput { toolConfig: ToolMetadata subBlocks: BlockSubBlockConfig[] oauthConfig?: OAuthConfig } /** * Returns filtered SubBlockConfig[] for rendering in tool-input context. * Uses subblock definitions as the primary source of UI metadata, * getting all features (wandConfig, rich conditions, dependsOn, etc.) for free. * * For blocks without paramVisibility annotations, falls back to inferring * visibility from the tool's param definitions. */ export function getSubBlocksForToolInput( toolId: string, blockType: string, currentValues?: Record, canonicalModeOverrides?: CanonicalModeOverrides, blockConfigOverride?: Pick ): SubBlocksForToolInput | null { try { const toolConfig = getToolMetadata(toolId) if (!toolConfig) { logger.warn(`Tool not found: ${toolId}`) return null } const blockConfigs = getBlockConfigurations() const blockConfig = blockConfigOverride ?? blockConfigs[blockType] if (!blockConfig?.subBlocks?.length) { return null } // Custom (deploy-as-block) blocks: render their own editable field sub-blocks // as `user-or-llm` (the hidden workflowId/inputMapping wiring is filtered by // RESERVED_PARAMS — `isSubBlockHidden` does NOT honor `hidden: true`, so the // explicit reserved filter is what keeps them out). if (blockType && isCustomBlockType(blockType)) { const fieldSubBlocks = (blockConfig.subBlocks as BlockSubBlockConfig[]) .filter((sb) => !sb.hidden && !RESERVED_PARAMS.has(sb.id)) .map((sb) => ({ ...sb, paramVisibility: 'user-or-llm' as ParameterVisibility })) return { toolConfig, subBlocks: fieldSubBlocks, oauthConfig: toolConfig.oauth, } } const allSubBlocks = blockConfig.subBlocks as BlockSubBlockConfig[] const canonicalIndex = buildCanonicalIndex(allSubBlocks) // Build values for condition evaluation const values = currentValues || {} const valuesWithOperation = { ...values } if (valuesWithOperation.operation === undefined) { const parts = toolId.split('_') valuesWithOperation.operation = parts.length >= 3 ? parts.slice(2).join('_') : parts[parts.length - 1] } // Build a map of tool param IDs to their resolved visibility const toolParamVisibility: Record = {} for (const [paramId, param] of Object.entries(toolConfig.params || {})) { toolParamVisibility[paramId] = param.visibility ?? (param.required ? 'user-or-llm' : 'user-only') } // Track which canonical groups we've already included (to avoid duplicates) const includedCanonicalIds = new Set() const filtered: BlockSubBlockConfig[] = [] for (const sb of allSubBlocks) { // Skip excluded types if (EXCLUDED_SUBBLOCK_TYPES.has(sb.type)) continue // Skip trigger-mode-only subblocks if (isTriggerModeSubBlock(sb)) continue // Hide tool API key fields when running on hosted Sim or when env var is set if (isSubBlockHidden(sb)) continue // A field the deployment has switched off is not offerable here either — // the canvas already hides it, and offering it in tool-input lets an author // pick a value the executor will refuse (e.g. Python with no sandbox provider). if (!isSubBlockFeatureEnabled(sb)) continue // Determine the effective param ID (canonical or subblock id) const effectiveParamId = sb.canonicalParamId || sb.id // Resolve paramVisibility: explicit > inferred from tool params > skip let visibility = sb.paramVisibility if (!visibility) { // Infer from structural checks if (STRUCTURAL_SUBBLOCK_IDS.has(sb.id)) { visibility = 'hidden' } else if (AUTH_SUBBLOCK_TYPES.has(sb.type) && sb.canonicalParamId !== 'oauthCredential') { visibility = 'hidden' } else if (sb.canonicalParamId === 'oauthCredential') { visibility = 'user-only' } else if ( sb.password && (sb.id === 'botToken' || sb.id === 'accessToken' || sb.id === 'apiKey') ) { // Auth tokens without explicit paramVisibility are hidden // (they're handled by the OAuth credential selector or structurally) // But only if they don't have a matching tool param if (!(sb.id in toolParamVisibility)) { visibility = 'hidden' } else { visibility = toolParamVisibility[sb.id] || 'user-or-llm' } } else if (effectiveParamId in toolParamVisibility) { // Fallback: infer from tool param visibility visibility = toolParamVisibility[effectiveParamId] } else if (sb.id in toolParamVisibility) { visibility = toolParamVisibility[sb.id] } else if (sb.canonicalParamId) { visibility = 'user-or-llm' } else { continue } } // Filter by visibility: exclude hidden and llm-only if (visibility === 'hidden' || visibility === 'llm-only') continue if (sb.condition && !sb.reactiveCondition) { const conditionMet = evaluateSubBlockCondition( sb.condition as SubBlockCondition, valuesWithOperation ) if (!conditionMet) continue } // Handle canonical pairs: only include the active mode variant const canonicalId = canonicalIndex.canonicalIdBySubBlockId[sb.id] if (canonicalId) { const group = canonicalIndex.groupsById[canonicalId] if (group && isCanonicalPair(group)) { if (includedCanonicalIds.has(canonicalId)) continue includedCanonicalIds.add(canonicalId) // Determine active mode const mode = resolveCanonicalMode(group, valuesWithOperation, canonicalModeOverrides) if (mode === 'advanced') { // Find the advanced variant const advancedSb = allSubBlocks.find((s) => group.advancedIds.includes(s.id)) if (advancedSb) { filtered.push({ ...advancedSb, paramVisibility: visibility }) } } else { // Include basic variant (current sb if it's the basic one) if (group.basicId === sb.id) { filtered.push({ ...sb, paramVisibility: visibility }) } else { const basicSb = allSubBlocks.find((s) => s.id === group.basicId) if (basicSb) { filtered.push({ ...basicSb, paramVisibility: visibility }) } } } continue } } // Non-canonical, non-hidden, condition-passing subblock filtered.push({ ...sb, paramVisibility: visibility }) } return { toolConfig, subBlocks: filtered, oauthConfig: toolConfig.oauth, } } catch (error) { logger.error('Error getting subblocks for tool input:', error) return null } }