mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-27 15:58:11 -05:00
fix(hitl): add missing fields to block configs
This commit is contained in:
@@ -6,7 +6,6 @@ import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Code,
|
||||
Input,
|
||||
Label,
|
||||
Table,
|
||||
@@ -777,15 +776,6 @@ export default function ResumeExecutionPage({
|
||||
refreshSelectedDetail,
|
||||
])
|
||||
|
||||
const pauseResponsePreview = useMemo(() => {
|
||||
if (!selectedDetail?.pausePoint.response?.data) return '{}'
|
||||
try {
|
||||
return JSON.stringify(selectedDetail.pausePoint.response.data, null, 2)
|
||||
} catch {
|
||||
return String(selectedDetail.pausePoint.response.data)
|
||||
}
|
||||
}, [selectedDetail])
|
||||
|
||||
const isFormComplete = useMemo(() => {
|
||||
if (!isHumanMode || !hasInputFormat) return true
|
||||
return inputFormatFields.every((field) => {
|
||||
@@ -1155,10 +1145,12 @@ export default function ResumeExecutionPage({
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
<Label>Pause Data</Label>
|
||||
<Label>Display Data</Label>
|
||||
</div>
|
||||
<div style={{ padding: '16px' }}>
|
||||
<Code.Viewer code={pauseResponsePreview} language='json' />
|
||||
<p style={{ fontSize: '13px', color: 'var(--text-muted)' }}>
|
||||
No display data configured
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1183,19 +1183,6 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks, true)
|
||||
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
}
|
||||
} else if (sourceBlock.type === 'approval') {
|
||||
const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks)
|
||||
|
||||
const isSelfReference = activeSourceBlockId === blockId
|
||||
|
||||
if (dynamicOutputs.length > 0) {
|
||||
const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
|
||||
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
|
||||
} else {
|
||||
const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks)
|
||||
const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
|
||||
}
|
||||
} else if (sourceBlock.type === 'human_in_the_loop') {
|
||||
const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks)
|
||||
|
||||
@@ -1400,13 +1387,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
if (!accessibleBlock) continue
|
||||
|
||||
// Skip the current block - blocks cannot reference their own outputs
|
||||
// Exception: approval and human_in_the_loop blocks can reference their own outputs
|
||||
if (
|
||||
accessibleBlockId === blockId &&
|
||||
accessibleBlock.type !== 'approval' &&
|
||||
accessibleBlock.type !== 'human_in_the_loop'
|
||||
)
|
||||
continue
|
||||
// Exception: human_in_the_loop blocks can reference their own outputs (url, resumeEndpoint)
|
||||
if (accessibleBlockId === blockId && accessibleBlock.type !== 'human_in_the_loop') continue
|
||||
|
||||
const blockConfig = getBlock(accessibleBlock.type)
|
||||
|
||||
@@ -1520,19 +1502,6 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const outputPaths = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks, true)
|
||||
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
}
|
||||
} else if (accessibleBlock.type === 'approval') {
|
||||
const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks)
|
||||
|
||||
const isSelfReference = accessibleBlockId === blockId
|
||||
|
||||
if (dynamicOutputs.length > 0) {
|
||||
const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
|
||||
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
|
||||
} else {
|
||||
const outputPaths = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks)
|
||||
const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
|
||||
}
|
||||
} else if (accessibleBlock.type === 'human_in_the_loop') {
|
||||
const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks)
|
||||
|
||||
|
||||
@@ -769,7 +769,13 @@ Example 3 (Array Input):
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Generated response content' },
|
||||
model: { type: 'string', description: 'Model used for generation' },
|
||||
tokens: { type: 'any', description: 'Token usage statistics' },
|
||||
toolCalls: { type: 'any', description: 'Tool calls made' },
|
||||
tokens: { type: 'json', description: 'Token usage statistics' },
|
||||
toolCalls: { type: 'json', description: 'Tool calls made' },
|
||||
providerTiming: {
|
||||
type: 'json',
|
||||
description: 'Provider timing information',
|
||||
hiddenFromDisplay: true,
|
||||
},
|
||||
cost: { type: 'number', description: 'Cost of the API call', hiddenFromDisplay: true },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -162,5 +162,21 @@ export const HumanInTheLoopBlock: BlockConfig<ResponseBlockOutput> = {
|
||||
type: 'string',
|
||||
description: 'Resume API endpoint URL for direct curl requests',
|
||||
},
|
||||
response: {
|
||||
type: 'json',
|
||||
description: 'Display data shown to the approver',
|
||||
hiddenFromDisplay: true,
|
||||
},
|
||||
submission: {
|
||||
type: 'json',
|
||||
description: 'Form submission data from the approver',
|
||||
hiddenFromDisplay: true,
|
||||
},
|
||||
resumeInput: {
|
||||
type: 'json',
|
||||
description: 'Raw input data submitted when resuming',
|
||||
hiddenFromDisplay: true,
|
||||
},
|
||||
submittedAt: { type: 'string', description: 'ISO timestamp when the workflow was resumed' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -247,6 +247,7 @@ export const RouterBlock: BlockConfig<RouterResponse> = {
|
||||
tokens: { type: 'json', description: 'Token usage' },
|
||||
cost: { type: 'json', description: 'Cost information' },
|
||||
selectedPath: { type: 'json', description: 'Selected routing path' },
|
||||
selectedRoute: { type: 'string', description: 'Selected route ID' },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,11 @@ export const WorkflowBlock: BlockConfig = {
|
||||
childWorkflowName: { type: 'string', description: 'Child workflow name' },
|
||||
result: { type: 'json', description: 'Workflow execution result' },
|
||||
error: { type: 'string', description: 'Error message' },
|
||||
childTraceSpans: {
|
||||
type: 'json',
|
||||
description: 'Child workflow trace spans',
|
||||
hiddenFromDisplay: true,
|
||||
},
|
||||
},
|
||||
hideFromToolbar: true,
|
||||
}
|
||||
|
||||
@@ -43,5 +43,10 @@ export const WorkflowInputBlock: BlockConfig = {
|
||||
childWorkflowName: { type: 'string', description: 'Child workflow name' },
|
||||
result: { type: 'json', description: 'Workflow execution result' },
|
||||
error: { type: 'string', description: 'Error message' },
|
||||
childTraceSpans: {
|
||||
type: 'json',
|
||||
description: 'Child workflow trace spans',
|
||||
hiddenFromDisplay: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -157,6 +157,11 @@ export type OutputFieldDefinition =
|
||||
* Uses the same condition format as subBlocks.
|
||||
*/
|
||||
condition?: OutputCondition
|
||||
/**
|
||||
* If true, this output is hidden from display in the tag dropdown and logs,
|
||||
* but still available for resolution and execution.
|
||||
*/
|
||||
hiddenFromDisplay?: boolean
|
||||
}
|
||||
|
||||
export interface ParamConfig {
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import type { LoopType, ParallelType } from '@/lib/workflows/types'
|
||||
|
||||
/**
|
||||
* Runtime-injected keys for trigger blocks that should be hidden from logs/display.
|
||||
* These are added during execution but aren't part of the block's static output schema.
|
||||
*/
|
||||
export const TRIGGER_INTERNAL_KEYS = ['webhook', 'workflowId'] as const
|
||||
export type TriggerInternalKey = (typeof TRIGGER_INTERNAL_KEYS)[number]
|
||||
|
||||
export function isTriggerInternalKey(key: string): key is TriggerInternalKey {
|
||||
return TRIGGER_INTERNAL_KEYS.includes(key as TriggerInternalKey)
|
||||
}
|
||||
|
||||
export enum BlockType {
|
||||
PARALLEL = 'parallel',
|
||||
LOOP = 'loop',
|
||||
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
DEFAULTS,
|
||||
EDGE,
|
||||
isSentinelBlockType,
|
||||
isTriggerBehavior,
|
||||
isWorkflowBlockType,
|
||||
} from '@/executor/constants'
|
||||
import type { DAGNode } from '@/executor/dag/builder'
|
||||
import { ChildWorkflowError } from '@/executor/errors/child-workflow-error'
|
||||
@@ -30,6 +28,7 @@ import type {
|
||||
} from '@/executor/types'
|
||||
import { streamingResponseFormatProcessor } from '@/executor/utils'
|
||||
import { buildBlockExecutionError, normalizeError } from '@/executor/utils/errors'
|
||||
import { filterOutputForLog } from '@/executor/utils/output-filter'
|
||||
import { validateBlockType } from '@/executor/utils/permission-check'
|
||||
import type { VariableResolver } from '@/executor/variables/resolver'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
@@ -149,13 +148,15 @@ export class BlockExecutor {
|
||||
blockLog.endedAt = new Date().toISOString()
|
||||
blockLog.durationMs = duration
|
||||
blockLog.success = true
|
||||
blockLog.output = this.filterOutputForLog(block, normalizedOutput)
|
||||
blockLog.output = filterOutputForLog(block.metadata?.id || '', normalizedOutput, { block })
|
||||
}
|
||||
|
||||
this.state.setBlockOutput(node.id, normalizedOutput, duration)
|
||||
|
||||
if (!isSentinel) {
|
||||
const displayOutput = this.filterOutputForDisplay(block, normalizedOutput)
|
||||
const displayOutput = filterOutputForLog(block.metadata?.id || '', normalizedOutput, {
|
||||
block,
|
||||
})
|
||||
this.callOnBlockComplete(ctx, node, block, resolvedInputs, displayOutput, duration)
|
||||
}
|
||||
|
||||
@@ -233,7 +234,7 @@ export class BlockExecutor {
|
||||
blockLog.success = false
|
||||
blockLog.error = errorMessage
|
||||
blockLog.input = input
|
||||
blockLog.output = this.filterOutputForLog(block, errorOutput)
|
||||
blockLog.output = filterOutputForLog(block.metadata?.id || '', errorOutput, { block })
|
||||
}
|
||||
|
||||
logger.error(
|
||||
@@ -246,7 +247,7 @@ export class BlockExecutor {
|
||||
)
|
||||
|
||||
if (!isSentinel) {
|
||||
const displayOutput = this.filterOutputForDisplay(block, errorOutput)
|
||||
const displayOutput = filterOutputForLog(block.metadata?.id || '', errorOutput, { block })
|
||||
this.callOnBlockComplete(ctx, node, block, input, displayOutput, duration)
|
||||
}
|
||||
|
||||
@@ -335,51 +336,6 @@ export class BlockExecutor {
|
||||
return { result: output }
|
||||
}
|
||||
|
||||
private filterOutputForLog(
|
||||
block: SerializedBlock,
|
||||
output: NormalizedBlockOutput
|
||||
): NormalizedBlockOutput {
|
||||
const blockType = block.metadata?.id
|
||||
|
||||
if (blockType === BlockType.HUMAN_IN_THE_LOOP) {
|
||||
const filtered: NormalizedBlockOutput = {}
|
||||
for (const [key, value] of Object.entries(output)) {
|
||||
if (key.startsWith('_')) continue
|
||||
if (key === 'response') continue
|
||||
filtered[key] = value
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
if (isTriggerBehavior(block)) {
|
||||
const filtered: NormalizedBlockOutput = {}
|
||||
const internalKeys = ['webhook', 'workflowId']
|
||||
for (const [key, value] of Object.entries(output)) {
|
||||
if (internalKeys.includes(key)) continue
|
||||
filtered[key] = value
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
private filterOutputForDisplay(
|
||||
block: SerializedBlock,
|
||||
output: NormalizedBlockOutput
|
||||
): NormalizedBlockOutput {
|
||||
const filtered = this.filterOutputForLog(block, output)
|
||||
|
||||
if (isWorkflowBlockType(block.metadata?.id)) {
|
||||
const { childTraceSpans: _, ...displayOutput } = filtered as {
|
||||
childTraceSpans?: unknown
|
||||
} & Record<string, unknown>
|
||||
return displayOutput
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
private callOnBlockStart(ctx: ExecutionContext, node: DAGNode, block: SerializedBlock): void {
|
||||
const blockId = node.id
|
||||
const blockName = block.metadata?.name ?? blockId
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { BlockType, isTriggerBehavior } from '@/executor/constants'
|
||||
import { BlockType, isTriggerBehavior, isTriggerInternalKey } from '@/executor/constants'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
|
||||
@@ -33,7 +33,12 @@ export class TriggerBlockHandler implements BlockHandler {
|
||||
const starterOutput = starterState.output
|
||||
|
||||
if (starterOutput.webhook?.data) {
|
||||
const { webhook, workflowId, ...cleanOutput } = starterOutput
|
||||
const cleanOutput: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(starterOutput)) {
|
||||
if (!isTriggerInternalKey(key)) {
|
||||
cleanOutput[key] = value
|
||||
}
|
||||
}
|
||||
return cleanOutput
|
||||
}
|
||||
|
||||
|
||||
54
apps/sim/executor/utils/output-filter.ts
Normal file
54
apps/sim/executor/utils/output-filter.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { getBlock } from '@/blocks'
|
||||
import { isTriggerBehavior, isTriggerInternalKey } from '@/executor/constants'
|
||||
import type { NormalizedBlockOutput } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
|
||||
/**
|
||||
* Filters block output for logging/display purposes.
|
||||
* Removes internal fields and fields marked with hiddenFromDisplay.
|
||||
*
|
||||
* @param blockType - The block type string (e.g., 'human_in_the_loop', 'workflow')
|
||||
* @param output - The raw block output to filter
|
||||
* @param options - Optional configuration
|
||||
* @param options.block - Full SerializedBlock for trigger behavior detection
|
||||
* @param options.additionalHiddenKeys - Extra keys to filter out (e.g., 'resume')
|
||||
*/
|
||||
export function filterOutputForLog(
|
||||
blockType: string,
|
||||
output: NormalizedBlockOutput,
|
||||
options?: {
|
||||
block?: SerializedBlock
|
||||
additionalHiddenKeys?: string[]
|
||||
}
|
||||
): NormalizedBlockOutput {
|
||||
const blockConfig = blockType ? getBlock(blockType) : undefined
|
||||
const filtered: NormalizedBlockOutput = {}
|
||||
const additionalHiddenKeys = options?.additionalHiddenKeys ?? []
|
||||
|
||||
for (const [key, value] of Object.entries(output)) {
|
||||
// Skip internal keys (underscore prefix)
|
||||
if (key.startsWith('_')) continue
|
||||
|
||||
// Skip fields marked as hidden in block config
|
||||
if (blockConfig?.outputs) {
|
||||
const outputDef = blockConfig.outputs[key]
|
||||
if (outputDef && typeof outputDef === 'object' && outputDef.hiddenFromDisplay) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Skip runtime-injected trigger keys not in block config
|
||||
if (options?.block && isTriggerBehavior(options.block) && isTriggerInternalKey(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip additional hidden keys specified by caller
|
||||
if (additionalHiddenKeys.includes(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
filtered[key] = value
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
@@ -86,8 +86,8 @@ function evaluateOutputCondition(
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters outputs based on their conditions.
|
||||
* Returns a new OutputDefinition with only the outputs whose conditions are met.
|
||||
* Filters outputs based on their conditions and hiddenFromDisplay flag.
|
||||
* Returns a new OutputDefinition with only the outputs that should be shown.
|
||||
*/
|
||||
function filterOutputsByCondition(
|
||||
outputs: OutputDefinition,
|
||||
@@ -96,6 +96,16 @@ function filterOutputsByCondition(
|
||||
const filtered: OutputDefinition = {}
|
||||
|
||||
for (const [key, value] of Object.entries(outputs)) {
|
||||
// Skip fields marked as hidden from display
|
||||
if (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
'hiddenFromDisplay' in value &&
|
||||
value.hiddenFromDisplay
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!value || typeof value !== 'object' || !('condition' in value)) {
|
||||
filtered[key] = value
|
||||
continue
|
||||
@@ -105,7 +115,7 @@ function filterOutputsByCondition(
|
||||
const passes = !condition || evaluateOutputCondition(condition, subBlocks)
|
||||
|
||||
if (passes) {
|
||||
const { condition: _, ...rest } = value
|
||||
const { condition: _, hiddenFromDisplay: __, ...rest } = value
|
||||
filtered[key] = rest
|
||||
}
|
||||
}
|
||||
@@ -259,50 +269,26 @@ export function getBlockOutputs(
|
||||
}
|
||||
|
||||
if (blockType === 'human_in_the_loop') {
|
||||
const hitlOutputs: OutputDefinition = {
|
||||
url: { type: 'string', description: 'Resume UI URL' },
|
||||
resumeEndpoint: {
|
||||
type: 'string',
|
||||
description: 'Resume API endpoint URL for direct curl requests',
|
||||
},
|
||||
}
|
||||
// Start with block config outputs (respects hiddenFromDisplay via filterOutputsByCondition)
|
||||
const baseOutputs = filterOutputsByCondition(
|
||||
{ ...(blockConfig.outputs || {}) } as OutputDefinition,
|
||||
subBlocks
|
||||
)
|
||||
|
||||
// Add inputFormat fields (resume form fields)
|
||||
const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value)
|
||||
|
||||
for (const field of normalizedInputFormat) {
|
||||
const fieldName = field?.name?.trim()
|
||||
if (!fieldName) continue
|
||||
|
||||
hitlOutputs[fieldName] = {
|
||||
baseOutputs[fieldName] = {
|
||||
type: (field?.type || 'any') as any,
|
||||
description: `Field from resume form`,
|
||||
description: field?.description || `Field from resume form`,
|
||||
}
|
||||
}
|
||||
|
||||
return hitlOutputs
|
||||
}
|
||||
|
||||
if (blockType === 'approval') {
|
||||
// Start with only url (apiUrl commented out - not accessible as output)
|
||||
const pauseResumeOutputs: OutputDefinition = {
|
||||
url: { type: 'string', description: 'Resume UI URL' },
|
||||
// apiUrl: { type: 'string', description: 'Resume API URL' }, // Commented out - not accessible as output
|
||||
}
|
||||
|
||||
const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value)
|
||||
|
||||
// Add each input format field as a top-level output
|
||||
for (const field of normalizedInputFormat) {
|
||||
const fieldName = field?.name?.trim()
|
||||
if (!fieldName) continue
|
||||
|
||||
pauseResumeOutputs[fieldName] = {
|
||||
type: (field?.type || 'any') as any,
|
||||
description: `Field from input format`,
|
||||
}
|
||||
}
|
||||
|
||||
return pauseResumeOutputs
|
||||
return baseOutputs
|
||||
}
|
||||
|
||||
if (startPath === StartBlockPath.LEGACY_STARTER) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { LoggingSession } from '@/lib/logs/execution/logging-session'
|
||||
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
|
||||
import { ExecutionSnapshot } from '@/executor/execution/snapshot'
|
||||
import type { ExecutionResult, PausePoint, SerializedSnapshot } from '@/executor/types'
|
||||
import { filterOutputForLog } from '@/executor/utils/output-filter'
|
||||
import type { SerializedConnection } from '@/serializer/types'
|
||||
|
||||
const logger = createLogger('HumanInTheLoopManager')
|
||||
@@ -576,13 +577,11 @@ export class PauseResumeManager {
|
||||
log.blockId === contextId
|
||||
)
|
||||
if (blockLogIndex !== -1) {
|
||||
// Filter output for logging (exclude internal fields and response)
|
||||
const filteredOutput: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(mergedOutput)) {
|
||||
if (key.startsWith('_')) continue
|
||||
if (key === 'response') continue
|
||||
filteredOutput[key] = value
|
||||
}
|
||||
// Filter output for logging using shared utility
|
||||
// 'resume' is redundant with url/resumeEndpoint so we filter it out
|
||||
const filteredOutput = filterOutputForLog('human_in_the_loop', mergedOutput, {
|
||||
additionalHiddenKeys: ['resume'],
|
||||
})
|
||||
stateCopy.blockLogs[blockLogIndex] = {
|
||||
...stateCopy.blockLogs[blockLogIndex],
|
||||
blockId: stateBlockKey,
|
||||
|
||||
Reference in New Issue
Block a user