import { existsSync, readFileSync } from 'fs' import { join } from 'path' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { z } from 'zod' import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { getAllowedIntegrationsFromEnv, isHosted } from '@/lib/core/config/env-flags' import { getServiceAccountProviderForProviderId } from '@/lib/oauth/utils' import { isCustomBlockType } from '@/blocks/custom/build-config' import { getBlock } from '@/blocks/registry' import { AuthMode, type BlockConfig, isHiddenFromDisplay } from '@/blocks/types' import { isHiddenUnder, overlayVisibility } from '@/blocks/visibility/context' import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check' import { PROVIDER_DEFINITIONS } from '@/providers/models' import { tools as toolsRegistry } from '@/tools/registry' import { getTrigger, isTriggerValid } from '@/triggers' import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants' interface CopilotSubblockMetadata { id: string type: string title?: string required?: boolean description?: string placeholder?: string layout?: string mode?: string hidden?: boolean condition?: any // Dropdown/combobox options options?: { id: string; label?: string; hasIcon?: boolean }[] // Numeric constraints min?: number max?: number step?: number integer?: boolean // Text input properties rows?: number password?: boolean multiSelect?: boolean // Code/generation properties language?: string generationType?: string // OAuth/credential properties serviceId?: string requiredScopes?: string[] // File properties mimeType?: string acceptedTypes?: string multiple?: boolean maxSize?: number // Other properties connectionDroppable?: boolean columns?: string[] wandConfig?: any availableTriggers?: string[] triggerProvider?: string dependsOn?: string[] canonicalParamId?: string defaultValue?: any value?: string // 'function' if it's a function, undefined otherwise } interface CopilotToolMetadata { id: string name: string description?: string inputs?: any outputs?: any } interface CopilotTriggerMetadata { id: string outputs?: any configFields?: any } interface CopilotBlockMetadata { id: string name: string description: string bestPractices?: string inputSchema: CopilotSubblockMetadata[] inputDefinitions?: Record triggerAllowed?: boolean authType?: 'OAuth' | 'API Key' | 'Bot Token' tools: CopilotToolMetadata[] triggers: CopilotTriggerMetadata[] operationInputSchema: Record operations?: Record< string, { toolId?: string toolName?: string description?: string inputs?: Record outputs?: Record inputSchema?: CopilotSubblockMetadata[] } > outputs?: Record yamlDocumentation?: string } const GetBlocksMetadataInputSchema = z.object({ blockIds: z.array(z.string()).min(1) }) const GetBlocksMetadataResultSchema = z.object({ metadata: z.record(z.string(), z.any()) }) export const getBlocksMetadataServerTool: BaseServerTool< z.infer, z.infer > = { name: 'get_blocks_metadata', inputSchema: GetBlocksMetadataInputSchema, outputSchema: GetBlocksMetadataResultSchema, async execute( { blockIds }: z.infer, context?: { userId: string; workspaceId?: string } ): Promise> { const logger = createLogger('GetBlocksMetadataServerTool') logger.debug('Executing get_blocks_metadata', { count: blockIds?.length }) const permissionConfig = context?.userId && context?.workspaceId ? await getUserPermissionConfig(context.userId, context.workspaceId) : null const allowedIntegrations = permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv() const result: Record = {} for (const blockId of blockIds || []) { if (allowedIntegrations != null && !allowedIntegrations.includes(blockId.toLowerCase())) { logger.debug('Block not allowed by permission group', { blockId }) continue } let metadata: any if (SPECIAL_BLOCKS_METADATA[blockId]) { const specialBlock = SPECIAL_BLOCKS_METADATA[blockId] const { commonParameters, operationParameters } = splitParametersByOperation( specialBlock.subBlocks || [], specialBlock.inputs || {} ) metadata = { id: specialBlock.id, name: specialBlock.name, description: specialBlock.description || '', inputSchema: commonParameters, inputDefinitions: specialBlock.inputs || {}, tools: [], triggers: [], operationInputSchema: operationParameters, outputs: specialBlock.outputs, } ;(metadata as any).subBlocks = undefined } else { const blockConfig: BlockConfig | undefined = getBlock(blockId) if (!blockConfig) { logger.debug('Block not found in registry', { blockId }) continue } if (blockConfig.hideFromToolbar) { logger.debug('Skipping block hidden from toolbar', { blockId }) continue } // getBlock is pure, so the viewer's visibility must be checked // explicitly: unrevealed preview blocks and kill-switched types stay // out of the agent's metadata (the router wraps this tool in // withBlockVisibility). if (isHiddenUnder(overlayVisibility(), blockConfig)) { logger.debug('Skipping block gated by visibility', { blockId }) continue } if (isCustomBlockType(blockId)) { // Custom (deploy-as-block) blocks run a bound workflow via an internal // `workflow_executor`; the agent never configures a workflowId/inputMapping. // Present it as self-contained: its visible input fields + curated outputs, // no tools/operations. const visibleSubBlocks = (blockConfig.subBlocks || []).filter((sb) => !sb.hidden) const outputs = blockConfig.outputs ? Object.fromEntries( Object.entries(blockConfig.outputs).filter(([_, def]) => !isHiddenFromDisplay(def)) ) : undefined metadata = { id: blockId, name: blockConfig.name || blockId, description: blockConfig.longDescription || blockConfig.description || '', bestPractices: blockConfig.bestPractices, inputSchema: visibleSubBlocks.map(processSubBlock), inputDefinitions: {}, tools: [], triggers: [], operationInputSchema: {}, outputs, } result[blockId] = removeNullish(metadata) as CopilotBlockMetadata continue } const tools: CopilotToolMetadata[] = Array.isArray(blockConfig.tools?.access) ? blockConfig.tools!.access.map((toolId) => { const tool = toolsRegistry[toolId] if (!tool) return { id: toolId, name: toolId } return { id: toolId, name: tool.name || toolId, description: getCopilotToolDescription(tool, { isHosted, fallbackName: toolId, }), inputs: tool.params || {}, outputs: tool.outputs || {}, } }) : [] const triggers: CopilotTriggerMetadata[] = [] const availableTriggerIds = blockConfig.triggers?.available || [] for (const tid of availableTriggerIds) { if (!isTriggerValid(tid)) { logger.debug('Invalid trigger ID found in block config', { blockId, triggerId: tid }) continue } const trig = getTrigger(tid) const configFields: Record = {} for (const subBlock of trig.subBlocks) { if ( (subBlock.mode === 'trigger' || subBlock.mode === 'trigger-advanced') && !SYSTEM_SUBBLOCK_IDS.includes(subBlock.id) ) { const fieldDef: any = { type: subBlock.type, required: subBlock.required || false, } if (subBlock.title) fieldDef.title = subBlock.title if (subBlock.description) fieldDef.description = subBlock.description if (subBlock.placeholder) fieldDef.placeholder = subBlock.placeholder if (subBlock.defaultValue !== undefined) fieldDef.default = subBlock.defaultValue if (subBlock.options && Array.isArray(subBlock.options)) { fieldDef.options = subBlock.options.map((opt: any) => ({ id: opt.id, label: opt.label || opt.id, })) } if (subBlock.condition) { const cond = typeof subBlock.condition === 'function' ? subBlock.condition() : subBlock.condition if (cond) { fieldDef.condition = cond } } configFields[subBlock.id] = fieldDef } } triggers.push({ id: tid, outputs: trig.outputs || {}, configFields, }) } const blockInputs = computeBlockLevelInputs(blockConfig) const { commonParameters, operationParameters } = splitParametersByOperation( Array.isArray(blockConfig.subBlocks) ? blockConfig.subBlocks.filter( (sb) => sb.mode !== 'trigger' && sb.mode !== 'trigger-advanced' ) : [], blockInputs ) const operationInputs = computeOperationLevelInputs(blockConfig) const operationIds = resolveOperationIds(blockConfig, operationParameters) const operations: Record = {} for (const opId of operationIds) { const resolvedToolId = resolveToolIdForOperation(blockConfig, opId) const toolCfg = resolvedToolId ? toolsRegistry[resolvedToolId] : undefined const toolParams: Record = toolCfg?.params || {} const toolOutputs: Record = toolCfg?.outputs ? Object.fromEntries( Object.entries(toolCfg.outputs).filter(([_, def]) => !isHiddenFromDisplay(def)) ) : {} const filteredToolParams: Record = {} for (const [k, v] of Object.entries(toolParams)) { if (!(k in blockInputs)) filteredToolParams[k] = v } operations[opId] = { toolId: resolvedToolId, toolName: toolCfg?.name || resolvedToolId, description: toolCfg ? getCopilotToolDescription(toolCfg, { isHosted, fallbackName: resolvedToolId, }) : undefined, inputs: { ...filteredToolParams, ...(operationInputs[opId] || {}) }, outputs: toolOutputs, inputSchema: operationParameters[opId] || [], } } const filteredOutputs = blockConfig.outputs ? Object.fromEntries( Object.entries(blockConfig.outputs).filter(([_, def]) => !isHiddenFromDisplay(def)) ) : undefined metadata = { id: blockId, name: blockConfig.name || blockId, description: blockConfig.longDescription || blockConfig.description || '', bestPractices: blockConfig.bestPractices, inputSchema: commonParameters, inputDefinitions: blockInputs, triggerAllowed: !!blockConfig.triggerAllowed, authType: resolveAuthType(blockConfig.authMode), tools, triggers, operationInputSchema: operationParameters, operations, outputs: filteredOutputs, } } try { const workingDir = process.cwd() const isInAppsSim = workingDir.endsWith('/apps/sim') || workingDir.endsWith('\\apps\\sim') const basePath = isInAppsSim ? join(workingDir, '..', '..') : workingDir const docPath = join( basePath, 'apps', 'docs', 'content', 'docs', 'yaml', 'blocks', `${DOCS_FILE_MAPPING[blockId] || blockId}.mdx` ) if (existsSync(docPath)) { metadata.yamlDocumentation = readFileSync(docPath, 'utf-8') } } catch (error) { logger.warn('Failed to read YAML documentation file', { error: toError(error).message, }) } if (metadata) { result[blockId] = removeNullish(metadata) as CopilotBlockMetadata } } const transformedResult: Record = {} for (const [blockId, metadata] of Object.entries(result)) { transformedResult[blockId] = transformBlockMetadata(metadata) } return GetBlocksMetadataResultSchema.parse({ metadata: transformedResult }) }, } function transformBlockMetadata(metadata: CopilotBlockMetadata): any { const transformed: any = { blockType: metadata.id, name: metadata.name, description: metadata.description, } if (metadata.bestPractices) { transformed.bestPractices = metadata.bestPractices } if (metadata.authType) { transformed.authType = metadata.authType if (metadata.authType === 'OAuth') { transformed.requiredCredentials = { type: 'oauth', service: metadata.id, // e.g., 'gmail', 'slack', etc. description: `OAuth authentication required for ${metadata.name}`, } // Check if this service also supports service account credentials const oauthSubBlock = metadata.inputSchema?.find( (sb: CopilotSubblockMetadata) => sb.type === 'oauth-input' && sb.serviceId ) if (oauthSubBlock?.serviceId) { const serviceAccountProviderId = getServiceAccountProviderForProviderId( oauthSubBlock.serviceId ) if (serviceAccountProviderId) { transformed.requiredCredentials.serviceAccountType = serviceAccountProviderId transformed.requiredCredentials.description = `OAuth or service account authentication supported for ${metadata.name}` } } } else if (metadata.authType === 'API Key') { transformed.requiredCredentials = { type: 'api_key', description: `API key required for ${metadata.name}`, } } else if (metadata.authType === 'Bot Token') { transformed.requiredCredentials = { type: 'bot_token', description: `Bot token required for ${metadata.name}`, } } } const inputs = extractInputs(metadata) if (inputs.required.length > 0 || inputs.optional.length > 0) { transformed.inputs = inputs } const hasOperations = metadata.operations && Object.keys(metadata.operations).length > 0 if (hasOperations && metadata.operations) { const blockLevelInputs = new Set(Object.keys(metadata.inputDefinitions || {})) transformed.operations = Object.entries(metadata.operations).reduce( (acc, [opId, opData]) => { acc[opId] = { name: opData.toolName || opId, description: opData.description, inputs: extractOperationInputs(opData, blockLevelInputs), outputs: formatOutputsFromDefinition(opData.outputs || {}), } return acc }, {} as Record ) } if (!hasOperations) { const outputs = extractOutputs(metadata) if (outputs.length > 0) { transformed.outputs = outputs } } if (metadata.triggers && metadata.triggers.length > 0) { transformed.triggers = metadata.triggers.map((t) => ({ id: t.id, outputs: formatOutputsFromDefinition(t.outputs || {}), configFields: t.configFields || {}, })) } if (metadata.yamlDocumentation) { transformed.yamlDocumentation = metadata.yamlDocumentation } return transformed } function extractInputs(metadata: CopilotBlockMetadata): { required: any[] optional: any[] } { const required: any[] = [] const optional: any[] = [] const inputDefs = metadata.inputDefinitions || {} for (const schema of metadata.inputSchema || []) { // Skip trigger subBlocks - they're handled separately in triggers.configFields if (schema.mode === 'trigger' || schema.mode === 'trigger-advanced') { continue } if (schema.id === 'triggerConfig' || schema.type === 'trigger-config') { continue } const inputDef = inputDefs[schema.id] || inputDefs[schema.canonicalParamId || ''] let description = schema.description || inputDef?.description || schema.title if (schema.id === 'operation') { description = 'Operation to perform' } const input: any = { name: schema.id, type: mapSchemaTypeToSimpleType(schema.type, schema), description, } if (schema.options && schema.options.length > 0) { input.options = schema.options.map((opt) => opt.id || opt.label) } if (inputDef?.enum && Array.isArray(inputDef.enum)) { input.options = inputDef.enum } if (schema.defaultValue !== undefined) { input.default = schema.defaultValue } else if (inputDef?.default !== undefined) { input.default = inputDef.default } if (schema.type === 'slider' || schema.type === 'number-input') { if (schema.min !== undefined) input.min = schema.min if (schema.max !== undefined) input.max = schema.max } else if (inputDef?.minimum !== undefined || inputDef?.maximum !== undefined) { if (inputDef.minimum !== undefined) input.min = inputDef.minimum if (inputDef.maximum !== undefined) input.max = inputDef.maximum } const example = generateInputExample(schema, inputDef) if (example !== undefined) { input.example = example } const isOperationField = schema.id === 'operation' && metadata.operations && Object.keys(metadata.operations).length > 0 const isRequired = schema.required || inputDef?.required || isOperationField if (isRequired) { required.push(input) } else { optional.push(input) } } return { required, optional } } function extractOperationInputs( opData: any, blockLevelInputs: Set ): { required: any[] optional: any[] } { const required: any[] = [] const optional: any[] = [] const inputs = opData.inputs || {} for (const [key, inputDef] of Object.entries(inputs)) { if (blockLevelInputs.has(key)) { continue } const input: any = { name: key, type: (inputDef as any)?.type || 'string', description: (inputDef as any)?.description, } if ((inputDef as any)?.enum) { input.options = (inputDef as any).enum } if ((inputDef as any)?.default !== undefined) { input.default = (inputDef as any).default } if ((inputDef as any)?.example !== undefined) { input.example = (inputDef as any).example } if ((inputDef as any)?.required) { required.push(input) } else { optional.push(input) } } return { required, optional } } function extractOutputs(metadata: CopilotBlockMetadata): any[] { const outputs: any[] = [] if (metadata.outputs && Object.keys(metadata.outputs).length > 0) { return formatOutputsFromDefinition(metadata.outputs) } if (metadata.operations && Object.keys(metadata.operations).length > 0) { const firstOp = Object.values(metadata.operations)[0] return formatOutputsFromDefinition(firstOp.outputs || {}) } return outputs } function formatOutputsFromDefinition(outputDefs: Record): any[] { const outputs: any[] = [] for (const [key, def] of Object.entries(outputDefs)) { const output: any = { name: key, type: typeof def === 'string' ? def : def?.type || 'any', } if (typeof def === 'object') { if (def.description) output.description = def.description if (def.example) output.example = def.example } outputs.push(output) } return outputs } function mapSchemaTypeToSimpleType(schemaType: string, schema: CopilotSubblockMetadata): string { const typeMap: Record = { 'short-input': 'string', 'long-input': 'string', 'code-input': 'string', 'number-input': 'number', slider: 'number', dropdown: 'string', combobox: 'string', toggle: 'boolean', 'json-input': 'json', 'file-upload': 'file', 'multi-select': 'array', 'credential-input': 'credential', 'oauth-credential': 'credential', 'oauth-input': 'credential', } const mappedType = typeMap[schemaType] || schemaType if (schema.multiSelect) return 'array' return mappedType } function generateInputExample(schema: CopilotSubblockMetadata, inputDef?: any): any { if (inputDef?.example !== undefined) return inputDef.example switch (schema.type) { case 'short-input': case 'long-input': if (schema.id === 'systemPrompt') return 'You are a helpful assistant...' if (schema.id === 'userPrompt') return 'What is the weather today?' if (schema.placeholder) return schema.placeholder return undefined case 'number-input': case 'slider': return schema.defaultValue ?? schema.min ?? 0 case 'toggle': return schema.defaultValue ?? false case 'json-input': return schema.defaultValue ?? {} case 'dropdown': case 'combobox': if (schema.options && schema.options.length > 0) { return schema.options[0].id } return undefined default: return undefined } } function processSubBlock(sb: any): CopilotSubblockMetadata { const processed: CopilotSubblockMetadata = { id: sb.id, type: sb.type, } const optionalFields = { title: sb.title, required: sb.required, description: sb.description, placeholder: sb.placeholder, layout: sb.layout, mode: sb.mode, hidden: sb.hidden, canonicalParamId: sb.canonicalParamId, defaultValue: sb.defaultValue, // Numeric constraints min: sb.min, max: sb.max, step: sb.step, integer: sb.integer, // Text input properties rows: sb.rows, password: sb.password, multiSelect: sb.multiSelect, // Code/generation properties language: sb.language, generationType: sb.generationType, // OAuth/credential properties serviceId: sb.serviceId, requiredScopes: sb.requiredScopes, // File properties mimeType: sb.mimeType, acceptedTypes: sb.acceptedTypes, multiple: sb.multiple, maxSize: sb.maxSize, // Other properties connectionDroppable: sb.connectionDroppable, columns: sb.columns, wandConfig: sb.wandConfig, availableTriggers: sb.availableTriggers, triggerProvider: sb.triggerProvider, dependsOn: sb.dependsOn, } // Add non-null optional fields for (const [key, value] of Object.entries(optionalFields)) { if (value !== undefined && value !== null) { ;(processed as any)[key] = value } } // Handle condition normalization const condition = normalizeCondition(sb.condition) if (condition !== undefined) { processed.condition = condition } // Handle value field (check if it's a function) if (typeof sb.value === 'function') { processed.value = 'function' } // Process options with icon detection const options = resolveSubblockOptions(sb) if (options) { processed.options = options } return processed } function resolveAuthType( authMode: AuthMode | undefined ): 'OAuth' | 'API Key' | 'Bot Token' | undefined { if (!authMode) return undefined if (authMode === AuthMode.OAuth) return 'OAuth' if (authMode === AuthMode.ApiKey) return 'API Key' if (authMode === AuthMode.BotToken) return 'Bot Token' return undefined } /** * Gets all available models from PROVIDER_DEFINITIONS as static options. * This provides fallback data when store state is not available server-side. * Excludes dynamic providers (ollama, ollama-cloud, vllm, openrouter, fireworks) which require runtime fetching. */ function getStaticModelOptions(): { id: string; label?: string }[] { const models: { id: string; label?: string }[] = [] for (const provider of Object.values(PROVIDER_DEFINITIONS)) { // Skip providers with dynamic/fetched models if ( provider.id === 'ollama' || provider.id === 'ollama-cloud' || provider.id === 'vllm' || provider.id === 'openrouter' || provider.id === 'fireworks' || provider.id === 'together' || provider.id === 'baseten' ) { continue } if (provider?.models) { for (const model of provider.models) { models.push({ id: model.id, label: model.id }) } } } return models } /** * Attempts to call a dynamic options function with fallback data injected. * When the function accesses store state that's unavailable server-side, * this provides static fallback data from known sources. * * @param optionsFn - The options function to call * @returns Options array or undefined if options cannot be resolved */ function callOptionsWithFallback( optionsFn: () => any[] ): { id: string; label?: string; hasIcon?: boolean }[] | undefined { // Get static model data to use as fallback const staticModels = getStaticModelOptions() // Create a mock providers state with static data const mockProvidersState = { providers: { base: { models: staticModels.map((m) => m.id) }, ollama: { models: [] }, 'ollama-cloud': { models: [] }, vllm: { models: [] }, litellm: { models: [] }, openrouter: { models: [] }, fireworks: { models: [] }, together: { models: [] }, baseten: { models: [] }, }, } // Store original getState if it exists let originalGetState: (() => any) | undefined let store: any try { // Try to get the providers store module // eslint-disable-next-line @typescript-eslint/no-require-imports store = require('@/stores/providers') if (store?.useProvidersStore?.getState) { originalGetState = store.useProvidersStore.getState // Temporarily replace getState with our mock store.useProvidersStore.getState = () => mockProvidersState } } catch { // Store module not available, continue with mock } try { const result = optionsFn() return result } finally { // Restore original getState if (store?.useProvidersStore && originalGetState) { store.useProvidersStore.getState = originalGetState } } } function resolveSubblockOptions( sb: any ): { id: string; label?: string; hasIcon?: boolean }[] | undefined { // Skip if subblock uses fetchOptions (async network calls) if (sb.fetchOptions) { return undefined } let rawOptions: any[] | undefined try { if (typeof sb.options === 'function') { // Try calling with fallback data injection for store-dependent options rawOptions = callOptionsWithFallback(sb.options) } else { rawOptions = sb.options } } catch { // Options function failed even with fallback, skip return undefined } if (!Array.isArray(rawOptions) || rawOptions.length === 0) { return undefined } const normalized = rawOptions .map((opt: any) => { if (!opt) return undefined const id = typeof opt === 'object' ? opt.id : opt if (id === undefined || id === null) return undefined const result: { id: string; label?: string; hasIcon?: boolean } = { id: String(id), } if (typeof opt === 'object' && typeof opt.label === 'string') { result.label = opt.label } if (typeof opt === 'object' && opt.icon) { result.hasIcon = true } return result }) .filter((o): o is { id: string; label?: string; hasIcon?: boolean } => o !== undefined) return normalized.length > 0 ? normalized : undefined } function removeNullish(obj: any): any { if (!obj || typeof obj !== 'object') return obj const cleaned: any = Array.isArray(obj) ? [] : {} for (const [key, value] of Object.entries(obj)) { if (value !== null && value !== undefined) { cleaned[key] = value } } return cleaned } function normalizeCondition(condition: any): any | undefined { try { if (!condition) return undefined if (typeof condition === 'function') { return condition() } return condition } catch { return undefined } } function splitParametersByOperation( subBlocks: any[], blockInputsForDescriptions?: Record ): { commonParameters: CopilotSubblockMetadata[] operationParameters: Record } { const commonParameters: CopilotSubblockMetadata[] = [] const operationParameters: Record = {} for (const sb of subBlocks || []) { const cond = normalizeCondition(sb.condition) const processed = processSubBlock(sb) if (cond && cond.field === 'operation' && !cond.not && cond.value !== undefined) { const values: any[] = Array.isArray(cond.value) ? cond.value : [cond.value] for (const v of values) { const key = String(v) if (!operationParameters[key]) operationParameters[key] = [] operationParameters[key].push(processed) } } else { // Override description from inputDefinitions if available (by id or canonicalParamId) if (blockInputsForDescriptions) { const candidates = [sb.id, sb.canonicalParamId].filter(Boolean) for (const key of candidates) { const bi = (blockInputsForDescriptions as any)[key as string] if (bi && typeof bi.description === 'string') { processed.description = bi.description break } } } commonParameters.push(processed) } } return { commonParameters, operationParameters } } function computeBlockLevelInputs(blockConfig: BlockConfig): Record { const inputs = blockConfig.inputs || {} const subBlocks: any[] = Array.isArray(blockConfig.subBlocks) ? blockConfig.subBlocks.filter((sb) => sb.mode !== 'trigger' && sb.mode !== 'trigger-advanced') : [] const byParamKey: Record = {} for (const sb of subBlocks) { if (sb.id) { byParamKey[sb.id] = byParamKey[sb.id] || [] byParamKey[sb.id].push(sb) } if (sb.canonicalParamId) { byParamKey[sb.canonicalParamId] = byParamKey[sb.canonicalParamId] || [] byParamKey[sb.canonicalParamId].push(sb) } } const blockInputs: Record = {} for (const key of Object.keys(inputs)) { const sbs = byParamKey[key] || [] const isOperationGated = sbs.some((sb) => { const cond = normalizeCondition(sb.condition) return cond && cond.field === 'operation' && !cond.not && cond.value !== undefined }) if (!isOperationGated) { blockInputs[key] = inputs[key] } } return blockInputs } function computeOperationLevelInputs( blockConfig: BlockConfig ): Record> { const inputs = blockConfig.inputs || {} const subBlocks = Array.isArray(blockConfig.subBlocks) ? blockConfig.subBlocks.filter((sb) => sb.mode !== 'trigger' && sb.mode !== 'trigger-advanced') : [] const opInputs: Record> = {} for (const sb of subBlocks) { const cond = normalizeCondition(sb.condition) if (!cond || cond.field !== 'operation' || cond.not) continue const keys: string[] = [] if (sb.canonicalParamId) keys.push(sb.canonicalParamId) if (sb.id) keys.push(sb.id) const values = Array.isArray(cond.value) ? cond.value : [cond.value] for (const key of keys) { if (!(key in inputs)) continue for (const v of values) { const op = String(v) if (!opInputs[op]) opInputs[op] = {} opInputs[op][key] = inputs[key] } } } return opInputs } function resolveOperationIds( blockConfig: BlockConfig, operationParameters: Record ): string[] { const opBlock = (blockConfig.subBlocks || []).find((sb) => sb.id === 'operation') if (opBlock && Array.isArray(opBlock.options)) { const ids = opBlock.options.map((o) => o.id).filter(Boolean) if (ids.length > 0) return ids } return Object.keys(operationParameters) } function resolveToolIdForOperation(blockConfig: BlockConfig, opId: string): string | undefined { try { const toolSelector = blockConfig.tools?.config?.tool if (typeof toolSelector === 'function') { const maybeToolId = toolSelector({ operation: opId }) if (typeof maybeToolId === 'string') return maybeToolId } } catch (error) { const toolLogger = createLogger('GetBlocksMetadataServerTool') toolLogger.warn('Failed to resolve tool ID for operation', { error: toError(error).message, }) } return undefined } const DOCS_FILE_MAPPING: Record = {} const SPECIAL_BLOCKS_METADATA: Record = { loop: { id: 'loop', name: 'Loop', description: 'Control flow block for iterating over collections or repeating actions', longDescription: 'Control flow block for iterating over collections or repeating actions serially', bestPractices: ` - Set reasonable limits for iterations. - Use forEach for collection processing, for loops for fixed iterations. - Cannot have loops/parallels inside a loop block. - For yaml it needs to connect blocks inside to the start field of the block. - IMPORTANT for while/doWhile: The condition is evaluated BEFORE each iteration starts, so blocks INSIDE the loop cannot be referenced in the condition (their outputs don't exist yet when the condition runs). - For while/doWhile conditions, use: for iteration count, workflow variables (set by blocks OUTSIDE the loop), or references to blocks OUTSIDE the loop. - To break a while/doWhile loop based on internal block results, use a variables block OUTSIDE the loop and update it from inside, then reference that variable in the condition. `, inputs: { loopType: { type: 'string', required: true, enum: ['for', 'forEach', 'while', 'doWhile'], description: "Loop Type - 'for' runs N times, 'forEach' iterates over collection, 'while' runs while condition is true, 'doWhile' runs at least once then checks condition", }, iterations: { type: 'number', required: false, minimum: 1, maximum: 1000, description: "Number of iterations (for 'for' loopType)", example: 5, }, collection: { type: 'string', required: false, description: "Collection to iterate over (for 'forEach' loopType)", example: '', }, condition: { type: 'string', required: false, description: "Condition to evaluate (for 'while' and 'doWhile' loopType). IMPORTANT: Cannot reference blocks INSIDE the loop - use , workflow variables, or blocks OUTSIDE the loop instead.", example: ' < 10', }, maxConcurrency: { type: 'number', required: false, default: 1, minimum: 1, maximum: 10, description: 'Max parallel executions (1 = sequential)', example: 1, }, }, outputs: { results: { type: 'array', description: 'Array of results from each iteration' }, currentIndex: { type: 'number', description: 'Current iteration index (0-based)' }, currentItem: { type: 'any', description: 'Current item being iterated (for forEach loops)' }, totalIterations: { type: 'number', description: 'Total number of iterations' }, }, subBlocks: [ { id: 'loopType', title: 'Loop Type', type: 'dropdown', required: true, options: [ { label: 'For Loop (count)', id: 'for' }, { label: 'For Each (collection)', id: 'forEach' }, { label: 'While (condition)', id: 'while' }, { label: 'Do While (condition)', id: 'doWhile' }, ], }, { id: 'iterations', title: 'Iterations', type: 'slider', min: 1, max: 1000, integer: true, condition: { field: 'loopType', value: 'for' }, }, { id: 'collection', title: 'Collection', type: 'short-input', placeholder: 'Array or object to iterate over...', condition: { field: 'loopType', value: 'forEach' }, }, { id: 'condition', title: 'Condition', type: 'code', language: 'javascript', placeholder: ' < 10 or ', description: 'Cannot reference blocks inside the loop. Use , workflow variables, or blocks outside the loop.', condition: { field: 'loopType', value: ['while', 'doWhile'] }, }, { id: 'maxConcurrency', title: 'Max Concurrency', type: 'slider', min: 1, max: 10, integer: true, default: 1, }, ], }, parallel: { id: 'parallel', name: 'Parallel', description: 'Control flow block for executing multiple branches simultaneously', longDescription: 'Control flow block for executing multiple branches simultaneously', bestPractices: ` - Keep structures inside simple. Cannot have multiple blocks within a parallel block. - Cannot have loops/parallels inside a parallel block. - Agent block combobox can be if the user wants to query multiple models in parallel. The collection has to be an array of correct model strings available for the agent block. - For yaml it needs to connect blocks inside to the start field of the block. `, inputs: { parallelType: { type: 'string', required: true, enum: ['count', 'collection'], description: "Parallel Type - 'count' runs N branches, 'collection' runs one per item", }, count: { type: 'number', required: false, minimum: 1, maximum: 100, description: "Number of parallel branches (for 'count' type)", example: 3, }, collection: { type: 'string', required: false, description: "Collection to process in parallel (for 'collection' type)", example: '', }, maxConcurrency: { type: 'number', required: false, default: 10, minimum: 1, maximum: 50, description: 'Max concurrent executions at once', example: 10, }, }, outputs: { results: { type: 'array', description: 'Array of results from all parallel branches' }, index: { type: 'number', description: 'Current branch index (0-based)' }, currentItem: { type: 'any', description: 'Current item for this branch (for collection type)', }, items: { type: 'array', description: 'All distribution items' }, }, subBlocks: [ { id: 'parallelType', title: 'Parallel Type', type: 'dropdown', required: true, options: [ { label: 'Count (number)', id: 'count' }, { label: 'Collection (array)', id: 'collection' }, ], }, { id: 'count', title: 'Count', type: 'slider', min: 1, max: 100, integer: true, condition: { field: 'parallelType', value: 'count' }, }, { id: 'collection', title: 'Collection', type: 'short-input', placeholder: 'Array to process in parallel...', condition: { field: 'parallelType', value: 'collection' }, }, { id: 'maxConcurrency', title: 'Max Concurrency', type: 'slider', min: 1, max: 50, integer: true, default: 10, }, ], }, }