fix(custom-tool): fix textarea, param dropdown for available params, validation for invalid schemas, variable resolution in custom tools and subflow tags (#1117)

* fix(custom-tools): fix text area for custom tools

* added param dropdown in agent custom tool

* add syntax highlighting for params, fix dropdown styling

* ux

* add tooltip to prevent indicate invalid json schema on schema and code tabs

* feat(custom-tool): added stricter JSON schema validation and error when saving json schema for custom tools

* fix(custom-tool): allow variable resolution in custom tools

* fix variable resolution in subflow tags

* refactored function execution to use helpers

* cleanup

* fix block variable resolution to inject at runtime

* fix highlighting code

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
This commit is contained in:
Waleed Latif
2025-08-23 13:15:12 -07:00
committed by GitHub
parent 25b2c45ec0
commit 730164abee
26 changed files with 688 additions and 244 deletions

View File

@@ -213,24 +213,81 @@ function createUserFriendlyErrorMessage(
}
/**
* Resolves environment variables and tags in code
* @param code - Code with variables
* @param params - Parameters that may contain variable values
* @param envVars - Environment variables from the workflow
* @returns Resolved code
* Resolves workflow variables with <variable.name> syntax
*/
function resolveWorkflowVariables(
code: string,
workflowVariables: Record<string, any>,
contextVariables: Record<string, any>
): string {
let resolvedCode = code
function resolveCodeVariables(
const variableMatches = resolvedCode.match(/<variable\.([^>]+)>/g) || []
for (const match of variableMatches) {
const variableName = match.slice('<variable.'.length, -1).trim()
// Find the variable by name (workflowVariables is indexed by ID, values are variable objects)
const foundVariable = Object.entries(workflowVariables).find(
([_, variable]) => (variable.name || '').replace(/\s+/g, '') === variableName
)
if (foundVariable) {
const variable = foundVariable[1]
// Get the typed value - handle different variable types
let variableValue = variable.value
if (variable.value !== undefined && variable.value !== null) {
try {
// Handle 'string' type the same as 'plain' for backward compatibility
const type = variable.type === 'string' ? 'plain' : variable.type
// For plain text, use exactly what's entered without modifications
if (type === 'plain' && typeof variableValue === 'string') {
// Use as-is for plain text
} else if (type === 'number') {
variableValue = Number(variableValue)
} else if (type === 'boolean') {
variableValue = variableValue === 'true' || variableValue === true
} else if (type === 'json') {
try {
variableValue =
typeof variableValue === 'string' ? JSON.parse(variableValue) : variableValue
} catch {
// Keep original value if JSON parsing fails
}
}
} catch (error) {
// Fallback to original value on error
variableValue = variable.value
}
}
// Create a safe variable reference
const safeVarName = `__variable_${variableName.replace(/[^a-zA-Z0-9_]/g, '_')}`
contextVariables[safeVarName] = variableValue
// Replace the variable reference with the safe variable name
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
} else {
// Variable not found - replace with empty string to avoid syntax errors
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), '')
}
}
return resolvedCode
}
/**
* Resolves environment variables with {{var_name}} syntax
*/
function resolveEnvironmentVariables(
code: string,
params: Record<string, any>,
envVars: Record<string, string> = {},
blockData: Record<string, any> = {},
blockNameMapping: Record<string, string> = {}
): { resolvedCode: string; contextVariables: Record<string, any> } {
envVars: Record<string, string>,
contextVariables: Record<string, any>
): string {
let resolvedCode = code
const contextVariables: Record<string, any> = {}
// Resolve environment variables with {{var_name}} syntax
const envVarMatches = resolvedCode.match(/\{\{([^}]+)\}\}/g) || []
for (const match of envVarMatches) {
const varName = match.slice(2, -2).trim()
@@ -245,7 +302,21 @@ function resolveCodeVariables(
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
}
// Resolve tags with <tag_name> syntax (including nested paths like <block.response.data>)
return resolvedCode
}
/**
* Resolves tags with <tag_name> syntax (including nested paths like <block.response.data>)
*/
function resolveTagVariables(
code: string,
params: Record<string, any>,
blockData: Record<string, any>,
blockNameMapping: Record<string, string>,
contextVariables: Record<string, any>
): string {
let resolvedCode = code
const tagMatches = resolvedCode.match(/<([a-zA-Z_][a-zA-Z0-9_.]*[a-zA-Z0-9_])>/g) || []
for (const match of tagMatches) {
@@ -300,6 +371,42 @@ function resolveCodeVariables(
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
}
return resolvedCode
}
/**
* Resolves environment variables and tags in code
* @param code - Code with variables
* @param params - Parameters that may contain variable values
* @param envVars - Environment variables from the workflow
* @returns Resolved code
*/
function resolveCodeVariables(
code: string,
params: Record<string, any>,
envVars: Record<string, string> = {},
blockData: Record<string, any> = {},
blockNameMapping: Record<string, string> = {},
workflowVariables: Record<string, any> = {}
): { resolvedCode: string; contextVariables: Record<string, any> } {
let resolvedCode = code
const contextVariables: Record<string, any> = {}
// Resolve workflow variables with <variable.name> syntax first
resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables)
// Resolve environment variables with {{var_name}} syntax
resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables)
// Resolve tags with <tag_name> syntax (including nested paths like <block.response.data>)
resolvedCode = resolveTagVariables(
resolvedCode,
params,
blockData,
blockNameMapping,
contextVariables
)
return { resolvedCode, contextVariables }
}
@@ -338,6 +445,7 @@ export async function POST(req: NextRequest) {
envVars = {},
blockData = {},
blockNameMapping = {},
workflowVariables = {},
workflowId,
isCustomTool = false,
} = body
@@ -360,7 +468,8 @@ export async function POST(req: NextRequest) {
executionParams,
envVars,
blockData,
blockNameMapping
blockNameMapping,
workflowVariables
)
resolvedCode = codeResolution.resolvedCode
const contextVariables = codeResolution.contextVariables
@@ -368,8 +477,8 @@ export async function POST(req: NextRequest) {
const executionMethod = 'vm' // Default execution method
logger.info(`[${requestId}] Using VM for code execution`, {
resolvedCode,
hasEnvVars: Object.keys(envVars).length > 0,
hasWorkflowVariables: Object.keys(workflowVariables).length > 0,
})
// Create a secure context with console logging

View File

@@ -39,6 +39,9 @@ export async function POST(request: NextRequest) {
stream,
messages,
environmentVariables,
workflowVariables,
blockData,
blockNameMapping,
reasoningEffort,
verbosity,
} = body
@@ -60,6 +63,7 @@ export async function POST(request: NextRequest) {
messageCount: messages?.length || 0,
hasEnvironmentVariables:
!!environmentVariables && Object.keys(environmentVariables).length > 0,
hasWorkflowVariables: !!workflowVariables && Object.keys(workflowVariables).length > 0,
reasoningEffort,
verbosity,
})
@@ -103,6 +107,9 @@ export async function POST(request: NextRequest) {
stream,
messages,
environmentVariables,
workflowVariables,
blockData,
blockNameMapping,
reasoningEffort,
verbosity,
})

