diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx index 16b8e60d6..4e7e975ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx @@ -4,11 +4,7 @@ import type React from 'react' import { useMemo } from 'react' import { RepeatIcon, SplitIcon } from 'lucide-react' import { Combobox, type ComboboxOptionGroup } from '@/components/emcn' -import { - extractFieldsFromSchema, - parseResponseFormatSafely, -} from '@/lib/core/utils/response-format' -import { getToolOutputs } from '@/lib/workflows/blocks/block-outputs' +import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { getBlock } from '@/blocks' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -124,42 +120,25 @@ export function OutputSelect({ : `block-${block.id}` const blockConfig = getBlock(block.type) - const responseFormatValue = - shouldUseBaseline && baselineWorkflow - ? baselineWorkflow.blocks?.[block.id]?.subBlocks?.responseFormat?.value - : subBlockValues?.[block.id]?.responseFormat - const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id) let outputsToProcess: Record = {} - - if (responseFormat) { - const schemaFields = extractFieldsFromSchema(responseFormat) - if (schemaFields.length > 0) { - schemaFields.forEach((field) => { - outputsToProcess[field.name] = { type: field.type } - }) - } else { - outputsToProcess = blockConfig?.outputs || {} + const rawSubBlockValues = + shouldUseBaseline && baselineWorkflow + ? baselineWorkflow.blocks?.[block.id]?.subBlocks + : subBlockValues?.[block.id] + const subBlocks: Record = {} + if (rawSubBlockValues && typeof rawSubBlockValues === 'object') { + for (const [key, val] of Object.entries(rawSubBlockValues)) { + // Handle both { value: ... } and raw value formats + subBlocks[key] = val && typeof val === 'object' && 'value' in val ? val : { value: val } } - } else { - // Build subBlocks object for tool selector - const rawSubBlockValues = - shouldUseBaseline && baselineWorkflow - ? baselineWorkflow.blocks?.[block.id]?.subBlocks - : subBlockValues?.[block.id] - const subBlocks: Record = {} - if (rawSubBlockValues && typeof rawSubBlockValues === 'object') { - for (const [key, val] of Object.entries(rawSubBlockValues)) { - // Handle both { value: ... } and raw value formats - subBlocks[key] = val && typeof val === 'object' && 'value' in val ? val : { value: val } - } - } - - const toolOutputs = blockConfig ? getToolOutputs(blockConfig, subBlocks) : {} - outputsToProcess = - Object.keys(toolOutputs).length > 0 ? toolOutputs : blockConfig?.outputs || {} } + outputsToProcess = getEffectiveBlockOutputs(block.type, subBlocks, { + triggerMode: Boolean(block.triggerMode), + preferToolOutputs: !block.triggerMode, + }) as Record + if (Object.keys(outputsToProcess).length === 0) return const addOutput = (path: string, outputObj: unknown, prefix = '') => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx index cf6baf554..b2a32bb3a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx @@ -61,8 +61,6 @@ function ConnectionItem({ blockId: connection.id, blockType: connection.type, mergedSubBlocks, - responseFormat: connection.responseFormat, - operation: connection.operation, triggerMode: sourceBlock?.triggerMode, }) const hasFields = fields.length > 0 diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx index fbec4fe0a..ac63cdfd8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx @@ -14,15 +14,9 @@ import { } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import { - extractFieldsFromSchema, - parseResponseFormatSafely, -} from '@/lib/core/utils/response-format' -import { - getBlockOutputPaths, - getBlockOutputType, + getEffectiveBlockOutputPaths, + getEffectiveBlockOutputType, getOutputPathsFromSchema, - getToolOutputPaths, - getToolOutputType, } from '@/lib/workflows/blocks/block-outputs' import { TRIGGER_TYPES } from '@/lib/workflows/triggers/triggers' import { KeyboardNavigationHandler } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler' @@ -214,43 +208,18 @@ const getOutputTypeForPath = ( outputPath: string, mergedSubBlocksOverride?: Record ): string => { - if (block?.triggerMode && blockConfig?.triggers?.enabled) { - return getBlockOutputType(block.type, outputPath, mergedSubBlocksOverride, true) - } - if (block?.type === 'starter') { - const startWorkflowValue = - mergedSubBlocksOverride?.startWorkflow?.value ?? getSubBlockValue(blockId, 'startWorkflow') - - if (startWorkflowValue === 'chat') { - const chatModeTypes: Record = { - input: 'string', - conversationId: 'string', - files: 'file[]', - } - return chatModeTypes[outputPath] || 'any' - } - const inputFormatValue = - mergedSubBlocksOverride?.inputFormat?.value ?? getSubBlockValue(blockId, 'inputFormat') - if (inputFormatValue && Array.isArray(inputFormatValue)) { - const field = inputFormatValue.find( - (f: { name?: string; type?: string }) => f.name === outputPath - ) - if (field?.type) return field.type - } - } else if (blockConfig?.category === 'triggers') { - const blockState = useWorkflowStore.getState().blocks[blockId] - const subBlocks = mergedSubBlocksOverride ?? (blockState?.subBlocks || {}) - return getBlockOutputType(block.type, outputPath, subBlocks) - } else if (blockConfig?.tools?.config?.tool) { - const blockState = useWorkflowStore.getState().blocks[blockId] - const subBlocks = mergedSubBlocksOverride ?? (blockState?.subBlocks || {}) - return getToolOutputType(blockConfig, subBlocks, outputPath) + if (block?.type === 'variables') { + return 'any' } const subBlocks = mergedSubBlocksOverride ?? useWorkflowStore.getState().blocks[blockId]?.subBlocks - const triggerMode = block?.triggerMode && blockConfig?.triggers?.enabled - return getBlockOutputType(block?.type ?? '', outputPath, subBlocks, triggerMode) + const triggerMode = Boolean(block?.triggerMode && blockConfig?.triggers?.enabled) + + return getEffectiveBlockOutputType(block?.type ?? '', outputPath, subBlocks, { + triggerMode, + preferToolOutputs: !triggerMode, + }) } /** @@ -1088,24 +1057,9 @@ export const TagDropdown: React.FC = ({ const normalizedBlockName = normalizeName(blockName) const mergedSubBlocks = getMergedSubBlocks(activeSourceBlockId) - const responseFormatValue = mergedSubBlocks?.responseFormat?.value - const responseFormat = parseResponseFormatSafely(responseFormatValue, activeSourceBlockId) - let blockTags: string[] - if (sourceBlock.type === 'evaluator') { - const metricsValue = getSubBlockValue(activeSourceBlockId, 'metrics') - - if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) { - const validMetrics = metricsValue.filter((metric: { name?: string }) => metric?.name) - blockTags = validMetrics.map( - (metric: { name: string }) => `${normalizedBlockName}.${metric.name.toLowerCase()}` - ) - } else { - const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } - } else if (sourceBlock.type === 'variables') { + if (sourceBlock.type === 'variables') { const variablesValue = getSubBlockValue(activeSourceBlockId, 'variables') if (variablesValue && Array.isArray(variablesValue) && variablesValue.length > 0) { @@ -1119,106 +1073,21 @@ export const TagDropdown: React.FC = ({ } else { blockTags = [normalizedBlockName] } - } else if (responseFormat) { - const schemaFields = extractFieldsFromSchema(responseFormat) - if (schemaFields.length > 0) { - blockTags = schemaFields.map((field) => `${normalizedBlockName}.${field.name}`) - } else { - const outputPaths = getBlockOutputPaths( - sourceBlock.type, - mergedSubBlocks, - sourceBlock.triggerMode - ) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } - } else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) { - if (sourceBlock.type === 'starter') { - const startWorkflowValue = mergedSubBlocks?.startWorkflow?.value - - if (startWorkflowValue === 'chat') { - blockTags = [ - `${normalizedBlockName}.input`, - `${normalizedBlockName}.conversationId`, - `${normalizedBlockName}.files`, - ] - } else { - const inputFormatValue = mergedSubBlocks?.inputFormat?.value - - if ( - inputFormatValue && - Array.isArray(inputFormatValue) && - inputFormatValue.length > 0 - ) { - blockTags = inputFormatValue - .filter((field: { name?: string }) => field.name && field.name.trim() !== '') - .map((field: { name: string }) => `${normalizedBlockName}.${field.name}`) - } else { - blockTags = [normalizedBlockName] - } - } - } else if (sourceBlock.type === 'api_trigger' || sourceBlock.type === 'input_trigger') { - const inputFormatValue = mergedSubBlocks?.inputFormat?.value - - if (inputFormatValue && Array.isArray(inputFormatValue) && inputFormatValue.length > 0) { - blockTags = inputFormatValue - .filter((field: { name?: string }) => field.name && field.name.trim() !== '') - .map((field: { name: string }) => `${normalizedBlockName}.${field.name}`) - } else { - blockTags = [] - } - } else { - blockTags = [normalizedBlockName] - } } else { - if (blockConfig.category === 'triggers' || sourceBlock.type === 'starter') { - const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks) - if (dynamicOutputs.length > 0) { - blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - } else if (sourceBlock.type === 'starter') { - blockTags = [normalizedBlockName] - } else if (sourceBlock.type === TRIGGER_TYPES.GENERIC_WEBHOOK) { - blockTags = [normalizedBlockName] - } else { - blockTags = [] - } - } else if (sourceBlock?.triggerMode && blockConfig.triggers?.enabled) { - const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks, true) - if (dynamicOutputs.length > 0) { - blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - } else { - const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks, true) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } - } else if (sourceBlock.type === 'human_in_the_loop') { - const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks) + const outputPaths = getEffectiveBlockOutputPaths(sourceBlock.type, mergedSubBlocks, { + triggerMode: Boolean(sourceBlock.triggerMode), + preferToolOutputs: !sourceBlock.triggerMode, + }) + const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - const isSelfReference = activeSourceBlockId === blockId - - if (dynamicOutputs.length > 0) { - const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - blockTags = isSelfReference - ? allTags.filter((tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint')) - : allTags - } else { - const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks) - const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - blockTags = isSelfReference - ? allTags.filter((tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint')) - : allTags - } + if (sourceBlock.type === 'human_in_the_loop' && activeSourceBlockId === blockId) { + blockTags = allTags.filter( + (tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint') + ) + } else if (allTags.length === 0) { + blockTags = [normalizedBlockName] } else { - const toolOutputPaths = getToolOutputPaths(blockConfig, mergedSubBlocks) - - if (toolOutputPaths.length > 0) { - blockTags = toolOutputPaths.map((path) => `${normalizedBlockName}.${path}`) - } else { - const outputPaths = getBlockOutputPaths( - sourceBlock.type, - mergedSubBlocks, - sourceBlock.triggerMode - ) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } + blockTags = allTags } } @@ -1432,45 +1301,10 @@ export const TagDropdown: React.FC = ({ const normalizedBlockName = normalizeName(blockName) const mergedSubBlocks = getMergedSubBlocks(accessibleBlockId) - const responseFormatValue = mergedSubBlocks?.responseFormat?.value - const responseFormat = parseResponseFormatSafely(responseFormatValue, accessibleBlockId) let blockTags: string[] - if (blockConfig.category === 'triggers' || accessibleBlock.type === 'starter') { - const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks) - - if (dynamicOutputs.length > 0) { - blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - } else if (accessibleBlock.type === 'starter') { - const startWorkflowValue = mergedSubBlocks?.startWorkflow?.value - if (startWorkflowValue === 'chat') { - blockTags = [ - `${normalizedBlockName}.input`, - `${normalizedBlockName}.conversationId`, - `${normalizedBlockName}.files`, - ] - } else { - blockTags = [normalizedBlockName] - } - } else if (accessibleBlock.type === TRIGGER_TYPES.GENERIC_WEBHOOK) { - blockTags = [normalizedBlockName] - } else { - blockTags = [] - } - } else if (accessibleBlock.type === 'evaluator') { - const metricsValue = getSubBlockValue(accessibleBlockId, 'metrics') - - if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) { - const validMetrics = metricsValue.filter((metric: { name?: string }) => metric?.name) - blockTags = validMetrics.map( - (metric: { name: string }) => `${normalizedBlockName}.${metric.name.toLowerCase()}` - ) - } else { - const outputPaths = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } - } else if (accessibleBlock.type === 'variables') { + if (accessibleBlock.type === 'variables') { const variablesValue = getSubBlockValue(accessibleBlockId, 'variables') if (variablesValue && Array.isArray(variablesValue) && variablesValue.length > 0) { @@ -1484,57 +1318,21 @@ export const TagDropdown: React.FC = ({ } else { blockTags = [normalizedBlockName] } - } else if (responseFormat) { - const schemaFields = extractFieldsFromSchema(responseFormat) - if (schemaFields.length > 0) { - blockTags = schemaFields.map((field) => `${normalizedBlockName}.${field.name}`) - } else { - const outputPaths = getBlockOutputPaths( - accessibleBlock.type, - mergedSubBlocks, - accessibleBlock.triggerMode - ) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } - } else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) { - blockTags = [normalizedBlockName] } else { - const blockState = blocks[accessibleBlockId] - if (blockState?.triggerMode && blockConfig.triggers?.enabled) { - const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks, true) - if (dynamicOutputs.length > 0) { - blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - } else { - const outputPaths = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks, true) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } - } else if (accessibleBlock.type === 'human_in_the_loop') { - const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks) + const outputPaths = getEffectiveBlockOutputPaths(accessibleBlock.type, mergedSubBlocks, { + triggerMode: Boolean(accessibleBlock.triggerMode), + preferToolOutputs: !accessibleBlock.triggerMode, + }) + const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - const isSelfReference = accessibleBlockId === blockId - - if (dynamicOutputs.length > 0) { - const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - blockTags = isSelfReference - ? allTags.filter((tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint')) - : allTags - } else { - blockTags = [`${normalizedBlockName}.url`, `${normalizedBlockName}.resumeEndpoint`] - } + if (accessibleBlock.type === 'human_in_the_loop' && accessibleBlockId === blockId) { + blockTags = allTags.filter( + (tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint') + ) + } else if (allTags.length === 0) { + blockTags = [normalizedBlockName] } else { - const toolOutputPaths = getToolOutputPaths(blockConfig, mergedSubBlocks) - - if (toolOutputPaths.length > 0) { - blockTags = toolOutputPaths.map((path) => `${normalizedBlockName}.${path}`) - } else { - const outputPaths = getBlockOutputPaths( - accessibleBlock.type, - mergedSubBlocks, - accessibleBlock.triggerMode - ) - - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } + blockTags = allTags } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-block-connections.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-block-connections.ts index a61ee158a..ae3c4f0fa 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-block-connections.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-block-connections.ts @@ -1,9 +1,5 @@ import { useShallow } from 'zustand/react/shallow' -import { - extractFieldsFromSchema, - parseResponseFormatSafely, -} from '@/lib/core/utils/response-format' -import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' +import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -20,18 +16,7 @@ export interface ConnectedBlock { type: string outputType: string | string[] name: string - responseFormat?: { - // Support both formats - fields?: Field[] - name?: string - schema?: { - type: string - properties: Record - required?: string[] - } - } outputs?: Record - operation?: string } export function useBlockConnections(blockId: string) { @@ -103,46 +88,28 @@ export function useBlockConnections(blockId: string) { // Get merged subblocks for this source block const mergedSubBlocks = getMergedSubBlocks(sourceId) - // Get the response format from the subblock store - const responseFormatValue = useSubBlockStore.getState().getValue(sourceId, 'responseFormat') + const blockOutputs = getEffectiveBlockOutputs(sourceBlock.type, mergedSubBlocks, { + triggerMode: Boolean(sourceBlock.triggerMode), + preferToolOutputs: !sourceBlock.triggerMode, + }) - // Safely parse response format with proper error handling - const responseFormat = parseResponseFormatSafely(responseFormatValue, sourceId) - - // Get operation value for tool-based blocks - const operationValue = useSubBlockStore.getState().getValue(sourceId, 'operation') - - // Use getBlockOutputs to properly handle dynamic outputs from inputFormat - const blockOutputs = getBlockOutputs( - sourceBlock.type, - mergedSubBlocks, - sourceBlock.triggerMode - ) - - // Extract fields from the response format if available, otherwise use block outputs - let outputFields: Field[] - if (responseFormat) { - outputFields = extractFieldsFromSchema(responseFormat) - } else { - // Convert block outputs to field format - outputFields = Object.entries(blockOutputs).map(([key, value]: [string, any]) => ({ + const outputFields: Field[] = Object.entries(blockOutputs).map( + ([key, value]: [string, any]) => ({ name: key, type: value && typeof value === 'object' && 'type' in value ? value.type : 'string', description: value && typeof value === 'object' && 'description' in value ? value.description : undefined, - })) - } + }) + ) return { id: sourceBlock.id, type: sourceBlock.type, outputType: outputFields.map((field: Field) => field.name), name: sourceBlock.name, - responseFormat, outputs: blockOutputs, - operation: operationValue, distance: nodeDistances.get(sourceId) || Number.POSITIVE_INFINITY, } }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields.ts index 233f06e58..e6cc8720a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields.ts @@ -1,13 +1,10 @@ 'use client' import { useMemo } from 'react' -import { extractFieldsFromSchema } from '@/lib/core/utils/response-format' import { - getBlockOutputPaths, - getBlockOutputs, - getToolOutputs, + getEffectiveBlockOutputs, + getEvaluatorMetricOutputs, } from '@/lib/workflows/blocks/block-outputs' -import { TRIGGER_TYPES } from '@/lib/workflows/triggers/triggers' import type { SchemaField } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/components/field-item/field-item' import { getBlock } from '@/blocks' import { useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -76,11 +73,7 @@ const extractNestedFields = (properties: Record): SchemaField[] => /** * Creates a schema field from an output definition */ -const createFieldFromOutput = ( - name: string, - output: any, - responseFormatFields?: SchemaField[] -): SchemaField => { +const createFieldFromOutput = (name: string, output: any): SchemaField => { const hasExplicitType = isObject(output) && typeof output.type === 'string' const type = hasExplicitType ? output.type : isObject(output) ? 'object' : 'string' @@ -90,11 +83,7 @@ const createFieldFromOutput = ( description: isObject(output) && 'description' in output ? output.description : undefined, } - if (name === 'data' && responseFormatFields && responseFormatFields.length > 0) { - field.children = responseFormatFields - } else { - field.children = extractChildFields(output) - } + field.children = extractChildFields(output) return field } @@ -103,8 +92,6 @@ interface UseBlockOutputFieldsParams { blockId: string blockType: string mergedSubBlocks?: Record - responseFormat?: any - operation?: string triggerMode?: boolean } @@ -116,8 +103,6 @@ export function useBlockOutputFields({ blockId, blockType, mergedSubBlocks, - responseFormat, - operation, triggerMode, }: UseBlockOutputFieldsParams): SchemaField[] { return useMemo(() => { @@ -140,14 +125,24 @@ export function useBlockOutputFields({ // Handle evaluator blocks - use metrics if available if (blockType === 'evaluator') { - const metricsValue = mergedSubBlocks?.metrics?.value ?? getSubBlockValue(blockId, 'metrics') - - if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) { - const validMetrics = metricsValue.filter((metric: { name?: string }) => metric?.name) - return validMetrics.map((metric: { name: string }) => ({ - name: metric.name.toLowerCase(), - type: 'number', - description: `Metric: ${metric.name}`, + const metricOutputs = getEvaluatorMetricOutputs(mergedSubBlocks) + if (metricOutputs) { + return Object.entries(metricOutputs).map(([name, output]) => ({ + name, + type: + output && + typeof output === 'object' && + 'type' in output && + typeof output.type === 'string' + ? output.type + : 'number', + description: + output && + typeof output === 'object' && + 'description' in output && + typeof output.description === 'string' + ? output.description + : undefined, })) } // Fall through to use blockConfig.outputs @@ -172,123 +167,14 @@ export function useBlockOutputFields({ return [] } - // Get base outputs using getBlockOutputs (handles triggers, starter, approval, etc.) - let baseOutputs: Record = {} - - if (blockConfig.category === 'triggers' || blockType === 'starter') { - // Use getBlockOutputPaths to get dynamic outputs, then reconstruct the structure - const outputPaths = getBlockOutputPaths(blockType, mergedSubBlocks, triggerMode) - if (outputPaths.length > 0) { - // Reconstruct outputs structure from paths - // This is a simplified approach - we'll use the paths to build the structure - baseOutputs = getBlockOutputs(blockType, mergedSubBlocks, triggerMode) - } else if (blockType === 'starter') { - const startWorkflowValue = mergedSubBlocks?.startWorkflow?.value - if (startWorkflowValue === 'chat') { - baseOutputs = { - input: { type: 'string', description: 'User message' }, - conversationId: { type: 'string', description: 'Conversation ID' }, - files: { type: 'file[]', description: 'Uploaded files' }, - } - } else { - const inputFormatValue = mergedSubBlocks?.inputFormat?.value - if (inputFormatValue && Array.isArray(inputFormatValue) && inputFormatValue.length > 0) { - baseOutputs = {} - inputFormatValue.forEach((field: { name?: string; type?: string }) => { - if (field.name && field.name.trim() !== '') { - baseOutputs[field.name] = { - type: field.type || 'string', - description: `Field from input format`, - } - } - }) - } - } - } else if (blockType === TRIGGER_TYPES.GENERIC_WEBHOOK) { - // Generic webhook returns the whole payload - baseOutputs = {} - } else { - baseOutputs = {} - } - } else if (triggerMode && blockConfig.triggers?.enabled) { - // Trigger mode enabled - const dynamicOutputs = getBlockOutputPaths(blockType, mergedSubBlocks, true) - if (dynamicOutputs.length > 0) { - baseOutputs = getBlockOutputs(blockType, mergedSubBlocks, true) - } else { - baseOutputs = blockConfig.outputs || {} - } - } else if (blockType === 'approval') { - // Approval block uses dynamic outputs from inputFormat - baseOutputs = getBlockOutputs(blockType, mergedSubBlocks) - } else { - // For tool-based blocks, try to get tool outputs first - const toolOutputs = blockConfig ? getToolOutputs(blockConfig, mergedSubBlocks) : {} - - if (Object.keys(toolOutputs).length > 0) { - baseOutputs = toolOutputs - } else { - baseOutputs = getBlockOutputs(blockType, mergedSubBlocks, triggerMode) - } - } - - // Handle responseFormat - const responseFormatFields = responseFormat ? extractFieldsFromSchema(responseFormat) : [] - - // If responseFormat exists and has fields, merge with base outputs - if (responseFormatFields.length > 0) { - // If base outputs is empty, use responseFormat fields directly - if (Object.keys(baseOutputs).length === 0) { - return responseFormatFields.map((field) => ({ - name: field.name, - type: field.type, - description: field.description, - children: undefined, // ResponseFormat fields are flat - })) - } - - // Otherwise, merge: responseFormat takes precedence for 'data' field - const fields: SchemaField[] = [] - const responseFormatFieldNames = new Set(responseFormatFields.map((f) => f.name)) - - // Add base outputs, replacing 'data' with responseFormat fields if present - for (const [name, output] of Object.entries(baseOutputs)) { - if (name === 'data' && responseFormatFields.length > 0) { - fields.push( - createFieldFromOutput( - name, - output, - responseFormatFields.map((f) => ({ - name: f.name, - type: f.type, - description: f.description, - })) - ) - ) - } else if (!responseFormatFieldNames.has(name)) { - fields.push(createFieldFromOutput(name, output)) - } - } - - // Add responseFormat fields that aren't in base outputs - for (const field of responseFormatFields) { - if (!baseOutputs[field.name]) { - fields.push({ - name: field.name, - type: field.type, - description: field.description, - }) - } - } - - return fields - } - - // No responseFormat, just use base outputs + const baseOutputs = getEffectiveBlockOutputs(blockType, mergedSubBlocks, { + triggerMode: Boolean(triggerMode), + preferToolOutputs: !triggerMode, + }) as Record if (Object.keys(baseOutputs).length === 0) { return [] } return Object.entries(baseOutputs).map(([name, output]) => createFieldFromOutput(name, output)) - }, [blockId, blockType, mergedSubBlocks, responseFormat, operation, triggerMode]) + }, [blockId, blockType, mergedSubBlocks, triggerMode]) } diff --git a/apps/sim/executor/utils/block-data.ts b/apps/sim/executor/utils/block-data.ts index a5ef28d99..63cb07d60 100644 --- a/apps/sim/executor/utils/block-data.ts +++ b/apps/sim/executor/utils/block-data.ts @@ -1,8 +1,4 @@ -import { - extractFieldsFromSchema, - parseResponseFormatSafely, -} from '@/lib/core/utils/response-format' -import { normalizeInputFormatValue } from '@/lib/workflows/input-format' +import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { isTriggerBehavior, normalizeName } from '@/executor/constants' import type { ExecutionContext } from '@/executor/types' import type { OutputSchema } from '@/executor/utils/block-reference' @@ -12,8 +8,6 @@ import { isBranchNodeId, } from '@/executor/utils/subflow-utils' import type { SerializedBlock } from '@/serializer/types' -import type { ToolConfig } from '@/tools/types' -import { getTool } from '@/tools/utils' export interface BlockDataCollection { blockData: Record @@ -21,118 +15,42 @@ export interface BlockDataCollection { blockOutputSchemas: Record } -/** - * Block types where inputFormat fields should be merged into outputs schema. - * These are blocks where users define custom fields via inputFormat that become - * valid output paths (e.g., , , ). - * - * Note: This includes non-trigger blocks like 'starter' and 'human_in_the_loop' which - * have category 'blocks' but still need their inputFormat exposed as outputs. - */ -const BLOCKS_WITH_INPUT_FORMAT_OUTPUTS = [ - 'start_trigger', - 'starter', - 'api_trigger', - 'input_trigger', - 'generic_webhook', - 'human_in_the_loop', -] as const - -function getInputFormatFields(block: SerializedBlock): OutputSchema { - const inputFormat = normalizeInputFormatValue(block.config?.params?.inputFormat) - if (inputFormat.length === 0) { - return {} - } - - const schema: OutputSchema = {} - for (const field of inputFormat) { - if (!field.name) continue - schema[field.name] = { type: field.type || 'any' } - } - - return schema +interface SubBlockWithValue { + value?: unknown } -function getEvaluatorMetricsSchema(block: SerializedBlock): OutputSchema | undefined { - if (block.metadata?.id !== 'evaluator') return undefined +function paramsToSubBlocks( + params: Record | undefined +): Record { + if (!params) return {} - const metrics = block.config?.params?.metrics - if (!Array.isArray(metrics) || metrics.length === 0) return undefined - - const validMetrics = metrics.filter( - (m: { name?: string }) => m?.name && typeof m.name === 'string' - ) - if (validMetrics.length === 0) return undefined - - const schema: OutputSchema = { ...(block.outputs as OutputSchema) } - for (const metric of validMetrics) { - schema[metric.name.toLowerCase()] = { type: 'number' } + const subBlocks: Record = {} + for (const [key, value] of Object.entries(params)) { + subBlocks[key] = { value } } - return schema + return subBlocks } -function getResponseFormatSchema(block: SerializedBlock): OutputSchema | undefined { - const responseFormatValue = block.config?.params?.responseFormat - if (!responseFormatValue) return undefined - - const parsed = parseResponseFormatSafely(responseFormatValue, block.id) - if (!parsed) return undefined - - const fields = extractFieldsFromSchema(parsed) - if (fields.length === 0) return undefined - - const schema: OutputSchema = {} - for (const field of fields) { - schema[field.name] = { type: field.type || 'any' } - } - return schema -} - -export function getBlockSchema( - block: SerializedBlock, - toolConfig?: ToolConfig -): OutputSchema | undefined { +function getRegistrySchema(block: SerializedBlock): OutputSchema | undefined { const blockType = block.metadata?.id + if (!blockType) return undefined - if ( - blockType && - BLOCKS_WITH_INPUT_FORMAT_OUTPUTS.includes( - blockType as (typeof BLOCKS_WITH_INPUT_FORMAT_OUTPUTS)[number] - ) - ) { - const baseOutputs = (block.outputs as OutputSchema) || {} - const inputFormatFields = getInputFormatFields(block) - const merged = { ...baseOutputs, ...inputFormatFields } - if (Object.keys(merged).length > 0) { - return merged - } + const subBlocks = paramsToSubBlocks(block.config?.params) + const triggerMode = isTriggerBehavior(block) + const outputs = getEffectiveBlockOutputs(blockType, subBlocks, { + triggerMode, + preferToolOutputs: !triggerMode, + includeHidden: true, + }) as OutputSchema + + if (!outputs || Object.keys(outputs).length === 0) { + return undefined } + return outputs +} - const evaluatorSchema = getEvaluatorMetricsSchema(block) - if (evaluatorSchema) { - return evaluatorSchema - } - - const responseFormatSchema = getResponseFormatSchema(block) - if (responseFormatSchema) { - return responseFormatSchema - } - - const isTrigger = isTriggerBehavior(block) - - if (isTrigger && block.outputs && Object.keys(block.outputs).length > 0) { - return block.outputs as OutputSchema - } - - if (toolConfig?.outputs && Object.keys(toolConfig.outputs).length > 0) { - return toolConfig.outputs as OutputSchema - } - - if (block.outputs && Object.keys(block.outputs).length > 0) { - return block.outputs as OutputSchema - } - - return undefined +export function getBlockSchema(block: SerializedBlock): OutputSchema | undefined { + return getRegistrySchema(block) } export function collectBlockData( @@ -170,9 +88,7 @@ export function collectBlockData( blockNameMapping[normalizeName(block.metadata.name)] = id } - const toolId = block.config?.tool - const toolConfig = toolId ? getTool(toolId) : undefined - const schema = getBlockSchema(block, toolConfig) + const schema = getBlockSchema(block) if (schema && Object.keys(schema).length > 0) { blockOutputSchemas[id] = schema } diff --git a/apps/sim/executor/variables/resolvers/block.test.ts b/apps/sim/executor/variables/resolvers/block.test.ts index 01a804900..8673b0d48 100644 --- a/apps/sim/executor/variables/resolvers/block.test.ts +++ b/apps/sim/executor/variables/resolvers/block.test.ts @@ -6,10 +6,6 @@ import type { ResolutionContext } from './reference' vi.mock('@sim/logger', () => loggerMock) -vi.mock('@/lib/workflows/blocks/block-outputs', () => ({ - getBlockOutputs: vi.fn(() => ({})), -})) - function createTestWorkflow( blocks: Array<{ id: string @@ -144,34 +140,22 @@ describe('BlockResolver', () => { expect(resolver.resolve('', ctx)).toBeUndefined() }) - it.concurrent('should throw error for path not in output schema', async () => { - const { getBlockOutputs } = await import('@/lib/workflows/blocks/block-outputs') - const mockGetBlockOutputs = vi.mocked(getBlockOutputs) - const customOutputs = { - validField: { type: 'string', description: 'A valid field' }, - nested: { - child: { type: 'number', description: 'Nested child' }, - }, - } - mockGetBlockOutputs.mockReturnValue(customOutputs as any) - + it.concurrent('should throw error for path not in output schema', () => { const workflow = createTestWorkflow([ { id: 'source', - outputs: customOutputs, + type: 'start_trigger', }, ]) const resolver = new BlockResolver(workflow) const ctx = createTestContext('current', { - source: { validField: 'value', nested: { child: 42 } }, + source: { input: 'value' }, }) expect(() => resolver.resolve('', ctx)).toThrow( /"invalidField" doesn't exist on block "source"/ ) expect(() => resolver.resolve('', ctx)).toThrow(/Available fields:/) - - mockGetBlockOutputs.mockReturnValue({}) }) it.concurrent('should return undefined for path in schema but missing in data', () => { @@ -193,6 +177,59 @@ describe('BlockResolver', () => { expect(resolver.resolve('', ctx)).toBeUndefined() }) + it.concurrent( + 'should allow hiddenFromDisplay fields for pre-execution schema validation', + () => { + const workflow = createTestWorkflow([ + { + id: 'workflow-block', + name: 'Workflow', + type: 'workflow', + }, + ]) + const resolver = new BlockResolver(workflow) + const ctx = createTestContext('current', {}) + + expect(resolver.resolve('', ctx)).toBeUndefined() + } + ) + + it.concurrent( + 'should allow hiddenFromDisplay fields for workflow_input pre-execution schema validation', + () => { + const workflow = createTestWorkflow([ + { + id: 'workflow-input-block', + name: 'Workflow Input', + type: 'workflow_input', + }, + ]) + const resolver = new BlockResolver(workflow) + const ctx = createTestContext('current', {}) + + expect(resolver.resolve('', ctx)).toBeUndefined() + } + ) + + it.concurrent( + 'should allow hiddenFromDisplay fields for HITL pre-execution schema validation', + () => { + const workflow = createTestWorkflow([ + { + id: 'hitl-block', + name: 'HITL', + type: 'human_in_the_loop', + }, + ]) + const resolver = new BlockResolver(workflow) + const ctx = createTestContext('current', {}) + + expect(resolver.resolve('', ctx)).toBeUndefined() + expect(resolver.resolve('', ctx)).toBeUndefined() + expect(resolver.resolve('', ctx)).toBeUndefined() + } + ) + it.concurrent('should return undefined for non-existent block', () => { const workflow = createTestWorkflow([{ id: 'existing' }]) const resolver = new BlockResolver(workflow) diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index 63ab36138..1b4335e59 100644 --- a/apps/sim/executor/variables/resolvers/block.ts +++ b/apps/sim/executor/variables/resolvers/block.ts @@ -17,7 +17,6 @@ import { type Resolver, } from '@/executor/variables/resolvers/reference' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' -import { getTool } from '@/tools/utils' export class BlockResolver implements Resolver { private nameToBlockId: Map @@ -68,9 +67,7 @@ export class BlockResolver implements Resolver { blockData[blockId] = output } - const toolId = block.config?.tool - const toolConfig = toolId ? getTool(toolId) : undefined - const outputSchema = getBlockSchema(block, toolConfig) + const outputSchema = getBlockSchema(block) if (outputSchema && Object.keys(outputSchema).length > 0) { blockOutputSchemas[blockId] = outputSchema diff --git a/apps/sim/lib/copilot/tools/client/workflow/block-output-utils.ts b/apps/sim/lib/copilot/tools/client/workflow/block-output-utils.ts index 4916cb770..81d94b5f7 100644 --- a/apps/sim/lib/copilot/tools/client/workflow/block-output-utils.ts +++ b/apps/sim/lib/copilot/tools/client/workflow/block-output-utils.ts @@ -1,9 +1,7 @@ import { - extractFieldsFromSchema, - parseResponseFormatSafely, -} from '@/lib/core/utils/response-format' -import { getBlockOutputPaths } from '@/lib/workflows/blocks/block-outputs' -import { getBlock } from '@/blocks' + getEffectiveBlockOutputPaths, + getEvaluatorMetricOutputs, +} from '@/lib/workflows/blocks/block-outputs' import { normalizeName } from '@/executor/constants' import { useVariablesStore } from '@/stores/panel/variables/store' import type { Variable } from '@/stores/panel/variables/types' @@ -92,7 +90,6 @@ export function getSubflowInsidePaths( export function computeBlockOutputPaths(block: BlockState, ctx: WorkflowContext): string[] { const { blocks, loops, parallels, subBlockValues } = ctx - const blockConfig = getBlock(block.type) const mergedSubBlocks = getMergedSubBlocks(blocks, subBlockValues, block.id) if (block.type === 'loop' || block.type === 'parallel') { @@ -101,12 +98,14 @@ export function computeBlockOutputPaths(block: BlockState, ctx: WorkflowContext) } if (block.type === 'evaluator') { - const metricsValue = getSubBlockValue(blocks, subBlockValues, block.id, 'metrics') - if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) { - const validMetrics = metricsValue.filter((metric: { name?: string }) => metric?.name) - return validMetrics.map((metric: { name: string }) => metric.name.toLowerCase()) + const metricOutputs = getEvaluatorMetricOutputs(mergedSubBlocks) + if (metricOutputs) { + return Object.keys(metricOutputs) } - return getBlockOutputPaths(block.type, mergedSubBlocks) + return getEffectiveBlockOutputPaths(block.type, mergedSubBlocks, { + triggerMode: Boolean(block.triggerMode), + preferToolOutputs: !block.triggerMode, + }) } if (block.type === 'variables') { @@ -122,18 +121,10 @@ export function computeBlockOutputPaths(block: BlockState, ctx: WorkflowContext) return [] } - if (blockConfig) { - const responseFormatValue = mergedSubBlocks?.responseFormat?.value - const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id) - if (responseFormat) { - const schemaFields = extractFieldsFromSchema(responseFormat) - if (schemaFields.length > 0) { - return schemaFields.map((field) => field.name) - } - } - } - - return getBlockOutputPaths(block.type, mergedSubBlocks, block.triggerMode) + return getEffectiveBlockOutputPaths(block.type, mergedSubBlocks, { + triggerMode: Boolean(block.triggerMode), + preferToolOutputs: !block.triggerMode, + }) } export function formatOutputsWithPrefix(paths: string[], blockName: string): string[] { diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts index 7b945d6b0..5203151da 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts @@ -233,7 +233,11 @@ export const getBlocksMetadataServerTool: BaseServerTool< const resolvedToolId = resolveToolIdForOperation(blockConfig, opId) const toolCfg = resolvedToolId ? toolsRegistry[resolvedToolId] : undefined const toolParams: Record = toolCfg?.params || {} - const toolOutputs: Record = toolCfg?.outputs || {} + 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 diff --git a/apps/sim/lib/workflows/blocks/block-outputs.test.ts b/apps/sim/lib/workflows/blocks/block-outputs.test.ts new file mode 100644 index 000000000..ccd2b22e7 --- /dev/null +++ b/apps/sim/lib/workflows/blocks/block-outputs.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest' +import { + getEffectiveBlockOutputPaths, + getEffectiveBlockOutputs, + getEffectiveBlockOutputType, +} from '@/lib/workflows/blocks/block-outputs' + +type SubBlocks = Record + +function rootPaths(paths: string[]): string[] { + return [...new Set(paths.map((path) => path.split('.')[0]).filter(Boolean))].sort() +} + +describe('block outputs parity', () => { + it.concurrent('keeps evaluator tag paths and types aligned', () => { + const subBlocks: SubBlocks = { + metrics: { + value: [ + { + name: 'Accuracy', + description: 'How accurate the answer is', + range: { min: 0, max: 1 }, + }, + { + name: 'Relevance', + description: 'How relevant the answer is', + range: { min: 0, max: 1 }, + }, + ], + }, + } + + const options = { triggerMode: false, preferToolOutputs: true } + const outputs = getEffectiveBlockOutputs('evaluator', subBlocks, options) + const paths = getEffectiveBlockOutputPaths('evaluator', subBlocks, options) + + expect(rootPaths(paths)).toEqual(Object.keys(outputs).sort()) + expect(paths).toContain('accuracy') + expect(paths).toContain('relevance') + expect(getEffectiveBlockOutputType('evaluator', 'accuracy', subBlocks, options)).toBe('number') + expect(getEffectiveBlockOutputType('evaluator', 'relevance', subBlocks, options)).toBe('number') + }) + + it.concurrent('keeps agent responseFormat tag paths and types aligned', () => { + const subBlocks: SubBlocks = { + responseFormat: { + value: { + name: 'calculator_output', + schema: { + type: 'object', + properties: { + min: { type: 'number' }, + max: { type: 'number' }, + }, + required: ['min', 'max'], + additionalProperties: false, + }, + strict: true, + }, + }, + } + + const options = { triggerMode: false, preferToolOutputs: true } + const outputs = getEffectiveBlockOutputs('agent', subBlocks, options) + const paths = getEffectiveBlockOutputPaths('agent', subBlocks, options) + + expect(rootPaths(paths)).toEqual(Object.keys(outputs).sort()) + expect(paths).toContain('min') + expect(paths).toContain('max') + expect(getEffectiveBlockOutputType('agent', 'min', subBlocks, options)).toBe('number') + expect(getEffectiveBlockOutputType('agent', 'max', subBlocks, options)).toBe('number') + }) +}) diff --git a/apps/sim/lib/workflows/blocks/block-outputs.ts b/apps/sim/lib/workflows/blocks/block-outputs.ts index edb95fdf0..8574e7422 100644 --- a/apps/sim/lib/workflows/blocks/block-outputs.ts +++ b/apps/sim/lib/workflows/blocks/block-outputs.ts @@ -33,6 +33,12 @@ interface SubBlockWithValue { value?: unknown } +interface EffectiveOutputOptions { + triggerMode?: boolean + preferToolOutputs?: boolean + includeHidden?: boolean +} + type ConditionValue = string | number | boolean /** @@ -96,12 +102,13 @@ function evaluateOutputCondition( */ function filterOutputsByCondition( outputs: OutputDefinition, - subBlocks: Record | undefined + subBlocks: Record | undefined, + includeHidden = false ): OutputDefinition { const filtered: OutputDefinition = {} for (const [key, value] of Object.entries(outputs)) { - if (isHiddenFromDisplay(value)) continue + if (!includeHidden && isHiddenFromDisplay(value)) continue if (!value || typeof value !== 'object' || !('condition' in value)) { filtered[key] = value @@ -112,8 +119,13 @@ function filterOutputsByCondition( const passes = !condition || evaluateOutputCondition(condition, subBlocks) if (passes) { - const { condition: _, hiddenFromDisplay: __, ...rest } = value - filtered[key] = rest + if (includeHidden) { + const { condition: _, ...rest } = value + filtered[key] = rest + } else { + const { condition: _, hiddenFromDisplay: __, ...rest } = value + filtered[key] = rest + } } } @@ -243,8 +255,10 @@ function applyInputFormatToOutputs( export function getBlockOutputs( blockType: string, subBlocks?: Record, - triggerMode?: boolean + triggerMode?: boolean, + options?: { includeHidden?: boolean } ): OutputDefinition { + const includeHidden = options?.includeHidden ?? false const blockConfig = getBlock(blockType) if (!blockConfig) return {} @@ -269,7 +283,8 @@ export function getBlockOutputs( // Start with block config outputs (respects hiddenFromDisplay via filterOutputsByCondition) const baseOutputs = filterOutputsByCondition( { ...(blockConfig.outputs || {}) } as OutputDefinition, - subBlocks + subBlocks, + includeHidden ) // Add inputFormat fields (resume form fields) @@ -313,10 +328,112 @@ export function getBlockOutputs( } const baseOutputs = { ...(blockConfig.outputs || {}) } - const filteredOutputs = filterOutputsByCondition(baseOutputs, subBlocks) + const filteredOutputs = filterOutputsByCondition(baseOutputs, subBlocks, includeHidden) return applyInputFormatToOutputs(blockType, blockConfig, subBlocks, filteredOutputs) } +export function getResponseFormatOutputs( + subBlocks?: Record, + blockId = 'block' +): OutputDefinition | undefined { + const responseFormatValue = subBlocks?.responseFormat?.value + if (!responseFormatValue) return undefined + + const parsed = parseResponseFormatSafely(responseFormatValue, blockId) + if (!parsed) return undefined + + const fields = extractFieldsFromSchema(parsed) + if (fields.length === 0) return undefined + + const outputs: OutputDefinition = {} + for (const field of fields) { + outputs[field.name] = { + type: (field.type || 'any') as any, + description: field.description || `Field from Agent: ${field.name}`, + } + } + + return outputs +} + +export function getEvaluatorMetricOutputs( + subBlocks?: Record +): OutputDefinition | undefined { + const metricsValue = subBlocks?.metrics?.value + if (!metricsValue || !Array.isArray(metricsValue) || metricsValue.length === 0) return undefined + + const validMetrics = metricsValue.filter((metric: { name?: string }) => metric?.name) + if (validMetrics.length === 0) return undefined + + const outputs: OutputDefinition = {} + for (const metric of validMetrics as Array<{ name: string }>) { + outputs[metric.name.toLowerCase()] = { + type: 'number', + description: `Metric score: ${metric.name}`, + } + } + + return outputs +} + +export function getEffectiveBlockOutputs( + blockType: string, + subBlocks?: Record, + options?: EffectiveOutputOptions +): OutputDefinition { + const triggerMode = options?.triggerMode ?? false + const preferToolOutputs = options?.preferToolOutputs ?? !triggerMode + const includeHidden = options?.includeHidden ?? false + + if (blockType === 'agent') { + const responseFormatOutputs = getResponseFormatOutputs(subBlocks, 'agent') + if (responseFormatOutputs) return responseFormatOutputs + } + + let baseOutputs: OutputDefinition + if (triggerMode) { + baseOutputs = getBlockOutputs(blockType, subBlocks, true, { includeHidden }) + } else if (preferToolOutputs) { + const blockConfig = getBlock(blockType) + const toolOutputs = blockConfig + ? (getToolOutputs(blockConfig, subBlocks, { includeHidden }) as OutputDefinition) + : {} + baseOutputs = + toolOutputs && Object.keys(toolOutputs).length > 0 + ? toolOutputs + : getBlockOutputs(blockType, subBlocks, false, { includeHidden }) + } else { + baseOutputs = getBlockOutputs(blockType, subBlocks, false, { includeHidden }) + } + + if (blockType === 'evaluator') { + const metricOutputs = getEvaluatorMetricOutputs(subBlocks) + if (metricOutputs) { + return { ...baseOutputs, ...metricOutputs } + } + } + + return baseOutputs +} + +export function getEffectiveBlockOutputPaths( + blockType: string, + subBlocks?: Record, + options?: EffectiveOutputOptions +): string[] { + const outputs = getEffectiveBlockOutputs(blockType, subBlocks, options) + const paths = generateOutputPaths(outputs) + + if (blockType === TRIGGER_TYPES.START) { + return paths.filter((path) => { + const key = path.split('.')[0] + return !shouldFilterReservedField(blockType, key, '', subBlocks) + }) + } + + return paths +} + function shouldFilterReservedField( blockType: string, key: string, @@ -473,6 +590,26 @@ export function getBlockOutputType( return extractType(value) } +export function getEffectiveBlockOutputType( + blockType: string, + outputPath: string, + subBlocks?: Record, + options?: EffectiveOutputOptions +): string { + const outputs = getEffectiveBlockOutputs(blockType, subBlocks, options) + + const cleanPath = outputPath.replace(/\[(\d+)\]/g, '') + const pathParts = cleanPath.split('.').filter(Boolean) + + const filePropertyType = getFilePropertyType(outputs, pathParts) + if (filePropertyType) { + return filePropertyType + } + + const value = traverseOutputPath(outputs, pathParts) + return extractType(value) +} + /** * Recursively generates all output paths from an outputs schema. * @@ -594,8 +731,10 @@ function generateOutputPathsWithTypes( */ export function getToolOutputs( blockConfig: BlockConfig, - subBlocks?: Record + subBlocks?: Record, + options?: { includeHidden?: boolean } ): Record { + const includeHidden = options?.includeHidden ?? false if (!blockConfig?.tools?.config?.tool) return {} try { @@ -613,8 +752,12 @@ export function getToolOutputs( const toolConfig = getTool(toolId) if (!toolConfig?.outputs) return {} - - return toolConfig.outputs + if (includeHidden) { + return toolConfig.outputs + } + return Object.fromEntries( + Object.entries(toolConfig.outputs).filter(([_, def]) => !isHiddenFromDisplay(def)) + ) } catch (error) { logger.warn('Failed to get tool outputs', { error }) return {}