import { createLogger } from '@sim/logger' import { isRecordLike } from '@sim/utils/object' import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format' import { buildCanonicalIndex, type CanonicalModeOverrides, evaluateSubBlockCondition, isCanonicalPair, isSubBlockHidden, isTriggerModeSubBlock, resolveCanonicalMode, type SubBlockCondition, } from '@/lib/workflows/subblocks/visibility' import type { BlockConfig as AppBlockConfig, SubBlockConfig as BlockSubBlockConfig, GenerationType, } from '@/blocks/types' import { safeAssign } from '@/tools/safe-assign' import { isEmptyTagValue } from '@/tools/shared/tags' import type { OAuthConfig, ParameterVisibility, ToolConfig } from '@/tools/types' import { getTool } from '@/tools/utils' const logger = createLogger('ToolsParams') type ToolParamDefinition = ToolConfig['params'][string] /** * Checks if a value is non-empty (not undefined, null, or empty string) */ export function isNonEmpty(value: unknown): boolean { return value !== undefined && value !== null && value !== '' } // ============================================================================ // 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?: Record properties?: Record required?: string[] } export interface ToolSchema { type: 'object' properties: Record required: string[] } export interface UserToolSchemaOptions { surface?: 'default' | 'copilot' } export interface LLMToolSchemaResult { schema: ToolSchema enrichedDescription?: string } 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: ToolConfig 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. */ export function getToolIdForOperation(blockType: string, operation?: string): string | undefined { const block = 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 } /** * 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 ): ToolWithParameters | null { try { const toolConfig = getTool(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 } // 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 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) schema.properties[paramId] = propertySchema if (param.required) { 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 ): Promise { const schema: ToolSchema = { type: 'object', properties: {}, required: [], } 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) 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) { const enriched = await toolConfig.toolEnrichment.enrichTool( dependencyValue, schema, toolConfig.description ) if (enriched) { return { schema: enriched.parameters as ToolSchema, enrichedDescription: enriched.description, } } } } return { schema } } /** * Apply dynamic schema enrichment for workflow_executor's inputMapping parameter */ async function applyDynamicSchemaForWorkflow( propertySchema: any, 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: any = { 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 } /** * Deep merges inputMapping objects, where LLM values fill in empty/missing user values. * User-provided non-empty values take precedence. */ export function deepMergeInputMapping( llmInputMapping: Record | undefined, userInputMapping: Record | string | undefined ): Record { // Parse user inputMapping if it's a JSON string let parsedUserMapping: Record = {} if (typeof userInputMapping === 'string') { try { const parsed = JSON.parse(userInputMapping) if (isRecordLike(parsed)) { parsedUserMapping = parsed } } catch { // Invalid JSON, treat as empty } } else if ( typeof userInputMapping === 'object' && userInputMapping !== null && !Array.isArray(userInputMapping) ) { parsedUserMapping = userInputMapping } // If no LLM mapping, return user mapping (or empty) if (!llmInputMapping || typeof llmInputMapping !== 'object') { return parsedUserMapping } // Deep merge: LLM values as base, user non-empty values override // If user provides empty object {}, LLM values fill all fields (intentional) const merged: Record = { ...llmInputMapping } for (const [key, userValue] of Object.entries(parsedUserMapping)) { // Only override LLM value if user provided a non-empty value if (isNonEmpty(userValue)) { merged[key] = userValue } } return merged } /** * Merges user-provided parameters with LLM-generated parameters. * User-provided parameters take precedence, but empty strings are skipped * so that LLM-generated values are used when user clears a field. * * Special handling for inputMapping: deep merges so LLM can fill in * fields that user left empty in the UI. */ export function mergeToolParameters( userProvidedParams: Record, llmGeneratedParams: Record ): Record { // Filter out empty and effectively-empty values from user-provided params // so that cleared fields don't override LLM values const filteredUserParams: Record = {} for (const [key, value] of Object.entries(userProvidedParams)) { if (isNonEmpty(value)) { // Skip tag-based params if they're effectively empty (only default/unfilled entries) if ((key === 'documentTags' || key === 'tagFilters') && isEmptyTagValue(value)) { continue } filteredUserParams[key] = value } } // Start with LLM params as base const result: Record = { ...llmGeneratedParams } // Apply user params, with special handling for inputMapping for (const [key, userValue] of Object.entries(filteredUserParams)) { if (key === 'inputMapping') { // Deep merge inputMapping so LLM values fill in empty user fields const llmInputMapping = llmGeneratedParams.inputMapping as Record | undefined const mergedInputMapping = deepMergeInputMapping( llmInputMapping, userValue as Record | string | undefined ) result.inputMapping = mergedInputMapping } else { // Normal override for other params result[key] = userValue } } // If LLM provided inputMapping but user didn't, ensure it's included if (llmGeneratedParams.inputMapping && !filteredUserParams.inputMapping) { result.inputMapping = llmGeneratedParams.inputMapping } return result } /** * Filters out user-provided parameters from tool schema for LLM */ export function filterSchemaForLLM( originalSchema: ToolSchema, userProvidedParams: Record ): ToolSchema { 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 { ...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: ToolConfig 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 = getTool(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 } 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 // 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 } }