View File

@@ -18,6 +18,7 @@ interface CodeEditorProps {
highlightVariables?: boolean
onKeyDown?: (e: React.KeyboardEvent) => void
disabled?: boolean
schemaParameters?: Array<{ name: string; type: string; description: string; required: boolean }>
}
export function CodeEditor({
@@ -30,6 +31,7 @@ export function CodeEditor({
highlightVariables = true,
onKeyDown,
disabled = false,
schemaParameters = [],
}: CodeEditorProps) {
const [code, setCode] = useState(value)
const [visualLineHeights, setVisualLineHeights] = useState<number[]>([])
@@ -120,25 +122,80 @@ export function CodeEditor({
// First, get the default Prism highlighting
let highlighted = highlight(code, languages[language], language)
// Then, highlight environment variables with {{var_name}} syntax in blue
if (highlighted.includes('{{')) {
highlighted = highlighted.replace(
/\{\{([^}]+)\}\}/g,
'<span class="text-blue-500">{{$1}}</span>'
)
// Collect all syntax highlights to apply in a single pass
type SyntaxHighlight = {
start: number
end: number
replacement: string
}
const highlights: SyntaxHighlight[] = []
// Also highlight tags with <tag_name> syntax in blue
if (highlighted.includes('<') && !language.includes('html')) {
highlighted = highlighted.replace(/<([^>\s/]+)>/g, (match, group) => {
// Avoid replacing HTML tags in comments
if (match.startsWith('<!--') || match.includes('</')) {
return match
}
return `<span class="text-blue-500">&lt;${group}&gt;</span>`
// Find environment variables with {{var_name}} syntax
let match
const envVarRegex = /\{\{([^}]+)\}\}/g
while ((match = envVarRegex.exec(highlighted)) !== null) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
replacement: `<span class="text-blue-500">${match[0]}</span>`,
})
}
// Find tags with <tag_name> syntax (not in HTML context)
if (!language.includes('html')) {
const tagRegex = /<([^>\s/]+)>/g
while ((match = tagRegex.exec(highlighted)) !== null) {
// Skip HTML comments and closing tags
if (!match[0].startsWith('<!--') && !match[0].includes('</')) {
const escaped = `&lt;${match[1]}&gt;`
highlights.push({
start: match.index,
end: match.index + match[0].length,
replacement: `<span class="text-blue-500">${escaped}</span>`,
})
}
}
}
// Find schema parameters as whole words
if (schemaParameters.length > 0) {
schemaParameters.forEach((param) => {
// Escape special regex characters in parameter name
const escapedName = param.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const paramRegex = new RegExp(`\\b(${escapedName})\\b`, 'g')
while ((match = paramRegex.exec(highlighted)) !== null) {
// Check if this position is already inside an HTML tag
// by looking for unclosed < before this position
let insideTag = false
let pos = match.index - 1
while (pos >= 0) {
if (highlighted[pos] === '>') break
if (highlighted[pos] === '<') {
insideTag = true
break
}
pos--
}
if (!insideTag) {
highlights.push({
start: match.index,
end: match.index + match[0].length,
replacement: `<span class="text-green-600 font-medium">${match[0]}</span>`,
})
}
}
})
}
// Sort highlights by start position (reverse order to maintain positions)
highlights.sort((a, b) => b.start - a.start)
// Apply all highlights
highlights.forEach(({ start, end, replacement }) => {
highlighted = highlighted.slice(0, start) + replacement + highlighted.slice(end)
})
return highlighted
}
@@ -204,12 +261,17 @@ export function CodeEditor({
disabled={disabled}
style={{
fontFamily: 'inherit',
minHeight: '46px',
minHeight: minHeight,
lineHeight: '21px',
height: '100%',
}}
className={cn('focus:outline-none', isCollapsed && 'pointer-events-none select-none')}
className={cn(
'h-full focus:outline-none',
isCollapsed && 'pointer-events-none select-none'
)}
textareaClassName={cn(
'focus:outline-none focus:ring-0 bg-transparent',
'!min-h-full !h-full resize-none !block',
(isCollapsed || disabled) && 'pointer-events-none'
)}
/>

View File

@@ -22,6 +22,7 @@ import {
import { checkEnvVarTrigger, EnvVarDropdown } from '@/components/ui/env-var-dropdown'
import { Label } from '@/components/ui/label'
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar'
@@ -36,6 +37,7 @@ interface CustomToolModalProps {
onOpenChange: (open: boolean) => void
onSave: (tool: CustomTool) => void
onDelete?: (toolId: string) => void
blockId: string
initialValues?: {
id?: string
schema: any
@@ -61,6 +63,7 @@ export function CustomToolModal({
onOpenChange,
onSave,
onDelete,
blockId,
initialValues,
}: CustomToolModalProps) {
const [activeSection, setActiveSection] = useState<ToolSection>('schema')
@@ -237,12 +240,16 @@ try {
// Environment variables and tags dropdown state
const [showEnvVars, setShowEnvVars] = useState(false)
const [showTags, setShowTags] = useState(false)
const [showSchemaParams, setShowSchemaParams] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
const [cursorPosition, setCursorPosition] = useState(0)
const codeEditorRef = useRef<HTMLDivElement>(null)
const schemaParamsDropdownRef = useRef<HTMLDivElement>(null)
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
// Add state for dropdown positioning
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 })
// Schema params keyboard navigation
const [schemaParamSelectedIndex, setSchemaParamSelectedIndex] = useState(0)
const addTool = useCustomToolsStore((state) => state.addTool)
const updateTool = useCustomToolsStore((state) => state.updateTool)
@@ -270,6 +277,21 @@ try {
}
}, [open, initialValues])
// Close schema params dropdown on outside click
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
schemaParamsDropdownRef.current &&
!schemaParamsDropdownRef.current.contains(event.target as Node)
) {
setShowSchemaParams(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const resetForm = () => {
setJsonSchema('')
setFunctionCode('')
@@ -309,6 +331,15 @@ try {
return false
}
// Validate that parameters object exists with correct structure
if (!parsed.function.parameters) {
return false
}
if (!parsed.function.parameters.type || parsed.function.parameters.properties === undefined) {
return false
}
return true
} catch (_error) {
return false
@@ -320,6 +351,25 @@ try {
return true // Allow empty code
}
// Extract parameters from JSON schema for autocomplete
const schemaParameters = useMemo(() => {
try {
if (!jsonSchema) return []
const parsed = JSON.parse(jsonSchema)
const properties = parsed?.function?.parameters?.properties
if (!properties) return []
return Object.keys(properties).map((key) => ({
name: key,
type: properties[key].type || 'any',
description: properties[key].description || '',
required: parsed?.function?.parameters?.required?.includes(key) || false,
}))
} catch {
return []
}
}, [jsonSchema])
// Memoize validation results to prevent unnecessary recalculations
const isSchemaValid = useMemo(() => validateJsonSchema(jsonSchema), [jsonSchema])
const isCodeValid = useMemo(() => validateFunctionCode(functionCode), [functionCode])
@@ -350,6 +400,34 @@ try {
return
}
// Validate parameters structure - must be present
if (!parsed.function.parameters) {
setSchemaError('Missing function.parameters object')
setActiveSection('schema')
return
}
if (!parsed.function.parameters.type) {
setSchemaError('Missing parameters.type field')
setActiveSection('schema')
return
}
if (parsed.function.parameters.properties === undefined) {
setSchemaError('Missing parameters.properties field')
setActiveSection('schema')
return
}
if (
typeof parsed.function.parameters.properties !== 'object' ||
parsed.function.parameters.properties === null
) {
setSchemaError('parameters.properties must be an object')
setActiveSection('schema')
return
}
// Check for duplicate tool name
const toolName = parsed.function.name
const customToolsStore = useCustomToolsStore.getState()
@@ -439,7 +517,52 @@ try {
// Prevent updates during AI generation/streaming
if (schemaGeneration.isLoading || schemaGeneration.isStreaming) return
setJsonSchema(value)
if (schemaError) {
// Real-time validation - show error immediately when schema is invalid
if (value.trim()) {
try {
const parsed = JSON.parse(value)
if (!parsed.type || parsed.type !== 'function') {
setSchemaError('Missing "type": "function"')
return
}
if (!parsed.function || !parsed.function.name) {
setSchemaError('Missing function.name field')
return
}
if (!parsed.function.parameters) {
setSchemaError('Missing function.parameters object')
return
}
if (!parsed.function.parameters.type) {
setSchemaError('Missing parameters.type field')
return
}
if (parsed.function.parameters.properties === undefined) {
setSchemaError('Missing parameters.properties field')
return
}
if (
typeof parsed.function.parameters.properties !== 'object' ||
parsed.function.parameters.properties === null
) {
setSchemaError('parameters.properties must be an object')
return
}
// Schema is valid, clear any existing error
setSchemaError(null)
} catch {
setSchemaError('Invalid JSON format')
}
} else {
// Clear error when schema is empty (will be caught during save)
setSchemaError(null)
}
}
@@ -499,9 +622,40 @@ try {
if (!tagTrigger.show) {
setActiveSourceBlockId(null)
}
// Show/hide schema parameters dropdown based on typing context
if (!codeGeneration.isStreaming && schemaParameters.length > 0) {
const schemaParamTrigger = checkSchemaParamTrigger(value, pos, schemaParameters)
if (schemaParamTrigger.show && !showSchemaParams) {
setShowSchemaParams(true)
setSchemaParamSelectedIndex(0)
} else if (!schemaParamTrigger.show && showSchemaParams) {
setShowSchemaParams(false)
}
}
}
}
// Function to check if we should show schema parameters dropdown
const checkSchemaParamTrigger = (text: string, cursorPos: number, parameters: any[]) => {
if (parameters.length === 0) return { show: false, searchTerm: '' }
// Look for partial parameter names after common patterns like 'const ', '= ', etc.
const beforeCursor = text.substring(0, cursorPos)
const words = beforeCursor.split(/[\s=();,{}[\]]+/)
const currentWord = words[words.length - 1] || ''
// Show dropdown if typing and current word could be a parameter
if (currentWord.length > 0 && /^[a-zA-Z_][\w]*$/.test(currentWord)) {
const matchingParams = parameters.filter((param) =>
param.name.toLowerCase().startsWith(currentWord.toLowerCase())
)
return { show: matchingParams.length > 0, searchTerm: currentWord, matches: matchingParams }
}
return { show: false, searchTerm: '' }
}
// Handle environment variable selection
const handleEnvVarSelect = (newValue: string) => {
setFunctionCode(newValue)
@@ -515,6 +669,32 @@ try {
setActiveSourceBlockId(null)
}
// Handle schema parameter selection
const handleSchemaParamSelect = (paramName: string) => {
const textarea = codeEditorRef.current?.querySelector('textarea')
if (textarea) {
const pos = textarea.selectionStart
const beforeCursor = functionCode.substring(0, pos)
const afterCursor = functionCode.substring(pos)
// Find the start of the current word
const words = beforeCursor.split(/[\s=();,{}[\]]+/)
const currentWord = words[words.length - 1] || ''
const wordStart = beforeCursor.lastIndexOf(currentWord)
// Replace the current partial word with the selected parameter
const newValue = beforeCursor.substring(0, wordStart) + paramName + afterCursor
setFunctionCode(newValue)
setShowSchemaParams(false)
// Set cursor position after the inserted parameter
setTimeout(() => {
textarea.focus()
textarea.setSelectionRange(wordStart + paramName.length, wordStart + paramName.length)
}, 0)
}
}
// Handle key press events
const handleKeyDown = (e: React.KeyboardEvent) => {
// Allow AI prompt interaction (e.g., Escape to close prompt bar)
@@ -535,10 +715,14 @@ try {
e.stopPropagation()
return
}
// Close dropdowns only if AI prompt isn't active
if (!showEnvVars && !showTags) {
// Close dropdowns first, only close modal if no dropdowns are open
if (showEnvVars || showTags || showSchemaParams) {
setShowEnvVars(false)
setShowTags(false)
setShowSchemaParams(false)
e.preventDefault()
e.stopPropagation()
return
}
}
@@ -552,7 +736,37 @@ try {
return
}
// Let dropdowns handle their own keyboard events if visible
// Handle schema parameters dropdown keyboard navigation
if (showSchemaParams && schemaParameters.length > 0) {
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
e.stopPropagation()
setSchemaParamSelectedIndex((prev) => Math.min(prev + 1, schemaParameters.length - 1))
break
case 'ArrowUp':
e.preventDefault()
e.stopPropagation()
setSchemaParamSelectedIndex((prev) => Math.max(prev - 1, 0))
break
case 'Enter':
e.preventDefault()
e.stopPropagation()
if (schemaParamSelectedIndex >= 0 && schemaParamSelectedIndex < schemaParameters.length) {
const selectedParam = schemaParameters[schemaParamSelectedIndex]
handleSchemaParamSelect(selectedParam.name)
}
break
case 'Escape':
e.preventDefault()
e.stopPropagation()
setShowSchemaParams(false)
break
}
return // Don't handle other dropdown events when schema params is active
}
// Let other dropdowns handle their own keyboard events if visible
if (showEnvVars || showTags) {
if (['ArrowDown', 'ArrowUp', 'Enter'].includes(e.key)) {
e.preventDefault()
@@ -619,6 +833,16 @@ try {
<DialogContent
className='flex h-[80vh] flex-col gap-0 p-0 sm:max-w-[700px]'
hideCloseButton
onKeyDown={(e) => {
// Intercept Escape key when dropdowns are open
if (e.key === 'Escape' && (showEnvVars || showTags || showSchemaParams)) {
e.preventDefault()
e.stopPropagation()
setShowEnvVars(false)
setShowTags(false)
setShowSchemaParams(false)
}
}}
>
<DialogHeader className='border-b px-6 py-4'>
<div className='flex items-center justify-between'>
@@ -729,7 +953,7 @@ try {
</div>
{schemaError &&
!schemaGeneration.isStreaming && ( // Hide schema error while streaming
<span className='ml-4 flex-shrink-0 text-red-600 text-sm'>{schemaError}</span>
<div className='ml-4 break-words text-red-600 text-sm'>{schemaError}</div>
)}
</div>
<CodeEditor
@@ -799,9 +1023,25 @@ try {
</div>
{codeError &&
!codeGeneration.isStreaming && ( // Hide code error while streaming
<span className='ml-4 flex-shrink-0 text-red-600 text-sm'>{codeError}</span>
<div className='ml-4 break-words text-red-600 text-sm'>{codeError}</div>
)}
</div>
{schemaParameters.length > 0 && (
<div className='mb-2 rounded-md bg-muted/50 p-2'>
<p className='text-muted-foreground text-xs'>
<span className='font-medium'>Available parameters:</span>{' '}
{schemaParameters.map((param, index) => (
<span key={param.name}>
<code className='rounded bg-background px-1 py-0.5 text-foreground'>
{param.name}
</code>
{index < schemaParameters.length - 1 && ', '}
</span>
))}
{'. '}Start typing a parameter name for autocomplete.
</p>
</div>
)}
<div ref={codeEditorRef} className='relative'>
<CodeEditor
value={functionCode}
@@ -819,6 +1059,7 @@ try {
highlightVariables={true}
disabled={codeGeneration.isLoading || codeGeneration.isStreaming} // Use disabled prop instead of readOnly
onKeyDown={handleKeyDown} // Pass keydown handler
schemaParameters={schemaParameters} // Pass schema parameters for highlighting
/>
{/* Environment variables dropdown */}
@@ -847,7 +1088,7 @@ try {
<TagDropdown
visible={showTags}
onSelect={handleTagSelect}
blockId=''
blockId={blockId}
activeSourceBlockId={activeSourceBlockId}
inputValue={functionCode}
cursorPosition={cursorPosition}
@@ -863,6 +1104,49 @@ try {
}}
/>
)}
{/* Schema parameters dropdown */}
{showSchemaParams && schemaParameters.length > 0 && (
<div
ref={schemaParamsDropdownRef}
className='absolute z-[9999] mt-1 w-64 overflow-visible rounded-md border bg-popover shadow-md'
style={{
top: `${dropdownPosition.top}px`,
left: `${dropdownPosition.left}px`,
}}
>
<div className='py-1'>
<div className='px-2 pt-2.5 pb-0.5 font-medium text-muted-foreground text-xs'>
Available Parameters
</div>
<div>
{schemaParameters.map((param, index) => (
<button
key={param.name}
onClick={() => handleSchemaParamSelect(param.name)}
onMouseEnter={() => setSchemaParamSelectedIndex(index)}
className={cn(
'flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm',
'hover:bg-accent hover:text-accent-foreground',
'focus:bg-accent focus:text-accent-foreground focus:outline-none',
index === schemaParamSelectedIndex &&
'bg-accent text-accent-foreground'
)}
>
<div
className='flex h-5 w-5 items-center justify-center rounded'
style={{ backgroundColor: '#2F8BFF' }}
>
<span className='h-3 w-3 font-bold text-white text-xs'>P</span>
</div>
<span className='flex-1 truncate'>{param.name}</span>
<span className='text-muted-foreground text-xs'>{param.type}</span>
</button>
))}
</div>
</div>
</div>
)}
</div>
<div className='h-6' />
</div>
@@ -899,11 +1183,27 @@ try {
Cancel
</Button>
{activeSection === 'schema' ? (
<Button onClick={() => setActiveSection('code')} disabled={!isSchemaValid}>
<Button
onClick={() => setActiveSection('code')}
disabled={!isSchemaValid || !!schemaError}
>
Next
</Button>
) : (
<Button onClick={handleSave}>{isEditing ? 'Update Tool' : 'Save Tool'}</Button>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button onClick={handleSave} disabled={!isSchemaValid}>
{isEditing ? 'Update Tool' : 'Save Tool'}
</Button>
</span>
</TooltipTrigger>
{!isSchemaValid && (
<TooltipContent side='top'>
<p>Invalid JSON schema</p>
</TooltipContent>
)}
</Tooltip>
)}
</div>
</div>

View File

@@ -1320,7 +1320,7 @@ export function ToolInput({
// For custom tools, extract parameters from schema
const customToolParams =
isCustomTool && tool.schema
isCustomTool && tool.schema && tool.schema.function?.parameters?.properties
? Object.entries(tool.schema.function.parameters.properties || {}).map(
([paramId, param]: [string, any]) => ({
id: paramId,
@@ -1824,6 +1824,7 @@ export function ToolInput({
}}
onSave={editingToolIndex !== null ? handleSaveCustomTool : handleAddCustomTool}
onDelete={handleDeleteTool}
blockId={blockId}
initialValues={
editingToolIndex !== null && selectedTools[editingToolIndex]?.type === 'custom-tool'
? {

View File

@@ -23,6 +23,33 @@ const DEFAULT_FUNCTION_TIMEOUT = 5000
const REQUEST_TIMEOUT = 120000
const CUSTOM_TOOL_PREFIX = 'custom_'
/**
* Helper function to collect runtime block outputs and name mappings
* for tag resolution in custom tools and prompts
*/
function collectBlockData(context: ExecutionContext): {
blockData: Record<string, any>
blockNameMapping: Record<string, string>
} {
const blockData: Record<string, any> = {}
const blockNameMapping: Record<string, string> = {}
for (const [id, state] of context.blockStates.entries()) {
if (state.output !== undefined) {
blockData[id] = state.output
const workflowBlock = context.workflow?.blocks?.find((b) => b.id === id)
if (workflowBlock?.metadata?.name) {
// Map both the display name and normalized form
blockNameMapping[workflowBlock.metadata.name] = id
const normalized = workflowBlock.metadata.name.replace(/\s+/g, '').toLowerCase()
blockNameMapping[normalized] = id
}
}
}
return { blockData, blockNameMapping }
}
/**
* Handler for Agent blocks that process LLM requests with optional tools.
*/
@@ -172,6 +199,9 @@ export class AgentBlockHandler implements BlockHandler {
// Merge user-provided parameters with LLM-generated parameters
const mergedParams = mergeToolParameters(userProvidedParams, callParams)
// Collect block outputs for tag resolution
const { blockData, blockNameMapping } = collectBlockData(context)
const result = await executeTool(
'function_execute',
{
@@ -179,6 +209,9 @@ export class AgentBlockHandler implements BlockHandler {
...mergedParams,
timeout: tool.timeout ?? DEFAULT_FUNCTION_TIMEOUT,
envVars: context.environmentVariables || {},
workflowVariables: context.workflowVariables || {},
blockData,
blockNameMapping,
isCustomTool: true,
_context: { workflowId: context.workflowId },
},
@@ -352,6 +385,9 @@ export class AgentBlockHandler implements BlockHandler {
const validMessages = this.validateMessages(messages)
// Collect block outputs for runtime resolution
const { blockData, blockNameMapping } = collectBlockData(context)
return {
provider: providerId,
model,
@@ -368,6 +404,9 @@ export class AgentBlockHandler implements BlockHandler {
stream: streaming,
messages,
environmentVariables: context.environmentVariables || {},
workflowVariables: context.workflowVariables || {},
blockData,
blockNameMapping,
reasoningEffort: inputs.reasoningEffort,
verbosity: inputs.verbosity,
}
@@ -457,6 +496,9 @@ export class AgentBlockHandler implements BlockHandler {
const finalApiKey = this.getApiKey(providerId, model, providerRequest.apiKey)
// Collect block outputs for runtime resolution
const { blockData, blockNameMapping } = collectBlockData(context)
const response = await executeProviderRequest(providerId, {
model,
systemPrompt: 'systemPrompt' in providerRequest ? providerRequest.systemPrompt : undefined,
@@ -472,6 +514,9 @@ export class AgentBlockHandler implements BlockHandler {
stream: providerRequest.stream,
messages: 'messages' in providerRequest ? providerRequest.messages : undefined,
environmentVariables: context.environmentVariables || {},
workflowVariables: context.workflowVariables || {},
blockData,
blockNameMapping,
})
this.logExecutionSuccess(providerId, model, context, block, providerStartTime, response)

View File

@@ -77,6 +77,7 @@ describe('FunctionBlockHandler', () => {
code: inputs.code,
timeout: inputs.timeout,
envVars: {},
workflowVariables: {},
blockData: {},
blockNameMapping: {},
_context: { workflowId: mockContext.workflowId },
@@ -108,6 +109,7 @@ describe('FunctionBlockHandler', () => {
code: expectedCode,
timeout: inputs.timeout,
envVars: {},
workflowVariables: {},
blockData: {},
blockNameMapping: {},
_context: { workflowId: mockContext.workflowId },
@@ -132,6 +134,7 @@ describe('FunctionBlockHandler', () => {
code: inputs.code,
timeout: 5000, // Default timeout
envVars: {},
workflowVariables: {},
blockData: {},
blockNameMapping: {},
_context: { workflowId: mockContext.workflowId },

View File

@@ -6,6 +6,33 @@ import { executeTool } from '@/tools'
const logger = createLogger('FunctionBlockHandler')
/**
* Helper function to collect runtime block outputs and name mappings
* for tag resolution in function execution
*/
function collectBlockData(context: ExecutionContext): {
blockData: Record<string, any>
blockNameMapping: Record<string, string>
} {
const blockData: Record<string, any> = {}
const blockNameMapping: Record<string, string> = {}
for (const [id, state] of context.blockStates.entries()) {
if (state.output !== undefined) {
blockData[id] = state.output
const workflowBlock = context.workflow?.blocks?.find((b) => b.id === id)
if (workflowBlock?.metadata?.name) {
// Map both the display name and normalized form
blockNameMapping[workflowBlock.metadata.name] = id
const normalized = workflowBlock.metadata.name.replace(/\s+/g, '').toLowerCase()
blockNameMapping[normalized] = id
}
}
}
return { blockData, blockNameMapping }
}
/**
* Handler for Function blocks that execute custom code.
*/
@@ -24,20 +51,7 @@ export class FunctionBlockHandler implements BlockHandler {
: inputs.code
// Extract block data for variable resolution
const blockData: Record<string, any> = {}
const blockNameMapping: Record<string, string> = {}
for (const [blockId, blockState] of context.blockStates.entries()) {
if (blockState.output) {
blockData[blockId] = blockState.output
// Try to find the block name from the workflow
const workflowBlock = context.workflow?.blocks?.find((b) => b.id === blockId)
if (workflowBlock?.metadata?.name) {
blockNameMapping[workflowBlock.metadata.name] = blockId
}
}
}
const { blockData, blockNameMapping } = collectBlockData(context)
// Directly use the function_execute tool which calls the API route
const result = await executeTool(
@@ -46,6 +60,7 @@ export class FunctionBlockHandler implements BlockHandler {
code: codeContent,
timeout: inputs.timeout || 5000,
envVars: context.environmentVariables || {},
workflowVariables: context.workflowVariables || {},
blockData: blockData, // Pass block data for variable resolution
blockNameMapping: blockNameMapping, // Pass block name to ID mapping
_context: { workflowId: context.workflowId },

View File

@@ -222,9 +222,10 @@ export class LoopBlockHandler implements BlockHandler {
}
}
// If we have a resolver, use it to resolve any block references in the expression
// If we have a resolver, use it to resolve any variable references first, then block references
if (this.resolver) {
const resolved = this.resolver.resolveBlockReferences(forEachItems, context, block)
const resolvedVars = this.resolver.resolveVariableReferences(forEachItems, block)
const resolved = this.resolver.resolveBlockReferences(resolvedVars, context, block)
// Try to parse the resolved value
try {

View File

@@ -413,9 +413,10 @@ export class ParallelBlockHandler implements BlockHandler {
}
}
// If we have a resolver, use it to resolve any block references in the expression
// If we have a resolver, use it to resolve any variable references first, then block references
if (this.resolver) {
const resolved = this.resolver.resolveBlockReferences(distribution, context, block)
const resolvedVars = this.resolver.resolveVariableReferences(distribution, block)
const resolved = this.resolver.resolveBlockReferences(resolvedVars, context, block)
// Try to parse the resolved value
try {

View File

@@ -119,23 +119,12 @@ export class Executor {
if (options.contextExtensions) {
this.contextExtensions = options.contextExtensions
this.isChildExecution = options.contextExtensions.isChildExecution || false
if (this.contextExtensions.stream) {
logger.info('Executor initialized with streaming enabled', {
hasSelectedOutputIds: Array.isArray(this.contextExtensions.selectedOutputIds),
selectedOutputCount: Array.isArray(this.contextExtensions.selectedOutputIds)
? this.contextExtensions.selectedOutputIds.length
: 0,
selectedOutputIds: this.contextExtensions.selectedOutputIds || [],
})
}
}
} else {
this.actualWorkflow = workflowParam
if (workflowInput) {
this.workflowInput = workflowInput
logger.info('[Executor] Using workflow input:', JSON.stringify(this.workflowInput, null, 2))
} else {
this.workflowInput = {}
}
@@ -400,8 +389,7 @@ export class Executor {
try {
reader.releaseLock()
} catch (releaseError: any) {
// Reader might already be released
logger.debug('Reader already released:', releaseError)
// Reader might already be released - this is expected and safe to ignore
}
}
}
@@ -641,15 +629,12 @@ export class Executor {
* @throws Error if workflow validation fails
*/
private validateWorkflow(startBlockId?: string): void {
let validationBlock: SerializedBlock | undefined
if (startBlockId) {
// If starting from a specific block (webhook trigger or schedule trigger), validate that block exists
const startBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId)
if (!startBlock || !startBlock.enabled) {
throw new Error(`Start block ${startBlockId} not found or disabled`)
}
validationBlock = startBlock
// Trigger blocks (webhook and schedule) can have incoming connections, so no need to check that
} else {
// Default validation for starter block
@@ -659,7 +644,6 @@ export class Executor {
if (!starterBlock || !starterBlock.enabled) {
throw new Error('Workflow must have an enabled starter block')
}
validationBlock = starterBlock
const incomingToStarter = this.actualWorkflow.connections.filter(
(conn) => conn.target === starterBlock.id
@@ -741,6 +725,7 @@ export class Executor {
duration: 0, // Initialize with zero, will be updated throughout execution
},
environmentVariables: this.environmentVariables,
workflowVariables: this.workflowVariables,
decisions: {
router: new Map(),
condition: new Map(),
@@ -808,11 +793,6 @@ export class Executor {
? this.workflowInput.input[field.name] // Try to get from input.field
: this.workflowInput?.[field.name] // Fallback to direct field access
logger.info(
`[Executor] Processing input field ${field.name} (${field.type}):`,
inputValue !== undefined ? JSON.stringify(inputValue) : 'undefined'
)
if (inputValue === undefined || inputValue === null) {
if (Object.hasOwn(field, 'value')) {
inputValue = (field as any).value
@@ -873,8 +853,6 @@ export class Executor {
blockOutput.files = this.workflowInput.files
}
logger.info(`[Executor] Starting block output:`, JSON.stringify(blockOutput, null, 2))
context.blockStates.set(initBlock.id, {
output: blockOutput,
executed: true,
@@ -967,11 +945,6 @@ export class Executor {
}
}
logger.info(
'[Executor] Fallback starting block output:',
JSON.stringify(blockOutput, null, 2)
)
context.blockStates.set(initBlock.id, {
output: blockOutput,
executed: true,
@@ -1342,7 +1315,7 @@ export class Executor {
const results: (NormalizedBlockOutput | StreamingExecution)[] = []
const errors: Error[] = []
settledResults.forEach((result, index) => {
settledResults.forEach((result) => {
if (result.status === 'fulfilled') {
results.push(result.value)
} else {
@@ -1443,7 +1416,6 @@ export class Executor {
}
const addConsole = useConsoleStore.getState().addConsole
const { setActiveBlocks } = useExecutionStore.getState()
try {
if (block.enabled === false) {

View File

@@ -107,6 +107,7 @@ export interface ExecutionContext {
blockLogs: BlockLog[] // Chronological log of block executions
metadata: ExecutionMetadata // Timing metadata for the execution
environmentVariables: Record<string, string> // Environment variables available during execution
workflowVariables?: Record<string, any> // Workflow variables available during execution
// Routing decisions for path determination
decisions: {

View File

@@ -4,7 +4,7 @@ import type { StreamingExecution } from '@/executor/types'
import { executeTool } from '@/tools'
import { getProviderDefaultModel, getProviderModels } from '../models'
import type { ProviderConfig, ProviderRequest, ProviderResponse, TimeSegment } from '../types'
import { prepareToolsWithUsageControl, trackForcedToolUsage } from '../utils'
import { prepareToolExecution, prepareToolsWithUsageControl, trackForcedToolUsage } from '../utils'
const logger = createLogger('AnthropicProvider')
@@ -456,28 +456,11 @@ ${fieldDescriptions}
// Execute the tool
const toolCallStartTime = Date.now()
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
}
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId
? {
_context: {
workflowId: request.workflowId,
...(request.chatId ? { chatId: request.chatId } : {}),
...(request.userId ? { userId: request.userId } : {}),
},
}
: {}),
...(request.environmentVariables
? { envVars: request.environmentVariables }
: {}),
}
const { toolParams, executionParams } = prepareToolExecution(
tool,
toolArgs,
request
)
// Use general tool system for requests
const result = await executeTool(toolName, executionParams, true)
@@ -827,26 +810,7 @@ ${fieldDescriptions}
// Execute the tool
const toolCallStartTime = Date.now()
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
}
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId
? {
_context: {
workflowId: request.workflowId,
...(request.chatId ? { chatId: request.chatId } : {}),
...(request.userId ? { userId: request.userId } : {}),
},
}
: {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
// Use general tool system for requests
const result = await executeTool(toolName, executionParams, true)

View File

@@ -9,7 +9,11 @@ import type {
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { prepareToolsWithUsageControl, trackForcedToolUsage } from '@/providers/utils'
import {
prepareToolExecution,
prepareToolsWithUsageControl,
trackForcedToolUsage,
} from '@/providers/utils'
import { executeTool } from '@/tools'
const logger = createLogger('AzureOpenAIProvider')
@@ -383,25 +387,7 @@ export const azureOpenAIProvider: ProviderConfig = {
// Execute the tool
const toolCallStartTime = Date.now()
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
}
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId
? {
_context: {
workflowId: request.workflowId,
...(request.chatId ? { chatId: request.chatId } : {}),
},
}
: {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()

View File

@@ -8,6 +8,7 @@ import type {
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { prepareToolExecution } from '@/providers/utils'
import { executeTool } from '@/tools'
const logger = createLogger('CerebrasProvider')
@@ -287,25 +288,7 @@ export const cerebrasProvider: ProviderConfig = {
// Execute the tool
const toolCallStartTime = Date.now()
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
}
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId
? {
_context: {
workflowId: request.workflowId,
...(request.chatId ? { chatId: request.chatId } : {}),
},
}
: {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()

View File

@@ -8,7 +8,11 @@ import type {
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { prepareToolsWithUsageControl, trackForcedToolUsage } from '@/providers/utils'
import {
prepareToolExecution,
prepareToolsWithUsageControl,
trackForcedToolUsage,
} from '@/providers/utils'
import { executeTool } from '@/tools'
const logger = createLogger('DeepseekProvider')
@@ -289,25 +293,7 @@ export const deepseekProvider: ProviderConfig = {
// Execute the tool
const toolCallStartTime = Date.now()
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
}
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId
? {
_context: {
workflowId: request.workflowId,
...(request.chatId ? { chatId: request.chatId } : {}),
},
}
: {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()

View File

@@ -8,6 +8,7 @@ import type {
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { prepareToolExecution } from '@/providers/utils'
import { executeTool } from '@/tools'
const logger = createLogger('GroqProvider')
@@ -258,25 +259,7 @@ export const groqProvider: ProviderConfig = {
// Execute the tool
const toolCallStartTime = Date.now()
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
}
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId
? {
_context: {
workflowId: request.workflowId,
...(request.chatId ? { chatId: request.chatId } : {}),
},
}
: {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()

View File

@@ -152,6 +152,9 @@ export interface ProviderRequest {
stream?: boolean
streamToolCalls?: boolean // Whether to stream tool call responses back to user (default: false)
environmentVariables?: Record<string, string> // Environment variables for tool execution
workflowVariables?: Record<string, any> // Workflow variables for <variable.name> resolution
blockData?: Record<string, any> // Runtime block outputs for <block.field> resolution in custom tools
blockNameMapping?: Record<string, string> // Mapping of block names to IDs for resolution
isCopilotRequest?: boolean // Flag to indicate this request is from the copilot system
// Azure OpenAI specific parameters
azureEndpoint?: string

View File

@@ -237,8 +237,6 @@ export function generateStructuredOutputInstructions(responseFormat: any): strin
})
.join('\n')
logger.info(`Generated structured output instructions for ${responseFormat.fields.length} fields`)
return `
Please provide your response in the following JSON format:
{
@@ -323,10 +321,6 @@ export function getCustomTools(): ProviderToolConfig[] {
// Get custom tools from the store
const customTools = useCustomToolsStore.getState().getAllTools()
if (customTools.length > 0) {
logger.info(`Found ${customTools.length} custom tools`)
}
// Transform each custom tool into a provider tool config
return customTools.map(transformCustomTool)
}
@@ -912,7 +906,15 @@ export function supportsToolUsageControl(provider: string): boolean {
export function prepareToolExecution(
tool: { params?: Record<string, any> },
llmArgs: Record<string, any>,
request: { workflowId?: string; chatId?: string; environmentVariables?: Record<string, any> }
request: {
workflowId?: string
chatId?: string
userId?: string
environmentVariables?: Record<string, any>
workflowVariables?: Record<string, any>
blockData?: Record<string, any>
blockNameMapping?: Record<string, string>
}
): {
toolParams: Record<string, any>
executionParams: Record<string, any>
@@ -931,10 +933,14 @@ export function prepareToolExecution(
_context: {
workflowId: request.workflowId,
...(request.chatId ? { chatId: request.chatId } : {}),
...(request.userId ? { userId: request.userId } : {}),
},
}
: {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
...(request.workflowVariables ? { workflowVariables: request.workflowVariables } : {}),
...(request.blockData ? { blockData: request.blockData } : {}),
...(request.blockNameMapping ? { blockNameMapping: request.blockNameMapping } : {}),
}
return { toolParams, executionParams }

View File

@@ -8,7 +8,11 @@ import type {
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { prepareToolsWithUsageControl, trackForcedToolUsage } from '@/providers/utils'
import {
prepareToolExecution,
prepareToolsWithUsageControl,
trackForcedToolUsage,
} from '@/providers/utils'
import { executeTool } from '@/tools'
const logger = createLogger('XAIProvider')
@@ -325,25 +329,7 @@ export const xAIProvider: ProviderConfig = {
const toolCallStartTime = Date.now()
// Only merge actual tool parameters for logging
const toolParams = {
...tool.params,
...toolArgs,
}
// Add system parameters for execution
const executionParams = {
...toolParams,
...(request.workflowId
? {
_context: {
workflowId: request.workflowId,
...(request.chatId ? { chatId: request.chatId } : {}),
},
}
: {}),
...(request.environmentVariables ? { envVars: request.environmentVariables } : {}),
}
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()

View File

@@ -50,6 +50,7 @@ describe('Function Execute Tool', () => {
expect(body).toEqual({
code: 'return 42',
envVars: {},
workflowVariables: {},
blockData: {},
blockNameMapping: {},
isCustomTool: false,
@@ -75,6 +76,7 @@ describe('Function Execute Tool', () => {
code: 'const x = 40;\nconst y = 2;\nreturn x + y;',
timeout: 10000,
envVars: {},
workflowVariables: {},
blockData: {},
blockNameMapping: {},
isCustomTool: false,
@@ -91,6 +93,7 @@ describe('Function Execute Tool', () => {
code: 'return 42',
timeout: 10000,
envVars: {},
workflowVariables: {},
blockData: {},
blockNameMapping: {},
isCustomTool: false,

View File

@@ -45,6 +45,13 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
description: 'Mapping of block names to block IDs',
default: {},
},
workflowVariables: {
type: 'object',
required: false,
visibility: 'user-only',
description: 'Workflow variables for <variable.name> resolution',
default: {},
},
},
request: {
@@ -62,6 +69,7 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
code: codeContent,
timeout: params.timeout || DEFAULT_TIMEOUT,
envVars: params.envVars || {},
workflowVariables: params.workflowVariables || {},
blockData: params.blockData || {},
blockNameMapping: params.blockNameMapping || {},
workflowId: params._context?.workflowId,

View File

@@ -5,6 +5,7 @@ export interface CodeExecutionInput {
timeout?: number
memoryLimit?: number
envVars?: Record<string, string>
workflowVariables?: Record<string, any>
blockData?: Record<string, any>
blockNameMapping?: Record<string, string>
_context?: {

View File

@@ -40,16 +40,11 @@ async function processFileOutputs(
return result
}
logger.info(`File processing for tool ${tool.id}: checking outputs`, Object.keys(result.output))
const processedOutput = await FileToolProcessor.processToolOutputs(
result.output,
tool,
executionContext
)
logger.info(
`File processing for tool ${tool.id}: processed outputs`,
Object.keys(processedOutput)
)
return {
...result,
@@ -536,7 +531,14 @@ function validateClientSideParams(
}
// Internal parameters that should be excluded from validation
const internalParamSet = new Set(['_context', 'workflowId', 'envVars'])
const internalParamSet = new Set([
'_context',
'workflowId',
'envVars',
'workflowVariables',
'blockData',
'blockNameMapping',
])
// Check required parameters
if (schema.required) {

View File

@@ -658,6 +658,9 @@ describe('createCustomToolRequestBody', () => {
BASE_URL: 'https://example.com',
},
workflowId: undefined,
workflowVariables: {},
blockData: {},
blockNameMapping: {},
isCustomTool: true,
})
})
@@ -682,6 +685,9 @@ describe('createCustomToolRequestBody', () => {
schema: { type: 'object', properties: {} },
envVars: {},
workflowId: 'test-workflow-123',
workflowVariables: {},
blockData: {},
blockNameMapping: {},
isCustomTool: true,
})
})

View File

@@ -242,12 +242,22 @@ export function createCustomToolRequestBody(
// 3. Empty object (fallback)
const envVars = params.envVars || (isClient ? getClientEnvVars(getStore) : {})
// Get workflow variables from params (passed from execution context)
const workflowVariables = params.workflowVariables || {}
// Get block data and mapping from params (passed from execution context)
const blockData = params.blockData || {}
const blockNameMapping = params.blockNameMapping || {}
// Include everything needed for execution
return {
code: customTool.code,
params: params, // These will be available in the VM context
schema: customTool.schema.function.parameters, // For validation
envVars: envVars, // Environment variables
workflowVariables: workflowVariables, // Workflow variables for <variable.name> resolution
blockData: blockData, // Runtime block outputs for <block.field> resolution
blockNameMapping: blockNameMapping, // Block name to ID mapping
workflowId: workflowId, // Pass workflowId for server-side context
isCustomTool: true, // Flag to indicate this is a custom tool execution
}