mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix(subflow): fixed subflow execution regardless of path decision (#707)
* fix typo in docs file * fix(subflows): fixed subflows executing irrespective of active path * added routing strategy * reorganized executor * brought folder renaming inline * cleanup
This commit is contained in:
@@ -24,8 +24,8 @@ interface FolderContextMenuProps {
|
||||
folderId: string
|
||||
folderName: string
|
||||
onCreateWorkflow: (folderId: string) => void
|
||||
onRename?: (folderId: string, newName: string) => void
|
||||
onDelete?: (folderId: string) => void
|
||||
onStartEdit?: () => void
|
||||
level: number
|
||||
}
|
||||
|
||||
@@ -33,23 +33,20 @@ export function FolderContextMenu({
|
||||
folderId,
|
||||
folderName,
|
||||
onCreateWorkflow,
|
||||
onRename,
|
||||
onDelete,
|
||||
onStartEdit,
|
||||
level,
|
||||
}: FolderContextMenuProps) {
|
||||
const [showSubfolderDialog, setShowSubfolderDialog] = useState(false)
|
||||
const [showRenameDialog, setShowRenameDialog] = useState(false)
|
||||
const [subfolderName, setSubfolderName] = useState('')
|
||||
const [renameName, setRenameName] = useState(folderName)
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
// Get user permissions for the workspace
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
|
||||
const { createFolder, updateFolder, deleteFolder } = useFolderStore()
|
||||
const { createFolder, deleteFolder } = useFolderStore()
|
||||
|
||||
const handleCreateWorkflow = () => {
|
||||
onCreateWorkflow(folderId)
|
||||
@@ -60,8 +57,9 @@ export function FolderContextMenu({
|
||||
}
|
||||
|
||||
const handleRename = () => {
|
||||
setRenameName(folderName)
|
||||
setShowRenameDialog(true)
|
||||
if (onStartEdit) {
|
||||
onStartEdit()
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
@@ -98,31 +96,9 @@ export function FolderContextMenu({
|
||||
}
|
||||
}
|
||||
|
||||
const handleRenameSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!renameName.trim()) return
|
||||
|
||||
setIsRenaming(true)
|
||||
try {
|
||||
if (onRename) {
|
||||
onRename(folderId, renameName.trim())
|
||||
} else {
|
||||
// Default rename behavior
|
||||
await updateFolder(folderId, { name: renameName.trim() })
|
||||
}
|
||||
setShowRenameDialog(false)
|
||||
} catch (error) {
|
||||
console.error('Failed to rename folder:', error)
|
||||
} finally {
|
||||
setIsRenaming(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setSubfolderName('')
|
||||
setShowSubfolderDialog(false)
|
||||
setRenameName(folderName)
|
||||
setShowRenameDialog(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -230,37 +206,6 @@ export function FolderContextMenu({
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Rename dialog */}
|
||||
<Dialog open={showRenameDialog} onOpenChange={setShowRenameDialog}>
|
||||
<DialogContent className='sm:max-w-[425px]' onClick={(e) => e.stopPropagation()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rename Folder</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleRenameSubmit} className='space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='rename-folder'>Folder Name</Label>
|
||||
<Input
|
||||
id='rename-folder'
|
||||
value={renameName}
|
||||
onChange={(e) => setRenameName(e.target.value)}
|
||||
placeholder='Enter folder name...'
|
||||
maxLength={50}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className='flex justify-end space-x-2'>
|
||||
<Button type='button' variant='outline' onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type='submit' disabled={!renameName.trim() || isRenaming}>
|
||||
{isRenaming ? 'Renaming...' : 'Rename'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { type FolderTreeNode, useFolderStore } from '@/stores/folders/store'
|
||||
@@ -48,14 +49,33 @@ export function FolderItem({
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editValue, setEditValue] = useState(folder.name)
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const dragStartedRef = useRef(false)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const isExpanded = expandedFolders.has(folder.id)
|
||||
const updateTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
||||
const pendingStateRef = useRef<boolean | null>(null)
|
||||
|
||||
// Update editValue when folder name changes
|
||||
useEffect(() => {
|
||||
setEditValue(folder.name)
|
||||
}, [folder.name])
|
||||
|
||||
// Focus input when entering edit mode
|
||||
useEffect(() => {
|
||||
if (isEditing && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
inputRef.current.select()
|
||||
}
|
||||
}, [isEditing])
|
||||
|
||||
const handleToggleExpanded = useCallback(() => {
|
||||
if (isEditing) return // Don't toggle when editing
|
||||
|
||||
const newExpandedState = !isExpanded
|
||||
toggleExpanded(folder.id)
|
||||
pendingStateRef.current = newExpandedState
|
||||
@@ -73,9 +93,11 @@ export function FolderItem({
|
||||
})
|
||||
}
|
||||
}, 300)
|
||||
}, [folder.id, isExpanded, toggleExpanded, updateFolderAPI])
|
||||
}, [folder.id, isExpanded, toggleExpanded, updateFolderAPI, isEditing])
|
||||
|
||||
const handleDragStart = (e: React.DragEvent) => {
|
||||
if (isEditing) return
|
||||
|
||||
dragStartedRef.current = true
|
||||
setIsDragging(true)
|
||||
|
||||
@@ -101,7 +123,7 @@ export function FolderItem({
|
||||
}
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (dragStartedRef.current) {
|
||||
if (dragStartedRef.current || isEditing) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
@@ -116,15 +138,57 @@ export function FolderItem({
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleRename = async (folderId: string, newName: string) => {
|
||||
const handleStartEdit = () => {
|
||||
setIsEditing(true)
|
||||
setEditValue(folder.name)
|
||||
}
|
||||
|
||||
const handleSaveEdit = async () => {
|
||||
if (!editValue.trim() || editValue.trim() === folder.name) {
|
||||
setIsEditing(false)
|
||||
setEditValue(folder.name)
|
||||
return
|
||||
}
|
||||
|
||||
setIsRenaming(true)
|
||||
try {
|
||||
await updateFolderAPI(folderId, { name: newName })
|
||||
await updateFolderAPI(folder.id, { name: editValue.trim() })
|
||||
logger.info(`Successfully renamed folder from "${folder.name}" to "${editValue.trim()}"`)
|
||||
setIsEditing(false)
|
||||
} catch (error) {
|
||||
logger.error('Failed to rename folder:', { error })
|
||||
logger.error('Failed to rename folder:', {
|
||||
error,
|
||||
folderId: folder.id,
|
||||
oldName: folder.name,
|
||||
newName: editValue.trim(),
|
||||
})
|
||||
// Reset to original name on error
|
||||
setEditValue(folder.name)
|
||||
} finally {
|
||||
setIsRenaming(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (folderId: string) => {
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditing(false)
|
||||
setEditValue(folder.name)
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleSaveEdit()
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
handleCancelEdit()
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputBlur = () => {
|
||||
handleSaveEdit()
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
setShowDeleteDialog(true)
|
||||
}
|
||||
|
||||
@@ -154,7 +218,7 @@ export function FolderItem({
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
onClick={handleClick}
|
||||
draggable={true}
|
||||
draggable={!isEditing}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
@@ -222,7 +286,7 @@ export function FolderItem({
|
||||
maxWidth: isFirstItem ? `${164 - level * 20}px` : `${206 - level * 20}px`,
|
||||
}}
|
||||
onClick={handleClick}
|
||||
draggable={true}
|
||||
draggable={!isEditing}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
@@ -234,18 +298,34 @@ export function FolderItem({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className='flex-1 select-none truncate text-muted-foreground'>{folder.name}</span>
|
||||
|
||||
<div className='flex items-center justify-center' onClick={(e) => e.stopPropagation()}>
|
||||
<FolderContextMenu
|
||||
folderId={folder.id}
|
||||
folderName={folder.name}
|
||||
onCreateWorkflow={onCreateWorkflow}
|
||||
onRename={handleRename}
|
||||
onDelete={handleDelete}
|
||||
level={level}
|
||||
{isEditing ? (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleInputBlur}
|
||||
className='h-6 flex-1 border-0 bg-transparent p-0 text-muted-foreground text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
maxLength={50}
|
||||
disabled={isRenaming}
|
||||
onClick={(e) => e.stopPropagation()} // Prevent folder toggle when clicking input
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className='flex-1 select-none truncate text-muted-foreground'>{folder.name}</span>
|
||||
)}
|
||||
|
||||
{!isEditing && (
|
||||
<div className='flex items-center justify-center' onClick={(e) => e.stopPropagation()}>
|
||||
<FolderContextMenu
|
||||
folderId={folder.id}
|
||||
folderName={folder.name}
|
||||
onCreateWorkflow={onCreateWorkflow}
|
||||
onDelete={handleDelete}
|
||||
onStartEdit={handleStartEdit}
|
||||
level={level}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
description: 'Manage Google Calendar events',
|
||||
longDescription:
|
||||
"Integrate Google Calendar functionality to create, read, update, and list calendar events within your workflow. Automate scheduling, check availability, and manage events using OAuth authentication. Email invitations are sent asynchronously and delivery depends on recipients' Google Calendar settings.",
|
||||
docsLink: 'https://docs.simstudio.ai/tools/google-calendar',
|
||||
docsLink: 'https://docs.simstudio.ai/tools/google_calendar',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: GoogleCalendarIcon,
|
||||
|
||||
29
apps/sim/executor/consts.ts
Normal file
29
apps/sim/executor/consts.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Enum defining all supported block types in the executor.
|
||||
* This centralizes block type definitions and eliminates magic strings.
|
||||
*/
|
||||
export enum BlockType {
|
||||
PARALLEL = 'parallel',
|
||||
LOOP = 'loop',
|
||||
ROUTER = 'router',
|
||||
CONDITION = 'condition',
|
||||
FUNCTION = 'function',
|
||||
AGENT = 'agent',
|
||||
API = 'api',
|
||||
EVALUATOR = 'evaluator',
|
||||
RESPONSE = 'response',
|
||||
WORKFLOW = 'workflow',
|
||||
STARTER = 'starter',
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of all block types for iteration and validation
|
||||
*/
|
||||
export const ALL_BLOCK_TYPES = Object.values(BlockType) as string[]
|
||||
|
||||
/**
|
||||
* Type guard to check if a string is a valid block type
|
||||
*/
|
||||
export function isValidBlockType(type: string): type is BlockType {
|
||||
return ALL_BLOCK_TYPES.includes(type)
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
|
||||
import { isHosted } from '@/lib/environment'
|
||||
import { getAllBlocks } from '@/blocks'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler'
|
||||
import type { ExecutionContext, StreamingExecution } from '@/executor/types'
|
||||
import { executeProviderRequest } from '@/providers'
|
||||
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
import type { ExecutionContext, StreamingExecution } from '../../types'
|
||||
import { AgentBlockHandler } from './agent-handler'
|
||||
|
||||
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
|
||||
|
||||
@@ -88,8 +89,8 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
mockBlock = {
|
||||
id: 'test-agent-block',
|
||||
metadata: { id: 'agent', name: 'Test Agent' },
|
||||
type: 'agent',
|
||||
metadata: { id: BlockType.AGENT, name: 'Test Agent' },
|
||||
type: BlockType.AGENT,
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'mock-tool',
|
||||
@@ -908,7 +909,7 @@ describe('AgentBlockHandler', () => {
|
||||
logs: [
|
||||
{
|
||||
blockId: 'some-id',
|
||||
blockType: 'agent',
|
||||
blockType: BlockType.AGENT,
|
||||
startedAt: new Date().toISOString(),
|
||||
endedAt: new Date().toISOString(),
|
||||
durationMs: 100,
|
||||
@@ -959,7 +960,7 @@ describe('AgentBlockHandler', () => {
|
||||
const logs = (result as StreamingExecution).execution.logs
|
||||
expect(logs?.length).toBe(1)
|
||||
if (logs && logs.length > 0 && logs[0]) {
|
||||
expect(logs[0].blockType).toBe('agent')
|
||||
expect(logs[0].blockType).toBe(BlockType.AGENT)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1069,7 +1070,7 @@ describe('AgentBlockHandler', () => {
|
||||
memories: [
|
||||
{
|
||||
key: 'conversation-1',
|
||||
type: 'agent',
|
||||
type: BlockType.AGENT,
|
||||
data: [
|
||||
{ role: 'user', content: 'Hi there!' },
|
||||
{ role: 'assistant', content: 'Hello! How can I help you?' },
|
||||
|
||||
@@ -2,13 +2,19 @@ import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getAllBlocks } from '@/blocks'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import type {
|
||||
AgentInputs,
|
||||
Message,
|
||||
StreamingConfig,
|
||||
ToolInput,
|
||||
} from '@/executor/handlers/agent/types'
|
||||
import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/executor/types'
|
||||
import { executeProviderRequest } from '@/providers'
|
||||
import { getApiKey, getProviderFromModel, transformBlockTool } from '@/providers/utils'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
import { getTool, getToolAsync } from '@/tools/utils'
|
||||
import type { BlockHandler, ExecutionContext, StreamingExecution } from '../../types'
|
||||
import type { AgentInputs, Message, StreamingConfig, ToolInput } from './types'
|
||||
|
||||
const logger = createLogger('AgentBlockHandler')
|
||||
|
||||
@@ -22,7 +28,7 @@ const CUSTOM_TOOL_PREFIX = 'custom_'
|
||||
*/
|
||||
export class AgentBlockHandler implements BlockHandler {
|
||||
canHandle(block: SerializedBlock): boolean {
|
||||
return block.metadata?.id === 'agent'
|
||||
return block.metadata?.id === BlockType.AGENT
|
||||
}
|
||||
|
||||
async execute(
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import '../../__test-utils__/mock-dependencies'
|
||||
|
||||
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { ApiBlockHandler } from '@/executor/handlers/api/api-handler'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { getTool } from '@/tools/utils'
|
||||
import type { ExecutionContext } from '../../types'
|
||||
import { ApiBlockHandler } from './api-handler'
|
||||
|
||||
const mockGetTool = vi.mocked(getTool)
|
||||
const mockExecuteTool = executeTool as Mock
|
||||
@@ -21,7 +22,7 @@ describe('ApiBlockHandler', () => {
|
||||
handler = new ApiBlockHandler()
|
||||
mockBlock = {
|
||||
id: 'api-block-1',
|
||||
metadata: { id: 'api', name: 'Test API Block' },
|
||||
metadata: { id: BlockType.API, name: 'Test API Block' },
|
||||
position: { x: 10, y: 10 },
|
||||
config: { tool: 'http_request', params: {} },
|
||||
inputs: {},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
import { getTool } from '@/tools/utils'
|
||||
import type { BlockHandler, ExecutionContext } from '../../types'
|
||||
|
||||
const logger = createLogger('ApiBlockHandler')
|
||||
|
||||
@@ -11,7 +12,7 @@ const logger = createLogger('ApiBlockHandler')
|
||||
*/
|
||||
export class ApiBlockHandler implements BlockHandler {
|
||||
canHandle(block: SerializedBlock): boolean {
|
||||
return block.metadata?.id === 'api'
|
||||
return block.metadata?.id === BlockType.API
|
||||
}
|
||||
|
||||
async execute(
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import '../../__test-utils__/mock-dependencies'
|
||||
|
||||
import { beforeEach, describe, expect, it, type Mocked, type MockedClass, vi } from 'vitest'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-handler'
|
||||
import { PathTracker } from '@/executor/path/path'
|
||||
import { InputResolver } from '@/executor/resolver/resolver'
|
||||
import type { BlockState, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
import { PathTracker } from '../../path'
|
||||
import { InputResolver } from '../../resolver'
|
||||
import type { BlockState, ExecutionContext } from '../../types'
|
||||
import { ConditionBlockHandler } from './condition-handler'
|
||||
|
||||
const MockPathTracker = PathTracker as MockedClass<typeof PathTracker>
|
||||
const MockInputResolver = InputResolver as MockedClass<typeof InputResolver>
|
||||
@@ -34,9 +35,9 @@ describe('ConditionBlockHandler', () => {
|
||||
}
|
||||
mockBlock = {
|
||||
id: 'cond-block-1',
|
||||
metadata: { id: 'condition', name: 'Test Condition' },
|
||||
metadata: { id: BlockType.CONDITION, name: 'Test Condition' },
|
||||
position: { x: 50, y: 50 },
|
||||
config: { tool: 'condition', params: {} },
|
||||
config: { tool: BlockType.CONDITION, params: {} },
|
||||
inputs: { conditions: 'json' }, // Corrected based on previous step
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import type { PathTracker } from '@/executor/path/path'
|
||||
import type { InputResolver } from '@/executor/resolver/resolver'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import type { PathTracker } from '../../path'
|
||||
import type { InputResolver } from '../../resolver'
|
||||
import type { BlockHandler, ExecutionContext } from '../../types'
|
||||
|
||||
const logger = createLogger('ConditionBlockHandler')
|
||||
|
||||
@@ -21,7 +22,7 @@ export class ConditionBlockHandler implements BlockHandler {
|
||||
) {}
|
||||
|
||||
canHandle(block: SerializedBlock): boolean {
|
||||
return block.metadata?.id === 'condition'
|
||||
return block.metadata?.id === BlockType.CONDITION
|
||||
}
|
||||
|
||||
async execute(
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import '../../__test-utils__/mock-dependencies'
|
||||
|
||||
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import { getProviderFromModel } from '@/providers/utils'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import type { ExecutionContext } from '../../types'
|
||||
import { EvaluatorBlockHandler } from './evaluator-handler'
|
||||
|
||||
const mockGetProviderFromModel = getProviderFromModel as Mock
|
||||
const mockFetch = global.fetch as unknown as Mock
|
||||
@@ -19,9 +20,9 @@ describe('EvaluatorBlockHandler', () => {
|
||||
|
||||
mockBlock = {
|
||||
id: 'eval-block-1',
|
||||
metadata: { id: 'evaluator', name: 'Test Evaluator' },
|
||||
metadata: { id: BlockType.EVALUATOR, name: 'Test Evaluator' },
|
||||
position: { x: 20, y: 20 },
|
||||
config: { tool: 'evaluator', params: {} },
|
||||
config: { tool: BlockType.EVALUATOR, params: {} },
|
||||
inputs: {
|
||||
content: 'string',
|
||||
metrics: 'json',
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import { calculateCost, getProviderFromModel } from '@/providers/utils'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import type { BlockHandler, ExecutionContext } from '../../types'
|
||||
|
||||
const logger = createLogger('EvaluatorBlockHandler')
|
||||
|
||||
@@ -12,7 +13,7 @@ const logger = createLogger('EvaluatorBlockHandler')
|
||||
*/
|
||||
export class EvaluatorBlockHandler implements BlockHandler {
|
||||
canHandle(block: SerializedBlock): boolean {
|
||||
return block.metadata?.id === 'evaluator'
|
||||
return block.metadata?.id === BlockType.EVALUATOR
|
||||
}
|
||||
|
||||
async execute(
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
import type { ExecutionContext } from '../../types'
|
||||
import { FunctionBlockHandler } from './function-handler'
|
||||
|
||||
vi.mock('@/lib/logs/console-logger', () => ({
|
||||
createLogger: vi.fn(() => ({
|
||||
@@ -29,9 +30,9 @@ describe('FunctionBlockHandler', () => {
|
||||
|
||||
mockBlock = {
|
||||
id: 'func-block-1',
|
||||
metadata: { id: 'function', name: 'Test Function' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Test Function' },
|
||||
position: { x: 30, y: 30 },
|
||||
config: { tool: 'function', params: {} },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: { code: 'string', timeout: 'number' }, // Using ParamType strings
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
import type { BlockHandler, ExecutionContext } from '../../types'
|
||||
|
||||
const logger = createLogger('FunctionBlockHandler')
|
||||
|
||||
@@ -10,7 +11,7 @@ const logger = createLogger('FunctionBlockHandler')
|
||||
*/
|
||||
export class FunctionBlockHandler implements BlockHandler {
|
||||
canHandle(block: SerializedBlock): boolean {
|
||||
return block.metadata?.id === 'function'
|
||||
return block.metadata?.id === BlockType.FUNCTION
|
||||
}
|
||||
|
||||
async execute(
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import '../../__test-utils__/mock-dependencies'
|
||||
|
||||
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { getTool } from '@/tools/utils'
|
||||
import type { ExecutionContext } from '../../types'
|
||||
import { GenericBlockHandler } from './generic-handler'
|
||||
|
||||
const mockGetTool = vi.mocked(getTool)
|
||||
const mockExecuteTool = executeTool as Mock
|
||||
@@ -74,7 +75,7 @@ describe('GenericBlockHandler', () => {
|
||||
})
|
||||
|
||||
it.concurrent('should always handle any block type', () => {
|
||||
const agentBlock: SerializedBlock = { ...mockBlock, metadata: { id: 'agent' } }
|
||||
const agentBlock: SerializedBlock = { ...mockBlock, metadata: { id: BlockType.AGENT } }
|
||||
expect(handler.canHandle(agentBlock)).toBe(true)
|
||||
expect(handler.canHandle(mockBlock)).toBe(true)
|
||||
const noMetaIdBlock: SerializedBlock = { ...mockBlock, metadata: undefined }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
import { getTool } from '@/tools/utils'
|
||||
import type { BlockHandler, ExecutionContext } from '../../types'
|
||||
|
||||
const logger = createLogger('GenericBlockHandler')
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { AgentBlockHandler } from './agent/agent-handler'
|
||||
import { ApiBlockHandler } from './api/api-handler'
|
||||
import { ConditionBlockHandler } from './condition/condition-handler'
|
||||
import { EvaluatorBlockHandler } from './evaluator/evaluator-handler'
|
||||
import { FunctionBlockHandler } from './function/function-handler'
|
||||
import { GenericBlockHandler } from './generic/generic-handler'
|
||||
import { LoopBlockHandler } from './loop/loop-handler'
|
||||
import { ParallelBlockHandler } from './parallel/parallel-handler'
|
||||
import { ResponseBlockHandler } from './response/response-handler'
|
||||
import { RouterBlockHandler } from './router/router-handler'
|
||||
import { WorkflowBlockHandler } from './workflow/workflow-handler'
|
||||
import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler'
|
||||
import { ApiBlockHandler } from '@/executor/handlers/api/api-handler'
|
||||
import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-handler'
|
||||
import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler'
|
||||
import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler'
|
||||
import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler'
|
||||
import { LoopBlockHandler } from '@/executor/handlers/loop/loop-handler'
|
||||
import { ParallelBlockHandler } from '@/executor/handlers/parallel/parallel-handler'
|
||||
import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler'
|
||||
import { RouterBlockHandler } from '@/executor/handlers/router/router-handler'
|
||||
import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler'
|
||||
|
||||
export {
|
||||
AgentBlockHandler,
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import { vi } from 'vitest'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { LoopBlockHandler } from '@/executor/handlers/loop/loop-handler'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import type { ExecutionContext } from '../../types'
|
||||
import { LoopBlockHandler } from './loop-handler'
|
||||
|
||||
describe('LoopBlockHandler', () => {
|
||||
let handler: LoopBlockHandler
|
||||
let mockContext: ExecutionContext
|
||||
let mockBlock: SerializedBlock
|
||||
|
||||
const mockPathTracker = {
|
||||
isInActivePath: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new LoopBlockHandler()
|
||||
|
||||
mockBlock = {
|
||||
id: 'loop-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'loop', params: {} },
|
||||
config: { tool: BlockType.LOOP, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'loop', name: 'Test Loop' },
|
||||
metadata: { id: BlockType.LOOP, name: 'Test Loop' },
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
@@ -66,7 +72,7 @@ describe('LoopBlockHandler', () => {
|
||||
|
||||
it('should not handle non-loop blocks', () => {
|
||||
if (mockBlock.metadata) {
|
||||
mockBlock.metadata.id = 'function'
|
||||
mockBlock.metadata.id = BlockType.FUNCTION
|
||||
}
|
||||
expect(handler.canHandle(mockBlock)).toBe(false)
|
||||
})
|
||||
@@ -212,4 +218,46 @@ describe('LoopBlockHandler', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PathTracker integration', () => {
|
||||
it('should activate children when in active path', async () => {
|
||||
const handlerWithPathTracker = new LoopBlockHandler(undefined, mockPathTracker as any)
|
||||
|
||||
// Mock PathTracker to return true (block is in active path)
|
||||
mockPathTracker.isInActivePath.mockReturnValue(true)
|
||||
|
||||
await handlerWithPathTracker.execute(mockBlock, {}, mockContext)
|
||||
|
||||
// Should activate children when in active path
|
||||
expect(mockContext.activeExecutionPath.has('inner-block')).toBe(true)
|
||||
expect(mockPathTracker.isInActivePath).toHaveBeenCalledWith('loop-1', mockContext)
|
||||
})
|
||||
|
||||
it('should not activate children when not in active path', async () => {
|
||||
const handlerWithPathTracker = new LoopBlockHandler(undefined, mockPathTracker as any)
|
||||
|
||||
// Mock PathTracker to return false (block is not in active path)
|
||||
mockPathTracker.isInActivePath.mockReturnValue(false)
|
||||
|
||||
await handlerWithPathTracker.execute(mockBlock, {}, mockContext)
|
||||
|
||||
// Should not activate children when not in active path
|
||||
expect(mockContext.activeExecutionPath.has('inner-block')).toBe(false)
|
||||
expect(mockPathTracker.isInActivePath).toHaveBeenCalledWith('loop-1', mockContext)
|
||||
})
|
||||
|
||||
it('should handle PathTracker errors gracefully', async () => {
|
||||
const handlerWithPathTracker = new LoopBlockHandler(undefined, mockPathTracker as any)
|
||||
|
||||
// Mock PathTracker to throw error
|
||||
mockPathTracker.isInActivePath.mockImplementation(() => {
|
||||
throw new Error('PathTracker error')
|
||||
})
|
||||
|
||||
await handlerWithPathTracker.execute(mockBlock, {}, mockContext)
|
||||
|
||||
// Should default to activating children when PathTracker fails
|
||||
expect(mockContext.activeExecutionPath.has('inner-block')).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import type { PathTracker } from '@/executor/path/path'
|
||||
import type { InputResolver } from '@/executor/resolver/resolver'
|
||||
import { Routing } from '@/executor/routing/routing'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import type { InputResolver } from '../../resolver'
|
||||
import type { BlockHandler, ExecutionContext } from '../../types'
|
||||
|
||||
const logger = createLogger('LoopBlockHandler')
|
||||
|
||||
@@ -13,10 +16,13 @@ const DEFAULT_MAX_ITERATIONS = 5
|
||||
* Loop blocks don't execute logic themselves but control the flow of blocks within them.
|
||||
*/
|
||||
export class LoopBlockHandler implements BlockHandler {
|
||||
constructor(private resolver?: InputResolver) {}
|
||||
constructor(
|
||||
private resolver?: InputResolver,
|
||||
private pathTracker?: PathTracker
|
||||
) {}
|
||||
|
||||
canHandle(block: SerializedBlock): boolean {
|
||||
return block.metadata?.id === 'loop'
|
||||
return block.metadata?.id === BlockType.LOOP
|
||||
}
|
||||
|
||||
async execute(
|
||||
@@ -126,15 +132,31 @@ export class LoopBlockHandler implements BlockHandler {
|
||||
`Loop ${block.id} - Incremented counter for next iteration: ${currentIteration + 1}`
|
||||
)
|
||||
|
||||
// Loop is still active, activate the loop-start-source connection
|
||||
const loopStartConnections =
|
||||
context.workflow?.connections.filter(
|
||||
(conn) => conn.source === block.id && conn.sourceHandle === 'loop-start-source'
|
||||
) || []
|
||||
// Use routing strategy to determine if this block requires active path checking
|
||||
const blockType = block.metadata?.id
|
||||
if (Routing.requiresActivePathCheck(blockType || '')) {
|
||||
let isInActivePath = true
|
||||
if (this.pathTracker) {
|
||||
try {
|
||||
isInActivePath = this.pathTracker.isInActivePath(block.id, context)
|
||||
} catch (error) {
|
||||
logger.warn(`PathTracker check failed for ${blockType} block ${block.id}:`, error)
|
||||
// Default to true to maintain existing behavior if PathTracker fails
|
||||
isInActivePath = true
|
||||
}
|
||||
}
|
||||
|
||||
for (const conn of loopStartConnections) {
|
||||
context.activeExecutionPath.add(conn.target)
|
||||
logger.info(`Activated loop start path to ${conn.target} for iteration ${currentIteration}`)
|
||||
// Only activate child nodes if this block is in the active execution path
|
||||
if (isInActivePath) {
|
||||
this.activateChildNodes(block, context, currentIteration)
|
||||
} else {
|
||||
logger.info(
|
||||
`${blockType} block ${block.id} is not in active execution path, skipping child activation`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Regular blocks always activate their children
|
||||
this.activateChildNodes(block, context, currentIteration)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -147,6 +169,26 @@ export class LoopBlockHandler implements BlockHandler {
|
||||
} as Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate child nodes for loop execution
|
||||
*/
|
||||
private activateChildNodes(
|
||||
block: SerializedBlock,
|
||||
context: ExecutionContext,
|
||||
currentIteration: number
|
||||
): void {
|
||||
// Loop is still active, activate the loop-start-source connection
|
||||
const loopStartConnections =
|
||||
context.workflow?.connections.filter(
|
||||
(conn) => conn.source === block.id && conn.sourceHandle === 'loop-start-source'
|
||||
) || []
|
||||
|
||||
for (const conn of loopStartConnections) {
|
||||
context.activeExecutionPath.add(conn.target)
|
||||
logger.info(`Activated loop start path to ${conn.target} for iteration ${currentIteration}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates forEach items expression or value
|
||||
*/
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createParallelExecutionState } from '@/executor/__test-utils__/executor-mocks'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { ParallelBlockHandler } from '@/executor/handlers/parallel/parallel-handler'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock, SerializedParallel } from '@/serializer/types'
|
||||
import { createParallelExecutionState } from '../../__test-utils__/executor-mocks'
|
||||
import type { ExecutionContext } from '../../types'
|
||||
import { ParallelBlockHandler } from './parallel-handler'
|
||||
|
||||
describe('ParallelBlockHandler', () => {
|
||||
const mockResolver = {
|
||||
resolveBlockReferences: vi.fn((expr: string) => expr),
|
||||
}
|
||||
|
||||
const mockPathTracker = {
|
||||
isInActivePath: vi.fn(),
|
||||
}
|
||||
|
||||
const createMockBlock = (id: string): SerializedBlock => ({
|
||||
id,
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: '', params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'parallel', name: 'Test Parallel' },
|
||||
metadata: { id: BlockType.PARALLEL, name: 'Test Parallel' },
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
@@ -46,7 +51,7 @@ describe('ParallelBlockHandler', () => {
|
||||
|
||||
expect(handler.canHandle(block)).toBe(true)
|
||||
|
||||
const nonParallelBlock = { ...block, metadata: { id: 'agent' } }
|
||||
const nonParallelBlock = { ...block, metadata: { id: BlockType.AGENT } }
|
||||
expect(handler.canHandle(nonParallelBlock)).toBe(false)
|
||||
})
|
||||
|
||||
@@ -394,24 +399,24 @@ describe('ParallelBlockHandler', () => {
|
||||
{
|
||||
id: 'agent-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'agent', params: {} },
|
||||
config: { tool: BlockType.AGENT, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'agent', name: 'Agent 1' },
|
||||
metadata: { id: BlockType.AGENT, name: 'Agent 1' },
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'function-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'function',
|
||||
tool: BlockType.FUNCTION,
|
||||
params: {
|
||||
code: 'return <parallel.results>;',
|
||||
},
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'function', name: 'Function 1' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function 1' },
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
@@ -481,4 +486,91 @@ describe('ParallelBlockHandler', () => {
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('PathTracker integration', () => {
|
||||
it('should activate children when in active path', async () => {
|
||||
const handler = new ParallelBlockHandler(mockResolver as any, mockPathTracker as any)
|
||||
const block = createMockBlock('parallel-1')
|
||||
const parallel = {
|
||||
id: 'parallel-1',
|
||||
nodes: ['agent-1'],
|
||||
distribution: ['item1', 'item2'],
|
||||
}
|
||||
|
||||
const context = createMockContext(parallel)
|
||||
context.workflow!.connections = [
|
||||
{
|
||||
source: 'parallel-1',
|
||||
target: 'agent-1',
|
||||
sourceHandle: 'parallel-start-source',
|
||||
},
|
||||
]
|
||||
|
||||
// Mock PathTracker to return true (block is in active path)
|
||||
mockPathTracker.isInActivePath.mockReturnValue(true)
|
||||
|
||||
await handler.execute(block, {}, context)
|
||||
|
||||
// Should activate children when in active path
|
||||
expect(context.activeExecutionPath.has('agent-1')).toBe(true)
|
||||
expect(mockPathTracker.isInActivePath).toHaveBeenCalledWith('parallel-1', context)
|
||||
})
|
||||
|
||||
it('should not activate children when not in active path', async () => {
|
||||
const handler = new ParallelBlockHandler(mockResolver as any, mockPathTracker as any)
|
||||
const block = createMockBlock('parallel-1')
|
||||
const parallel = {
|
||||
id: 'parallel-1',
|
||||
nodes: ['agent-1'],
|
||||
distribution: ['item1', 'item2'],
|
||||
}
|
||||
|
||||
const context = createMockContext(parallel)
|
||||
context.workflow!.connections = [
|
||||
{
|
||||
source: 'parallel-1',
|
||||
target: 'agent-1',
|
||||
sourceHandle: 'parallel-start-source',
|
||||
},
|
||||
]
|
||||
|
||||
// Mock PathTracker to return false (block is not in active path)
|
||||
mockPathTracker.isInActivePath.mockReturnValue(false)
|
||||
|
||||
await handler.execute(block, {}, context)
|
||||
|
||||
// Should not activate children when not in active path
|
||||
expect(context.activeExecutionPath.has('agent-1')).toBe(false)
|
||||
expect(mockPathTracker.isInActivePath).toHaveBeenCalledWith('parallel-1', context)
|
||||
})
|
||||
|
||||
it('should handle PathTracker errors gracefully', async () => {
|
||||
const handler = new ParallelBlockHandler(mockResolver as any, mockPathTracker as any)
|
||||
const block = createMockBlock('parallel-1')
|
||||
const parallel = {
|
||||
id: 'parallel-1',
|
||||
nodes: ['agent-1'],
|
||||
distribution: ['item1', 'item2'],
|
||||
}
|
||||
|
||||
const context = createMockContext(parallel)
|
||||
context.workflow!.connections = [
|
||||
{
|
||||
source: 'parallel-1',
|
||||
target: 'agent-1',
|
||||
sourceHandle: 'parallel-start-source',
|
||||
},
|
||||
]
|
||||
|
||||
// Mock PathTracker to throw error
|
||||
mockPathTracker.isInActivePath.mockImplementation(() => {
|
||||
throw new Error('PathTracker error')
|
||||
})
|
||||
|
||||
await handler.execute(block, {}, context)
|
||||
|
||||
// Should default to activating children when PathTracker fails
|
||||
expect(context.activeExecutionPath.has('agent-1')).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import type { PathTracker } from '@/executor/path/path'
|
||||
import type { InputResolver } from '@/executor/resolver/resolver'
|
||||
import { Routing } from '@/executor/routing/routing'
|
||||
import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import type { InputResolver } from '../../resolver'
|
||||
import type { BlockHandler, ExecutionContext, StreamingExecution } from '../../types'
|
||||
|
||||
const logger = createLogger('ParallelBlockHandler')
|
||||
|
||||
@@ -12,10 +15,13 @@ const logger = createLogger('ParallelBlockHandler')
|
||||
* create virtual instances for true parallel execution.
|
||||
*/
|
||||
export class ParallelBlockHandler implements BlockHandler {
|
||||
constructor(private resolver?: InputResolver) {}
|
||||
constructor(
|
||||
private resolver?: InputResolver,
|
||||
private pathTracker?: PathTracker
|
||||
) {}
|
||||
|
||||
canHandle(block: SerializedBlock): boolean {
|
||||
return block.metadata?.id === 'parallel'
|
||||
return block.metadata?.id === BlockType.PARALLEL
|
||||
}
|
||||
|
||||
async execute(
|
||||
@@ -185,15 +191,31 @@ export class ParallelBlockHandler implements BlockHandler {
|
||||
}
|
||||
// Note: For simple count-based parallels without distribution, we don't store items
|
||||
|
||||
// Activate all child nodes (the executor will handle creating virtual instances)
|
||||
const parallelStartConnections =
|
||||
context.workflow?.connections.filter(
|
||||
(conn) => conn.source === block.id && conn.sourceHandle === 'parallel-start-source'
|
||||
) || []
|
||||
// Use routing strategy to determine if this block requires active path checking
|
||||
const blockType = block.metadata?.id
|
||||
if (Routing.requiresActivePathCheck(blockType || '')) {
|
||||
let isInActivePath = true
|
||||
if (this.pathTracker) {
|
||||
try {
|
||||
isInActivePath = this.pathTracker.isInActivePath(block.id, context)
|
||||
} catch (error) {
|
||||
logger.warn(`PathTracker check failed for ${blockType} block ${block.id}:`, error)
|
||||
// Default to true to maintain existing behavior if PathTracker fails
|
||||
isInActivePath = true
|
||||
}
|
||||
}
|
||||
|
||||
for (const conn of parallelStartConnections) {
|
||||
context.activeExecutionPath.add(conn.target)
|
||||
logger.info(`Activated parallel path to ${conn.target}`)
|
||||
// Only activate child nodes if this block is in the active execution path
|
||||
if (isInActivePath) {
|
||||
this.activateChildNodes(block, context)
|
||||
} else {
|
||||
logger.info(
|
||||
`${blockType} block ${block.id} is not in active execution path, skipping child activation`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Regular blocks always activate their children
|
||||
this.activateChildNodes(block, context)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -291,6 +313,22 @@ export class ParallelBlockHandler implements BlockHandler {
|
||||
} as Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate child nodes for parallel execution
|
||||
*/
|
||||
private activateChildNodes(block: SerializedBlock, context: ExecutionContext): void {
|
||||
// Activate all child nodes (the executor will handle creating virtual instances)
|
||||
const parallelStartConnections =
|
||||
context.workflow?.connections.filter(
|
||||
(conn) => conn.source === block.id && conn.sourceHandle === 'parallel-start-source'
|
||||
) || []
|
||||
|
||||
for (const conn of parallelStartConnections) {
|
||||
context.activeExecutionPath.add(conn.target)
|
||||
logger.info(`Activated parallel path to ${conn.target}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all iterations of a parallel block have completed
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import type { BlockHandler } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import type { BlockHandler } from '../../types'
|
||||
|
||||
const logger = createLogger('ResponseBlockHandler')
|
||||
|
||||
@@ -15,7 +16,7 @@ interface JSONProperty {
|
||||
|
||||
export class ResponseBlockHandler implements BlockHandler {
|
||||
canHandle(block: SerializedBlock): boolean {
|
||||
return block.metadata?.id === 'response'
|
||||
return block.metadata?.id === BlockType.RESPONSE
|
||||
}
|
||||
|
||||
async execute(block: SerializedBlock, inputs: Record<string, any>): Promise<BlockOutput> {
|
||||
|
||||
@@ -11,11 +11,12 @@ import {
|
||||
vi,
|
||||
} from 'vitest'
|
||||
import { generateRouterPrompt } from '@/blocks/blocks/router'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { RouterBlockHandler } from '@/executor/handlers/router/router-handler'
|
||||
import { PathTracker } from '@/executor/path/path'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import { getProviderFromModel } from '@/providers/utils'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
import { PathTracker } from '../../path'
|
||||
import type { ExecutionContext } from '../../types'
|
||||
import { RouterBlockHandler } from './router-handler'
|
||||
|
||||
const mockGenerateRouterPrompt = generateRouterPrompt as Mock
|
||||
const mockGetProviderFromModel = getProviderFromModel as Mock
|
||||
@@ -52,9 +53,9 @@ describe('RouterBlockHandler', () => {
|
||||
}
|
||||
mockBlock = {
|
||||
id: 'router-block-1',
|
||||
metadata: { id: 'router', name: 'Test Router' },
|
||||
metadata: { id: BlockType.ROUTER, name: 'Test Router' },
|
||||
position: { x: 50, y: 50 },
|
||||
config: { tool: 'router', params: {} },
|
||||
config: { tool: BlockType.ROUTER, params: {} },
|
||||
inputs: { prompt: 'string', model: 'string' }, // Using ParamType strings
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
|
||||
@@ -2,10 +2,11 @@ import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { generateRouterPrompt } from '@/blocks/blocks/router'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import type { PathTracker } from '@/executor/path/path'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import { calculateCost, getProviderFromModel } from '@/providers/utils'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import type { PathTracker } from '../../path'
|
||||
import type { BlockHandler, ExecutionContext } from '../../types'
|
||||
|
||||
const logger = createLogger('RouterBlockHandler')
|
||||
|
||||
@@ -19,7 +20,7 @@ export class RouterBlockHandler implements BlockHandler {
|
||||
constructor(private pathTracker: PathTracker) {}
|
||||
|
||||
canHandle(block: SerializedBlock): boolean {
|
||||
return block.metadata?.id === 'router'
|
||||
return block.metadata?.id === BlockType.ROUTER
|
||||
}
|
||||
|
||||
async execute(
|
||||
@@ -144,7 +145,7 @@ export class RouterBlockHandler implements BlockHandler {
|
||||
|
||||
// Extract system prompt for agent blocks
|
||||
let systemPrompt = ''
|
||||
if (targetBlock.metadata?.id === 'agent') {
|
||||
if (targetBlock.metadata?.id === BlockType.AGENT) {
|
||||
// Try to get system prompt from different possible locations
|
||||
systemPrompt =
|
||||
targetBlock.config?.params?.systemPrompt || targetBlock.inputs?.systemPrompt || ''
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import type { ExecutionContext } from '../../types'
|
||||
import { WorkflowBlockHandler } from './workflow-handler'
|
||||
|
||||
// Mock fetch globally
|
||||
global.fetch = vi.fn()
|
||||
@@ -18,9 +19,9 @@ describe('WorkflowBlockHandler', () => {
|
||||
|
||||
mockBlock = {
|
||||
id: 'workflow-block-1',
|
||||
metadata: { id: 'workflow', name: 'Test Workflow Block' },
|
||||
metadata: { id: BlockType.WORKFLOW, name: 'Test Workflow Block' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'workflow', params: {} },
|
||||
config: { tool: BlockType.WORKFLOW, params: {} },
|
||||
inputs: { workflowId: 'string' },
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -64,9 +65,9 @@ describe('WorkflowBlockHandler', () => {
|
||||
blocks: [
|
||||
{
|
||||
id: 'starter',
|
||||
metadata: { id: 'starter', name: 'Starter' },
|
||||
metadata: { id: BlockType.STARTER, name: 'Starter' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'starter', params: {} },
|
||||
config: { tool: BlockType.STARTER, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -87,7 +88,7 @@ describe('WorkflowBlockHandler', () => {
|
||||
})
|
||||
|
||||
it('should not handle non-workflow blocks', () => {
|
||||
const nonWorkflowBlock = { ...mockBlock, metadata: { id: 'function' } }
|
||||
const nonWorkflowBlock = { ...mockBlock, metadata: { id: BlockType.FUNCTION } }
|
||||
expect(handler.canHandle(nonWorkflowBlock)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,11 +2,12 @@ import { generateInternalToken } from '@/lib/auth/internal'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getBaseUrl } from '@/lib/urls/utils'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { Executor } from '@/executor'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/executor/types'
|
||||
import { Serializer } from '@/serializer'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { Executor } from '../../index'
|
||||
import type { BlockHandler, ExecutionContext, StreamingExecution } from '../../types'
|
||||
|
||||
const logger = createLogger('WorkflowBlockHandler')
|
||||
|
||||
@@ -22,7 +23,7 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
private static executionStack = new Set<string>()
|
||||
|
||||
canHandle(block: SerializedBlock): boolean {
|
||||
return block.metadata?.id === 'workflow'
|
||||
return block.metadata?.id === BlockType.WORKFLOW
|
||||
}
|
||||
|
||||
async execute(
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* resolving inputs and dependencies, and managing errors.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import { Executor } from '@/executor'
|
||||
import {
|
||||
createMinimalWorkflow,
|
||||
createMockContext,
|
||||
@@ -15,8 +16,8 @@ import {
|
||||
createWorkflowWithErrorPath,
|
||||
createWorkflowWithLoop,
|
||||
setupAllMocks,
|
||||
} from './__test-utils__/executor-mocks'
|
||||
import { Executor } from './index'
|
||||
} from '@/executor/__test-utils__/executor-mocks'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
|
||||
vi.mock('@/stores/execution/store', () => ({
|
||||
useExecutionStore: {
|
||||
@@ -155,14 +156,14 @@ describe('Executor', () => {
|
||||
|
||||
test('should throw error for workflow without starter block', () => {
|
||||
const workflow = createMinimalWorkflow()
|
||||
workflow.blocks = workflow.blocks.filter((block) => block.metadata?.id !== 'starter')
|
||||
workflow.blocks = workflow.blocks.filter((block) => block.metadata?.id !== BlockType.STARTER)
|
||||
|
||||
expect(() => new Executor(workflow)).toThrow('Workflow must have an enabled starter block')
|
||||
})
|
||||
|
||||
test('should throw error for workflow with disabled starter block', () => {
|
||||
const workflow = createMinimalWorkflow()
|
||||
workflow.blocks.find((block) => block.metadata?.id === 'starter')!.enabled = false
|
||||
workflow.blocks.find((block) => block.metadata?.id === BlockType.STARTER)!.enabled = false
|
||||
|
||||
expect(() => new Executor(workflow)).toThrow('Workflow must have an enabled starter block')
|
||||
})
|
||||
@@ -459,7 +460,7 @@ describe('Executor', () => {
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
metadata: { id: 'condition', name: 'Condition Block' },
|
||||
metadata: { id: BlockType.CONDITION, name: 'Condition Block' },
|
||||
})
|
||||
|
||||
// Mock context
|
||||
@@ -641,7 +642,7 @@ describe('Executor', () => {
|
||||
{
|
||||
id: 'start',
|
||||
position: { x: 0, y: 0 },
|
||||
metadata: { id: 'starter', name: 'Start' },
|
||||
metadata: { id: BlockType.STARTER, name: 'Start' },
|
||||
config: { tool: 'test-tool', params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
@@ -650,7 +651,7 @@ describe('Executor', () => {
|
||||
{
|
||||
id: 'router',
|
||||
position: { x: 100, y: 0 },
|
||||
metadata: { id: 'router', name: 'Router' },
|
||||
metadata: { id: BlockType.ROUTER, name: 'Router' },
|
||||
config: { tool: 'test-tool', params: { prompt: 'test', model: 'gpt-4' } },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
@@ -659,7 +660,7 @@ describe('Executor', () => {
|
||||
{
|
||||
id: 'api1',
|
||||
position: { x: 200, y: -50 },
|
||||
metadata: { id: 'api', name: 'API 1' },
|
||||
metadata: { id: BlockType.API, name: 'API 1' },
|
||||
config: { tool: 'test-tool', params: { url: 'http://api1.com', method: 'GET' } },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
@@ -668,7 +669,7 @@ describe('Executor', () => {
|
||||
{
|
||||
id: 'api2',
|
||||
position: { x: 200, y: 50 },
|
||||
metadata: { id: 'api', name: 'API 2' },
|
||||
metadata: { id: BlockType.API, name: 'API 2' },
|
||||
config: { tool: 'test-tool', params: { url: 'http://api2.com', method: 'GET' } },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
@@ -677,7 +678,7 @@ describe('Executor', () => {
|
||||
{
|
||||
id: 'agent',
|
||||
position: { x: 300, y: 0 },
|
||||
metadata: { id: 'agent', name: 'Agent' },
|
||||
metadata: { id: BlockType.AGENT, name: 'Agent' },
|
||||
config: { tool: 'test-tool', params: { model: 'gpt-4', userPrompt: 'test' } },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
@@ -771,7 +772,7 @@ describe('Executor', () => {
|
||||
workflow.blocks.push({
|
||||
id: 'router1',
|
||||
position: { x: 200, y: 0 },
|
||||
metadata: { id: 'router', name: 'Router' },
|
||||
metadata: { id: BlockType.ROUTER, name: 'Router' },
|
||||
config: { tool: 'test-tool', params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
import { useExecutionStore } from '@/stores/execution/store'
|
||||
import { useConsoleStore } from '@/stores/panel/console/store'
|
||||
import { useGeneralStore } from '@/stores/settings/general/store'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import {
|
||||
AgentBlockHandler,
|
||||
ApiBlockHandler,
|
||||
@@ -17,11 +14,11 @@ import {
|
||||
ResponseBlockHandler,
|
||||
RouterBlockHandler,
|
||||
WorkflowBlockHandler,
|
||||
} from './handlers/index'
|
||||
import { LoopManager } from './loops'
|
||||
import { ParallelManager } from './parallels'
|
||||
import { PathTracker } from './path'
|
||||
import { InputResolver } from './resolver'
|
||||
} from '@/executor/handlers'
|
||||
import { LoopManager } from '@/executor/loops/loops'
|
||||
import { ParallelManager } from '@/executor/parallels/parallels'
|
||||
import { PathTracker } from '@/executor/path/path'
|
||||
import { InputResolver } from '@/executor/resolver/resolver'
|
||||
import type {
|
||||
BlockHandler,
|
||||
BlockLog,
|
||||
@@ -29,8 +26,12 @@ import type {
|
||||
ExecutionResult,
|
||||
NormalizedBlockOutput,
|
||||
StreamingExecution,
|
||||
} from './types'
|
||||
import { streamingResponseFormatProcessor } from './utils'
|
||||
} from '@/executor/types'
|
||||
import { streamingResponseFormatProcessor } from '@/executor/utils'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
import { useExecutionStore } from '@/stores/execution/store'
|
||||
import { useConsoleStore } from '@/stores/panel/console/store'
|
||||
import { useGeneralStore } from '@/stores/settings/general/store'
|
||||
|
||||
const logger = createLogger('Executor')
|
||||
|
||||
@@ -152,8 +153,8 @@ export class Executor {
|
||||
new EvaluatorBlockHandler(),
|
||||
new FunctionBlockHandler(),
|
||||
new ApiBlockHandler(),
|
||||
new LoopBlockHandler(this.resolver),
|
||||
new ParallelBlockHandler(this.resolver),
|
||||
new LoopBlockHandler(this.resolver, this.pathTracker),
|
||||
new ParallelBlockHandler(this.resolver, this.pathTracker),
|
||||
new ResponseBlockHandler(),
|
||||
new WorkflowBlockHandler(),
|
||||
new GenericBlockHandler(),
|
||||
@@ -548,7 +549,7 @@ export class Executor {
|
||||
*/
|
||||
private validateWorkflow(): void {
|
||||
const starterBlock = this.actualWorkflow.blocks.find(
|
||||
(block) => block.metadata?.id === 'starter'
|
||||
(block) => block.metadata?.id === BlockType.STARTER
|
||||
)
|
||||
if (!starterBlock || !starterBlock.enabled) {
|
||||
throw new Error('Workflow must have an enabled starter block')
|
||||
@@ -652,7 +653,7 @@ export class Executor {
|
||||
}
|
||||
|
||||
const starterBlock = this.actualWorkflow.blocks.find(
|
||||
(block) => block.metadata?.id === 'starter'
|
||||
(block) => block.metadata?.id === BlockType.STARTER
|
||||
)
|
||||
if (starterBlock) {
|
||||
// Initialize the starter block with the workflow input
|
||||
@@ -998,7 +999,7 @@ export class Executor {
|
||||
// Check if this is a loop block
|
||||
const isLoopBlock = incomingConnections.some((conn) => {
|
||||
const sourceBlock = this.actualWorkflow.blocks.find((b) => b.id === conn.source)
|
||||
return sourceBlock?.metadata?.id === 'loop'
|
||||
return sourceBlock?.metadata?.id === BlockType.LOOP
|
||||
})
|
||||
|
||||
if (isLoopBlock) {
|
||||
@@ -1081,7 +1082,7 @@ export class Executor {
|
||||
// For condition blocks, check if this is the selected path
|
||||
if (conn.sourceHandle?.startsWith('condition-')) {
|
||||
const sourceBlock = this.actualWorkflow.blocks.find((b) => b.id === conn.source)
|
||||
if (sourceBlock?.metadata?.id === 'condition') {
|
||||
if (sourceBlock?.metadata?.id === BlockType.CONDITION) {
|
||||
const conditionId = conn.sourceHandle.replace('condition-', '')
|
||||
const selectedCondition = context.decisions.condition.get(conn.source)
|
||||
|
||||
@@ -1096,7 +1097,7 @@ export class Executor {
|
||||
}
|
||||
|
||||
// For router blocks, check if this is the selected target
|
||||
if (sourceBlock?.metadata?.id === 'router') {
|
||||
if (sourceBlock?.metadata?.id === BlockType.ROUTER) {
|
||||
const selectedTarget = context.decisions.router.get(conn.source)
|
||||
|
||||
// If source is executed and this is not the selected target, consider it met
|
||||
@@ -1219,7 +1220,7 @@ export class Executor {
|
||||
|
||||
// Special case for starter block - it's already been initialized in createExecutionContext
|
||||
// This ensures we don't re-execute the starter block and just return its existing state
|
||||
if (block.metadata?.id === 'starter') {
|
||||
if (block.metadata?.id === BlockType.STARTER) {
|
||||
const starterState = context.blockStates.get(actualBlockId)
|
||||
if (starterState) {
|
||||
return starterState.output as NormalizedBlockOutput
|
||||
@@ -1243,7 +1244,9 @@ export class Executor {
|
||||
|
||||
// Check if this block needs the starter block's output
|
||||
// This is especially relevant for API, function, and conditions that might reference <start.input>
|
||||
const starterBlock = this.actualWorkflow.blocks.find((b) => b.metadata?.id === 'starter')
|
||||
const starterBlock = this.actualWorkflow.blocks.find(
|
||||
(b) => b.metadata?.id === BlockType.STARTER
|
||||
)
|
||||
if (starterBlock) {
|
||||
const starterState = context.blockStates.get(starterBlock.id)
|
||||
if (!starterState) {
|
||||
@@ -1345,7 +1348,7 @@ export class Executor {
|
||||
|
||||
// Skip console logging for infrastructure blocks like loops and parallels
|
||||
// For streaming blocks, we'll add the console entry after stream processing
|
||||
if (block.metadata?.id !== 'loop' && block.metadata?.id !== 'parallel') {
|
||||
if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) {
|
||||
addConsole({
|
||||
input: blockLog.input,
|
||||
output: blockLog.output,
|
||||
@@ -1415,7 +1418,7 @@ export class Executor {
|
||||
context.blockLogs.push(blockLog)
|
||||
|
||||
// Skip console logging for infrastructure blocks like loops and parallels
|
||||
if (block.metadata?.id !== 'loop' && block.metadata?.id !== 'parallel') {
|
||||
if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) {
|
||||
addConsole({
|
||||
input: blockLog.input,
|
||||
output: blockLog.output,
|
||||
@@ -1483,7 +1486,7 @@ export class Executor {
|
||||
context.blockLogs.push(blockLog)
|
||||
|
||||
// Skip console logging for infrastructure blocks like loops and parallels
|
||||
if (block.metadata?.id !== 'loop' && block.metadata?.id !== 'parallel') {
|
||||
if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) {
|
||||
addConsole({
|
||||
input: blockLog.input,
|
||||
output: {},
|
||||
@@ -1575,10 +1578,10 @@ export class Executor {
|
||||
// Skip for starter blocks which don't have error handles
|
||||
const block = this.actualWorkflow.blocks.find((b) => b.id === blockId)
|
||||
if (
|
||||
block?.metadata?.id === 'starter' ||
|
||||
block?.metadata?.id === 'condition' ||
|
||||
block?.metadata?.id === 'loop' ||
|
||||
block?.metadata?.id === 'parallel'
|
||||
block?.metadata?.id === BlockType.STARTER ||
|
||||
block?.metadata?.id === BlockType.CONDITION ||
|
||||
block?.metadata?.id === BlockType.LOOP ||
|
||||
block?.metadata?.id === BlockType.PARALLEL
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import { createMockContext } from '@/executor/__test-utils__/executor-mocks'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { LoopManager } from '@/executor/loops/loops'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedLoop, SerializedWorkflow } from '@/serializer/types'
|
||||
import { createMockContext } from './__test-utils__/executor-mocks'
|
||||
import { LoopManager } from './loops'
|
||||
import type { ExecutionContext } from './types'
|
||||
|
||||
vi.mock('@/lib/logs/console-logger', () => ({
|
||||
createLogger: () => ({
|
||||
@@ -40,8 +41,8 @@ describe('LoopManager', () => {
|
||||
{
|
||||
id: 'starter',
|
||||
position: { x: 0, y: 0 },
|
||||
metadata: { id: 'starter', name: 'Start' },
|
||||
config: { tool: 'starter', params: {} },
|
||||
metadata: { id: BlockType.STARTER, name: 'Start' },
|
||||
config: { tool: BlockType.STARTER, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -49,8 +50,8 @@ describe('LoopManager', () => {
|
||||
{
|
||||
id: 'loop-1',
|
||||
position: { x: 100, y: 0 },
|
||||
metadata: { id: 'loop', name: 'Test Loop' },
|
||||
config: { tool: 'loop', params: {} },
|
||||
metadata: { id: BlockType.LOOP, name: 'Test Loop' },
|
||||
config: { tool: BlockType.LOOP, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -58,8 +59,8 @@ describe('LoopManager', () => {
|
||||
{
|
||||
id: 'block-1',
|
||||
position: { x: 200, y: 0 },
|
||||
metadata: { id: 'function', name: 'Block 1' },
|
||||
config: { tool: 'function', params: {} },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Block 1' },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -67,8 +68,8 @@ describe('LoopManager', () => {
|
||||
{
|
||||
id: 'block-2',
|
||||
position: { x: 300, y: 0 },
|
||||
metadata: { id: 'function', name: 'Block 2' },
|
||||
config: { tool: 'function', params: {} },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Block 2' },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -76,8 +77,8 @@ describe('LoopManager', () => {
|
||||
{
|
||||
id: 'after-loop',
|
||||
position: { x: 400, y: 0 },
|
||||
metadata: { id: 'function', name: 'After Loop' },
|
||||
config: { tool: 'function', params: {} },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'After Loop' },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -409,7 +410,7 @@ describe('LoopManager', () => {
|
||||
test('should handle router blocks with selected paths', async () => {
|
||||
// Create a workflow with a router block inside the loop
|
||||
const workflow = createWorkflowWithLoop(createBasicLoop())
|
||||
workflow.blocks[2].metadata!.id = 'router' // Make block-1 a router
|
||||
workflow.blocks[2].metadata!.id = BlockType.ROUTER // Make block-1 a router
|
||||
workflow.connections = [
|
||||
{ source: 'starter', target: 'loop-1' },
|
||||
{ source: 'loop-1', target: 'block-1', sourceHandle: 'loop-start-source' },
|
||||
@@ -435,7 +436,7 @@ describe('LoopManager', () => {
|
||||
test('should handle condition blocks with selected paths', async () => {
|
||||
// Create a workflow with a condition block inside the loop
|
||||
const workflow = createWorkflowWithLoop(createBasicLoop())
|
||||
workflow.blocks[2].metadata!.id = 'condition' // Make block-1 a condition
|
||||
workflow.blocks[2].metadata!.id = BlockType.CONDITION // Make block-1 a condition
|
||||
workflow.connections = [
|
||||
{ source: 'starter', target: 'loop-1' },
|
||||
{ source: 'loop-1', target: 'block-1', sourceHandle: 'loop-start-source' },
|
||||
@@ -496,8 +497,8 @@ describe('LoopManager', () => {
|
||||
workflow.blocks.push({
|
||||
id: 'error-handler',
|
||||
position: { x: 350, y: 100 },
|
||||
metadata: { id: 'function', name: 'Error Handler' },
|
||||
config: { tool: 'function', params: {} },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Error Handler' },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock, SerializedConnection, SerializedLoop } from '@/serializer/types'
|
||||
import type { ExecutionContext } from './types'
|
||||
|
||||
const logger = createLogger('LoopManager')
|
||||
|
||||
@@ -366,13 +367,13 @@ export class LoopManager {
|
||||
const outgoing = blockOutgoingConnections.get(currentBlockId) || []
|
||||
|
||||
// Handle routing blocks specially
|
||||
if (block.metadata?.id === 'router') {
|
||||
if (block.metadata?.id === BlockType.ROUTER) {
|
||||
// For router blocks, only follow the selected path
|
||||
const selectedTarget = context.decisions.router.get(currentBlockId)
|
||||
if (selectedTarget && nodeIds.includes(selectedTarget)) {
|
||||
toVisit.push(selectedTarget)
|
||||
}
|
||||
} else if (block.metadata?.id === 'condition') {
|
||||
} else if (block.metadata?.id === BlockType.CONDITION) {
|
||||
// For condition blocks, only follow the selected condition path
|
||||
const selectedConditionId = context.decisions.condition.get(currentBlockId)
|
||||
if (selectedConditionId) {
|
||||
@@ -1,8 +1,9 @@
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
import { createParallelExecutionState } from '@/executor/__test-utils__/executor-mocks'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { ParallelManager } from '@/executor/parallels/parallels'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedWorkflow } from '@/serializer/types'
|
||||
import { createParallelExecutionState } from './__test-utils__/executor-mocks'
|
||||
import { ParallelManager } from './parallels'
|
||||
import type { ExecutionContext } from './types'
|
||||
|
||||
vi.mock('@/lib/logs/console-logger', () => ({
|
||||
createLogger: () => ({
|
||||
@@ -154,7 +155,7 @@ describe('ParallelManager', () => {
|
||||
const block = {
|
||||
id: 'func-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'function', params: {} },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -185,7 +186,7 @@ describe('ParallelManager', () => {
|
||||
const block = {
|
||||
id: 'func-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'function', params: {} },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import type { ExecutionContext, NormalizedBlockOutput } from '@/executor/types'
|
||||
import type { SerializedBlock, SerializedParallel, SerializedWorkflow } from '@/serializer/types'
|
||||
import type { ExecutionContext, NormalizedBlockOutput } from './types'
|
||||
|
||||
const logger = createLogger('ParallelManager')
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { PathTracker } from '@/executor/path/path'
|
||||
import { Routing } from '@/executor/routing/routing'
|
||||
import type { BlockState, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedWorkflow } from '@/serializer/types'
|
||||
import { PathTracker } from './path'
|
||||
import type { BlockState, ExecutionContext } from './types'
|
||||
|
||||
describe('PathTracker', () => {
|
||||
let pathTracker: PathTracker
|
||||
@@ -32,27 +34,27 @@ describe('PathTracker', () => {
|
||||
},
|
||||
{
|
||||
id: 'router1',
|
||||
metadata: { id: 'router' },
|
||||
metadata: { id: BlockType.ROUTER },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'router', params: {} },
|
||||
config: { tool: BlockType.ROUTER, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'condition1',
|
||||
metadata: { id: 'condition' },
|
||||
metadata: { id: BlockType.CONDITION },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'condition', params: {} },
|
||||
config: { tool: BlockType.CONDITION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'loop1',
|
||||
metadata: { id: 'loop' },
|
||||
metadata: { id: BlockType.LOOP },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'loop', params: {} },
|
||||
config: { tool: BlockType.LOOP, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -75,6 +77,7 @@ describe('PathTracker', () => {
|
||||
loopType: 'for',
|
||||
},
|
||||
},
|
||||
parallels: {},
|
||||
}
|
||||
|
||||
mockContext = {
|
||||
@@ -417,36 +420,36 @@ describe('PathTracker', () => {
|
||||
blocks: [
|
||||
{
|
||||
id: 'router1',
|
||||
metadata: { id: 'router', name: 'Router' },
|
||||
metadata: { id: BlockType.ROUTER, name: 'Router' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'router', params: {} },
|
||||
config: { tool: BlockType.ROUTER, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'api1',
|
||||
metadata: { id: 'api', name: 'API 1' },
|
||||
metadata: { id: BlockType.API, name: 'API 1' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'api', params: {} },
|
||||
config: { tool: BlockType.API, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'api2',
|
||||
metadata: { id: 'api', name: 'API 2' },
|
||||
metadata: { id: BlockType.API, name: 'API 2' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'api', params: {} },
|
||||
config: { tool: BlockType.API, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'agent1',
|
||||
metadata: { id: 'agent', name: 'Agent' },
|
||||
metadata: { id: BlockType.AGENT, name: 'Agent' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'agent', params: {} },
|
||||
config: { tool: BlockType.AGENT, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -485,7 +488,7 @@ describe('PathTracker', () => {
|
||||
output: {
|
||||
selectedPath: {
|
||||
blockId: 'api1',
|
||||
blockType: 'api',
|
||||
blockType: BlockType.API,
|
||||
blockTitle: 'API 1',
|
||||
},
|
||||
},
|
||||
@@ -508,9 +511,9 @@ describe('PathTracker', () => {
|
||||
// Add another level to test deep activation
|
||||
mockWorkflow.blocks.push({
|
||||
id: 'finalStep',
|
||||
metadata: { id: 'api', name: 'Final Step' },
|
||||
metadata: { id: BlockType.API, name: 'Final Step' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'api', params: {} },
|
||||
config: { tool: BlockType.API, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -524,7 +527,7 @@ describe('PathTracker', () => {
|
||||
output: {
|
||||
selectedPath: {
|
||||
blockId: 'api1',
|
||||
blockType: 'api',
|
||||
blockType: BlockType.API,
|
||||
blockTitle: 'API 1',
|
||||
},
|
||||
},
|
||||
@@ -552,7 +555,7 @@ describe('PathTracker', () => {
|
||||
output: {
|
||||
selectedPath: {
|
||||
blockId: 'api1',
|
||||
blockType: 'api',
|
||||
blockType: BlockType.API,
|
||||
blockTitle: 'API 1',
|
||||
},
|
||||
},
|
||||
@@ -586,7 +589,7 @@ describe('PathTracker', () => {
|
||||
output: {
|
||||
selectedPath: {
|
||||
blockId: 'api1',
|
||||
blockType: 'api',
|
||||
blockType: BlockType.API,
|
||||
blockTitle: 'API 1',
|
||||
},
|
||||
},
|
||||
@@ -602,4 +605,158 @@ describe('PathTracker', () => {
|
||||
expect(mockContext.activeExecutionPath.has('agent1')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('RoutingStrategy integration', () => {
|
||||
beforeEach(() => {
|
||||
// Add more block types to test the new routing strategy
|
||||
mockWorkflow.blocks.push(
|
||||
{
|
||||
id: 'parallel1',
|
||||
metadata: { id: BlockType.PARALLEL },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: BlockType.PARALLEL, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'function1',
|
||||
metadata: { id: BlockType.FUNCTION },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'agent1',
|
||||
metadata: { id: BlockType.AGENT },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: BlockType.AGENT, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
}
|
||||
)
|
||||
|
||||
mockWorkflow.connections.push(
|
||||
{ source: 'parallel1', target: 'function1', sourceHandle: 'parallel-start-source' },
|
||||
{ source: 'parallel1', target: 'agent1', sourceHandle: 'parallel-end-source' }
|
||||
)
|
||||
|
||||
mockWorkflow.parallels = {
|
||||
parallel1: {
|
||||
id: 'parallel1',
|
||||
nodes: ['function1'],
|
||||
distribution: ['item1', 'item2'],
|
||||
},
|
||||
}
|
||||
|
||||
pathTracker = new PathTracker(mockWorkflow)
|
||||
})
|
||||
|
||||
it('should correctly categorize different block types', () => {
|
||||
// Test that our refactored code properly uses RoutingStrategy
|
||||
expect(Routing.getCategory(BlockType.ROUTER)).toBe('routing')
|
||||
expect(Routing.getCategory(BlockType.CONDITION)).toBe('routing')
|
||||
expect(Routing.getCategory(BlockType.PARALLEL)).toBe('flow-control')
|
||||
expect(Routing.getCategory(BlockType.LOOP)).toBe('flow-control')
|
||||
expect(Routing.getCategory(BlockType.FUNCTION)).toBe('regular')
|
||||
expect(Routing.getCategory(BlockType.AGENT)).toBe('regular')
|
||||
})
|
||||
|
||||
it('should handle flow control blocks correctly in path checking', () => {
|
||||
// Test that parallel blocks are handled correctly
|
||||
mockContext.executedBlocks.add('parallel1')
|
||||
mockContext.activeExecutionPath.add('parallel1')
|
||||
|
||||
// Function1 should be reachable from parallel1 via parallel-start-source
|
||||
expect(pathTracker.isInActivePath('function1', mockContext)).toBe(true)
|
||||
|
||||
// Agent1 should be reachable from parallel1 via parallel-end-source
|
||||
expect(pathTracker.isInActivePath('agent1', mockContext)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle router selecting routing blocks correctly', () => {
|
||||
// Test the refactored logic where router selects another routing block
|
||||
const blockState: BlockState = {
|
||||
output: { selectedPath: { blockId: 'condition1' } },
|
||||
executed: true,
|
||||
executionTime: 100,
|
||||
}
|
||||
mockContext.blockStates.set('router1', blockState)
|
||||
|
||||
pathTracker.updateExecutionPaths(['router1'], mockContext)
|
||||
|
||||
// Condition1 should be activated but not its downstream paths
|
||||
// (since routing blocks make their own decisions)
|
||||
expect(mockContext.activeExecutionPath.has('condition1')).toBe(true)
|
||||
expect(mockContext.decisions.router.get('router1')).toBe('condition1')
|
||||
})
|
||||
|
||||
it('should handle router selecting flow control blocks correctly', () => {
|
||||
// Test the refactored logic where router selects a flow control block
|
||||
const blockState: BlockState = {
|
||||
output: { selectedPath: { blockId: 'parallel1' } },
|
||||
executed: true,
|
||||
executionTime: 100,
|
||||
}
|
||||
mockContext.blockStates.set('router1', blockState)
|
||||
|
||||
pathTracker.updateExecutionPaths(['router1'], mockContext)
|
||||
|
||||
// Parallel1 should be activated but not its downstream paths
|
||||
// (since flow control blocks don't activate downstream automatically)
|
||||
expect(mockContext.activeExecutionPath.has('parallel1')).toBe(true)
|
||||
expect(mockContext.decisions.router.get('router1')).toBe('parallel1')
|
||||
// Children should NOT be activated automatically
|
||||
expect(mockContext.activeExecutionPath.has('function1')).toBe(false)
|
||||
expect(mockContext.activeExecutionPath.has('agent1')).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle router selecting regular blocks correctly', () => {
|
||||
// Test that regular blocks still activate downstream paths
|
||||
const blockState: BlockState = {
|
||||
output: { selectedPath: { blockId: 'function1' } },
|
||||
executed: true,
|
||||
executionTime: 100,
|
||||
}
|
||||
mockContext.blockStates.set('router1', blockState)
|
||||
|
||||
pathTracker.updateExecutionPaths(['router1'], mockContext)
|
||||
|
||||
// Function1 should be activated and can activate downstream paths
|
||||
expect(mockContext.activeExecutionPath.has('function1')).toBe(true)
|
||||
expect(mockContext.decisions.router.get('router1')).toBe('function1')
|
||||
})
|
||||
|
||||
it('should use category-based logic for updatePathForBlock', () => {
|
||||
// Test that the refactored switch statement works correctly
|
||||
|
||||
// Test routing block (condition)
|
||||
const conditionState: BlockState = {
|
||||
output: { selectedConditionId: 'if' },
|
||||
executed: true,
|
||||
executionTime: 100,
|
||||
}
|
||||
mockContext.blockStates.set('condition1', conditionState)
|
||||
pathTracker.updateExecutionPaths(['condition1'], mockContext)
|
||||
expect(mockContext.decisions.condition.get('condition1')).toBe('if')
|
||||
|
||||
// Test flow control block (loop)
|
||||
pathTracker.updateExecutionPaths(['loop1'], mockContext)
|
||||
expect(mockContext.activeExecutionPath.has('block1')).toBe(true) // loop-start-source
|
||||
|
||||
// Test regular block
|
||||
const functionState: BlockState = {
|
||||
output: { result: 'success' },
|
||||
executed: true,
|
||||
executionTime: 100,
|
||||
}
|
||||
mockContext.blockStates.set('function1', functionState)
|
||||
mockContext.executedBlocks.add('function1')
|
||||
pathTracker.updateExecutionPaths(['function1'], mockContext)
|
||||
// Should activate downstream connections (handled by regular block logic)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { Routing } from '@/executor/routing/routing'
|
||||
import type { BlockState, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock, SerializedConnection, SerializedWorkflow } from '@/serializer/types'
|
||||
import type { BlockState, ExecutionContext } from './types'
|
||||
|
||||
const logger = createLogger('PathTracker')
|
||||
|
||||
@@ -79,14 +81,15 @@ export class PathTracker {
|
||||
const sourceBlock = this.getBlock(connection.source)
|
||||
if (!sourceBlock) return false
|
||||
|
||||
const blockType = sourceBlock.metadata?.id
|
||||
const blockType = sourceBlock.metadata?.id || ''
|
||||
const category = Routing.getCategory(blockType)
|
||||
|
||||
// Use strategy pattern for different block types
|
||||
switch (blockType) {
|
||||
case 'router':
|
||||
return this.isRouterConnectionActive(connection, context)
|
||||
case 'condition':
|
||||
return this.isConditionConnectionActive(connection, context)
|
||||
// Use routing strategy to determine connection checking method
|
||||
switch (category) {
|
||||
case 'routing':
|
||||
return blockType === BlockType.ROUTER
|
||||
? this.isRouterConnectionActive(connection, context)
|
||||
: this.isConditionConnectionActive(connection, context)
|
||||
default:
|
||||
return this.isRegularConnectionActive(connection, context)
|
||||
}
|
||||
@@ -137,17 +140,24 @@ export class PathTracker {
|
||||
* Update paths for a specific block based on its type
|
||||
*/
|
||||
private updatePathForBlock(block: SerializedBlock, context: ExecutionContext): void {
|
||||
const blockType = block.metadata?.id
|
||||
const blockType = block.metadata?.id || ''
|
||||
const category = Routing.getCategory(blockType)
|
||||
|
||||
switch (blockType) {
|
||||
case 'router':
|
||||
this.updateRouterPaths(block, context)
|
||||
switch (category) {
|
||||
case 'routing':
|
||||
if (blockType === BlockType.ROUTER) {
|
||||
this.updateRouterPaths(block, context)
|
||||
} else {
|
||||
this.updateConditionPaths(block, context)
|
||||
}
|
||||
break
|
||||
case 'condition':
|
||||
this.updateConditionPaths(block, context)
|
||||
break
|
||||
case 'loop':
|
||||
this.updateLoopPaths(block, context)
|
||||
case 'flow-control':
|
||||
if (blockType === BlockType.LOOP) {
|
||||
this.updateLoopPaths(block, context)
|
||||
} else {
|
||||
// For parallel blocks, they're handled by their own handler
|
||||
this.updateRegularBlockPaths(block, context)
|
||||
}
|
||||
break
|
||||
default:
|
||||
this.updateRegularBlockPaths(block, context)
|
||||
@@ -166,23 +176,43 @@ export class PathTracker {
|
||||
context.decisions.router.set(block.id, selectedPath)
|
||||
context.activeExecutionPath.add(selectedPath)
|
||||
|
||||
this.activateDownstreamPaths(selectedPath, context)
|
||||
// Check if the selected target should activate downstream paths
|
||||
const selectedBlock = this.getBlock(selectedPath)
|
||||
const selectedBlockType = selectedBlock?.metadata?.id || ''
|
||||
const selectedCategory = Routing.getCategory(selectedBlockType)
|
||||
|
||||
// Only activate downstream paths for regular blocks
|
||||
// Routing blocks make their own routing decisions when they execute
|
||||
// Flow control blocks manage their own path activation
|
||||
if (selectedCategory === 'regular') {
|
||||
this.activateDownstreamPathsSelectively(selectedPath, context)
|
||||
}
|
||||
|
||||
logger.info(`Router ${block.id} selected path: ${selectedPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively activate downstream paths from a block
|
||||
* Selectively activate downstream paths, respecting block routing behavior
|
||||
* This prevents flow control blocks from being activated when they should be controlled by routing
|
||||
*/
|
||||
private activateDownstreamPaths(blockId: string, context: ExecutionContext): void {
|
||||
private activateDownstreamPathsSelectively(blockId: string, context: ExecutionContext): void {
|
||||
const outgoingConnections = this.getOutgoingConnections(blockId)
|
||||
|
||||
for (const conn of outgoingConnections) {
|
||||
if (!context.activeExecutionPath.has(conn.target)) {
|
||||
context.activeExecutionPath.add(conn.target)
|
||||
const targetBlock = this.getBlock(conn.target)
|
||||
const targetBlockType = targetBlock?.metadata?.id
|
||||
|
||||
this.activateDownstreamPaths(conn.target, context)
|
||||
// Use routing strategy to determine if this connection should be activated
|
||||
if (!Routing.shouldSkipConnection(conn.sourceHandle, targetBlockType || '')) {
|
||||
context.activeExecutionPath.add(conn.target)
|
||||
|
||||
// Recursively activate downstream paths if the target block should activate downstream
|
||||
if (Routing.shouldActivateDownstream(targetBlockType || '')) {
|
||||
this.activateDownstreamPathsSelectively(conn.target, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,6 +268,14 @@ export class PathTracker {
|
||||
|
||||
for (const conn of outgoingConnections) {
|
||||
if (this.shouldActivateConnection(conn, hasError, isPartOfLoop, blockLoops, context)) {
|
||||
const targetBlock = this.getBlock(conn.target)
|
||||
const targetBlockType = targetBlock?.metadata?.id
|
||||
|
||||
// Use routing strategy to determine if this connection should be activated
|
||||
if (Routing.shouldSkipConnection(conn.sourceHandle, targetBlockType || '')) {
|
||||
continue
|
||||
}
|
||||
|
||||
context.activeExecutionPath.add(conn.target)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { InputResolver } from '@/executor/resolver/resolver'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
import { InputResolver } from './resolver'
|
||||
import type { ExecutionContext } from './types'
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@/lib/logs/console-logger', () => ({
|
||||
@@ -27,36 +28,36 @@ describe('InputResolver', () => {
|
||||
blocks: [
|
||||
{
|
||||
id: 'starter-block',
|
||||
metadata: { id: 'starter', name: 'Start' },
|
||||
metadata: { id: BlockType.STARTER, name: 'Start' },
|
||||
position: { x: 100, y: 100 },
|
||||
config: { tool: 'starter', params: {} },
|
||||
config: { tool: BlockType.STARTER, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'function-block',
|
||||
metadata: { id: 'function', name: 'Function' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function' },
|
||||
position: { x: 300, y: 100 },
|
||||
config: { tool: 'function', params: {} },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'condition-block',
|
||||
metadata: { id: 'condition', name: 'Condition' },
|
||||
metadata: { id: BlockType.CONDITION, name: 'Condition' },
|
||||
position: { x: 500, y: 100 },
|
||||
config: { tool: 'condition', params: {} },
|
||||
config: { tool: BlockType.CONDITION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'api-block',
|
||||
metadata: { id: 'api', name: 'API' },
|
||||
metadata: { id: BlockType.API, name: 'API' },
|
||||
position: { x: 700, y: 100 },
|
||||
config: { tool: 'api', params: {} },
|
||||
config: { tool: BlockType.API, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -429,7 +430,7 @@ describe('InputResolver', () => {
|
||||
it('should resolve environment variables in API key contexts', () => {
|
||||
const block: SerializedBlock = {
|
||||
id: 'test-block',
|
||||
metadata: { id: 'api', name: 'Test API Block' }, // API block type
|
||||
metadata: { id: BlockType.API, name: 'Test API Block' }, // API block type
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'api',
|
||||
@@ -626,10 +627,10 @@ describe('InputResolver', () => {
|
||||
it('should handle code input for function blocks', () => {
|
||||
const block: SerializedBlock = {
|
||||
id: 'code-block',
|
||||
metadata: { id: 'function', name: 'Code Block' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Code Block' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'function',
|
||||
tool: BlockType.FUNCTION,
|
||||
params: {
|
||||
code: 'const name = "<variable.stringVar>";\nconst num = <variable.numberVar>;\nreturn { name, num };',
|
||||
},
|
||||
@@ -652,7 +653,7 @@ describe('InputResolver', () => {
|
||||
it('should handle body input for API blocks', () => {
|
||||
const block: SerializedBlock = {
|
||||
id: 'api-block',
|
||||
metadata: { id: 'api', name: 'API Block' },
|
||||
metadata: { id: BlockType.API, name: 'API Block' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'api',
|
||||
@@ -679,7 +680,7 @@ describe('InputResolver', () => {
|
||||
it('should handle conditions parameter for condition blocks', () => {
|
||||
const block: SerializedBlock = {
|
||||
id: 'condition-block',
|
||||
metadata: { id: 'condition', name: 'Condition Block' },
|
||||
metadata: { id: BlockType.CONDITION, name: 'Condition Block' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'condition',
|
||||
@@ -734,10 +735,10 @@ describe('InputResolver', () => {
|
||||
const loopBlock: SerializedBlock = {
|
||||
id: 'loop-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'loop', params: {} },
|
||||
config: { tool: BlockType.LOOP, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'loop', name: 'Test Loop' },
|
||||
metadata: { id: BlockType.LOOP, name: 'Test Loop' },
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
@@ -745,14 +746,14 @@ describe('InputResolver', () => {
|
||||
id: 'function-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'function',
|
||||
tool: BlockType.FUNCTION,
|
||||
params: {
|
||||
item: '<loop.currentItem>', // Direct reference, not wrapped in quotes
|
||||
},
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'function', name: 'Process Item' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Process Item' },
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
@@ -796,10 +797,10 @@ describe('InputResolver', () => {
|
||||
const loopBlock: SerializedBlock = {
|
||||
id: 'loop-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'loop', params: {} },
|
||||
config: { tool: BlockType.LOOP, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'loop', name: 'Test Loop' },
|
||||
metadata: { id: BlockType.LOOP, name: 'Test Loop' },
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
@@ -807,14 +808,14 @@ describe('InputResolver', () => {
|
||||
id: 'function-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'function',
|
||||
tool: BlockType.FUNCTION,
|
||||
params: {
|
||||
index: '<loop.index>', // Direct reference, not wrapped in quotes
|
||||
},
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'function', name: 'Process Index' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Process Index' },
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
@@ -857,10 +858,10 @@ describe('InputResolver', () => {
|
||||
const loopBlock: SerializedBlock = {
|
||||
id: 'loop-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'loop', params: {} },
|
||||
config: { tool: BlockType.LOOP, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'loop', name: 'Test Loop' },
|
||||
metadata: { id: BlockType.LOOP, name: 'Test Loop' },
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
@@ -868,14 +869,14 @@ describe('InputResolver', () => {
|
||||
id: 'function-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'function',
|
||||
tool: BlockType.FUNCTION,
|
||||
params: {
|
||||
allItems: '<loop.items>', // Direct reference to all items
|
||||
},
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'function', name: 'Process All Items' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Process All Items' },
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
@@ -924,10 +925,10 @@ describe('InputResolver', () => {
|
||||
const loopBlock: SerializedBlock = {
|
||||
id: 'loop-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'loop', params: {} },
|
||||
config: { tool: BlockType.LOOP, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'loop', name: 'Test Loop' },
|
||||
metadata: { id: BlockType.LOOP, name: 'Test Loop' },
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
@@ -935,14 +936,14 @@ describe('InputResolver', () => {
|
||||
id: 'function-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: 'function',
|
||||
tool: BlockType.FUNCTION,
|
||||
params: {
|
||||
allItems: '<loop.items>', // Direct reference to all items
|
||||
},
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'function', name: 'Process All Items' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Process All Items' },
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
@@ -997,19 +998,19 @@ describe('InputResolver', () => {
|
||||
{
|
||||
id: 'parallel-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'parallel', params: {} },
|
||||
config: { tool: BlockType.PARALLEL, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'parallel', name: 'Parallel 1' },
|
||||
metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' },
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'function-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'function', params: { code: '<parallel.currentItem>' } },
|
||||
config: { tool: BlockType.FUNCTION, params: { code: '<parallel.currentItem>' } },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'function', name: 'Function 1' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function 1' },
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
@@ -1053,28 +1054,28 @@ describe('InputResolver', () => {
|
||||
{
|
||||
id: 'parallel-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'parallel', params: {} },
|
||||
config: { tool: BlockType.PARALLEL, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'parallel', name: 'Parallel 1' },
|
||||
metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' },
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'parallel-2',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'parallel', params: {} },
|
||||
config: { tool: BlockType.PARALLEL, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'parallel', name: 'Parallel 2' },
|
||||
metadata: { id: BlockType.PARALLEL, name: 'Parallel 2' },
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'function-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'function', params: { code: '<Parallel1.results>' } },
|
||||
config: { tool: BlockType.FUNCTION, params: { code: '<Parallel1.results>' } },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'function', name: 'Function 1' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function 1' },
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
@@ -1156,19 +1157,19 @@ describe('InputResolver', () => {
|
||||
{
|
||||
id: 'parallel-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'parallel', params: {} },
|
||||
config: { tool: BlockType.PARALLEL, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'parallel', name: 'Parallel 1' },
|
||||
metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' },
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'function-1',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'function', params: { code: '<parallel-1.results>' } },
|
||||
config: { tool: BlockType.FUNCTION, params: { code: '<parallel-1.results>' } },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'function', name: 'Function 1' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function 1' },
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
@@ -1244,36 +1245,36 @@ describe('InputResolver', () => {
|
||||
blocks: [
|
||||
{
|
||||
id: 'starter-1',
|
||||
metadata: { id: 'starter', name: 'Start' },
|
||||
metadata: { id: BlockType.STARTER, name: 'Start' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'starter', params: {} },
|
||||
config: { tool: BlockType.STARTER, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-1',
|
||||
metadata: { id: 'agent', name: 'Agent Block' },
|
||||
metadata: { id: BlockType.AGENT, name: 'Agent Block' },
|
||||
position: { x: 100, y: 100 },
|
||||
config: { tool: 'agent', params: {} },
|
||||
config: { tool: BlockType.AGENT, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'function-1',
|
||||
metadata: { id: 'function', name: 'Function Block' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function Block' },
|
||||
position: { x: 200, y: 200 },
|
||||
config: { tool: 'function', params: {} },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'isolated-block',
|
||||
metadata: { id: 'agent', name: 'Isolated Block' },
|
||||
metadata: { id: BlockType.AGENT, name: 'Isolated Block' },
|
||||
position: { x: 300, y: 300 },
|
||||
config: { tool: 'agent', params: {} },
|
||||
config: { tool: BlockType.AGENT, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -1301,7 +1302,7 @@ describe('InputResolver', () => {
|
||||
})
|
||||
// Always allow starter block access
|
||||
const starterBlock = workflowWithConnections.blocks.find(
|
||||
(b) => b.metadata?.id === 'starter'
|
||||
(b) => b.metadata?.id === BlockType.STARTER
|
||||
)
|
||||
if (starterBlock) {
|
||||
accessibleBlocks.add(starterBlock.id)
|
||||
@@ -1323,7 +1324,7 @@ describe('InputResolver', () => {
|
||||
})
|
||||
// Always allow starter block access
|
||||
const starterBlock = workflowWithConnections.blocks.find(
|
||||
(b) => b.metadata?.id === 'starter'
|
||||
(b) => b.metadata?.id === BlockType.STARTER
|
||||
)
|
||||
if (starterBlock) {
|
||||
accessibleBlocks.add(starterBlock.id)
|
||||
@@ -1370,7 +1371,7 @@ describe('InputResolver', () => {
|
||||
const testBlock: SerializedBlock = {
|
||||
...functionBlock,
|
||||
config: {
|
||||
tool: 'function',
|
||||
tool: BlockType.FUNCTION,
|
||||
params: {
|
||||
code: 'return <agent-1.content>', // function-1 can reference agent-1 (connected)
|
||||
},
|
||||
@@ -1385,9 +1386,9 @@ describe('InputResolver', () => {
|
||||
// Create a new block that is added to the workflow but not connected to isolated-block
|
||||
workflowWithConnections.blocks.push({
|
||||
id: 'test-block',
|
||||
metadata: { id: 'function', name: 'Test Block' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Test Block' },
|
||||
position: { x: 500, y: 500 },
|
||||
config: { tool: 'function', params: {} },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -1404,7 +1405,9 @@ describe('InputResolver', () => {
|
||||
}
|
||||
})
|
||||
// Always allow starter block access
|
||||
const starterBlock = workflowWithConnections.blocks.find((b) => b.metadata?.id === 'starter')
|
||||
const starterBlock = workflowWithConnections.blocks.find(
|
||||
(b) => b.metadata?.id === BlockType.STARTER
|
||||
)
|
||||
if (starterBlock) {
|
||||
testBlockAccessible.add(starterBlock.id)
|
||||
}
|
||||
@@ -1412,10 +1415,10 @@ describe('InputResolver', () => {
|
||||
|
||||
const testBlock: SerializedBlock = {
|
||||
id: 'test-block',
|
||||
metadata: { id: 'function', name: 'Test Block' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Test Block' },
|
||||
position: { x: 500, y: 500 },
|
||||
config: {
|
||||
tool: 'function',
|
||||
tool: BlockType.FUNCTION,
|
||||
params: {
|
||||
code: 'return <isolated-block.content>', // test-block cannot reference isolated-block (not connected)
|
||||
},
|
||||
@@ -1435,7 +1438,7 @@ describe('InputResolver', () => {
|
||||
const testBlock: SerializedBlock = {
|
||||
...functionBlock,
|
||||
config: {
|
||||
tool: 'function',
|
||||
tool: BlockType.FUNCTION,
|
||||
params: {
|
||||
code: 'return <start.input>', // Any block can reference start
|
||||
},
|
||||
@@ -1450,9 +1453,9 @@ describe('InputResolver', () => {
|
||||
// Create a test block in the workflow first
|
||||
workflowWithConnections.blocks.push({
|
||||
id: 'test-block-2',
|
||||
metadata: { id: 'function', name: 'Test Block 2' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Test Block 2' },
|
||||
position: { x: 600, y: 600 },
|
||||
config: { tool: 'function', params: {} },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -1469,7 +1472,9 @@ describe('InputResolver', () => {
|
||||
}
|
||||
})
|
||||
// Always allow starter block access
|
||||
const starterBlock = workflowWithConnections.blocks.find((b) => b.metadata?.id === 'starter')
|
||||
const starterBlock = workflowWithConnections.blocks.find(
|
||||
(b) => b.metadata?.id === BlockType.STARTER
|
||||
)
|
||||
if (starterBlock) {
|
||||
testBlock2Accessible.add(starterBlock.id)
|
||||
}
|
||||
@@ -1477,10 +1482,10 @@ describe('InputResolver', () => {
|
||||
|
||||
const testBlock: SerializedBlock = {
|
||||
id: 'test-block-2',
|
||||
metadata: { id: 'function', name: 'Test Block 2' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Test Block 2' },
|
||||
position: { x: 600, y: 600 },
|
||||
config: {
|
||||
tool: 'function',
|
||||
tool: BlockType.FUNCTION,
|
||||
params: {
|
||||
code: 'return <nonexistent.value>',
|
||||
},
|
||||
@@ -1500,7 +1505,7 @@ describe('InputResolver', () => {
|
||||
const testBlock: SerializedBlock = {
|
||||
...functionBlock,
|
||||
config: {
|
||||
tool: 'function',
|
||||
tool: BlockType.FUNCTION,
|
||||
params: {
|
||||
nameRef: '<Agent Block.content>', // Reference by actual name
|
||||
normalizedRef: '<agentblock.content>', // Reference by normalized name
|
||||
@@ -1523,9 +1528,9 @@ describe('InputResolver', () => {
|
||||
...workflowWithConnections.blocks,
|
||||
{
|
||||
id: 'response-1',
|
||||
metadata: { id: 'response', name: 'Response Block' },
|
||||
metadata: { id: BlockType.RESPONSE, name: 'Response Block' },
|
||||
position: { x: 400, y: 400 },
|
||||
config: { tool: 'response', params: {} },
|
||||
config: { tool: BlockType.RESPONSE, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -1555,7 +1560,9 @@ describe('InputResolver', () => {
|
||||
}
|
||||
})
|
||||
// Always allow starter block access
|
||||
const starterBlock = extendedWorkflow.blocks.find((b) => b.metadata?.id === 'starter')
|
||||
const starterBlock = extendedWorkflow.blocks.find(
|
||||
(b) => b.metadata?.id === BlockType.STARTER
|
||||
)
|
||||
if (starterBlock) {
|
||||
accessibleBlocks.add(starterBlock.id)
|
||||
}
|
||||
@@ -1572,7 +1579,9 @@ describe('InputResolver', () => {
|
||||
}
|
||||
})
|
||||
// Always allow starter block access
|
||||
const starterBlock = extendedWorkflow.blocks.find((b) => b.metadata?.id === 'starter')
|
||||
const starterBlock = extendedWorkflow.blocks.find(
|
||||
(b) => b.metadata?.id === BlockType.STARTER
|
||||
)
|
||||
if (starterBlock) {
|
||||
accessibleBlocks.add(starterBlock.id)
|
||||
}
|
||||
@@ -1590,7 +1599,7 @@ describe('InputResolver', () => {
|
||||
const testBlock: SerializedBlock = {
|
||||
...responseBlock,
|
||||
config: {
|
||||
tool: 'response',
|
||||
tool: BlockType.RESPONSE,
|
||||
params: {
|
||||
canReferenceFunction: '<function-1.result>', // Can reference directly connected function-1
|
||||
cannotReferenceAgent: '<agent-1.content>', // Cannot reference agent-1 (not directly connected)
|
||||
@@ -1614,7 +1623,7 @@ describe('InputResolver', () => {
|
||||
expect(() => {
|
||||
const block1 = {
|
||||
...testBlock,
|
||||
config: { tool: 'response', params: { test: '<function-1.result>' } },
|
||||
config: { tool: BlockType.RESPONSE, params: { test: '<function-1.result>' } },
|
||||
}
|
||||
extendedResolver.resolveInputs(block1, extendedContext)
|
||||
}).not.toThrow()
|
||||
@@ -1624,9 +1633,9 @@ describe('InputResolver', () => {
|
||||
// Add the response block to the workflow so it can be validated properly
|
||||
extendedWorkflow.blocks.push({
|
||||
id: 'test-response-block',
|
||||
metadata: { id: 'response', name: 'Test Response Block' },
|
||||
metadata: { id: BlockType.RESPONSE, name: 'Test Response Block' },
|
||||
position: { x: 500, y: 500 },
|
||||
config: { tool: 'response', params: {} },
|
||||
config: { tool: BlockType.RESPONSE, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -1635,9 +1644,9 @@ describe('InputResolver', () => {
|
||||
|
||||
const block2 = {
|
||||
id: 'test-response-block',
|
||||
metadata: { id: 'response', name: 'Test Response Block' },
|
||||
metadata: { id: BlockType.RESPONSE, name: 'Test Response Block' },
|
||||
position: { x: 500, y: 500 },
|
||||
config: { tool: 'response', params: { test: '<agent-1.content>' } },
|
||||
config: { tool: BlockType.RESPONSE, params: { test: '<agent-1.content>' } },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -1652,16 +1661,16 @@ describe('InputResolver', () => {
|
||||
blocks: [
|
||||
{
|
||||
id: 'starter-1',
|
||||
metadata: { id: 'starter', name: 'Start' },
|
||||
metadata: { id: BlockType.STARTER, name: 'Start' },
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'starter', params: {} },
|
||||
config: { tool: BlockType.STARTER, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'loop-1',
|
||||
metadata: { id: 'loop', name: 'Loop' },
|
||||
metadata: { id: BlockType.LOOP, name: 'Loop' },
|
||||
position: { x: 100, y: 100 },
|
||||
config: { tool: '', params: {} },
|
||||
inputs: {},
|
||||
@@ -1670,18 +1679,18 @@ describe('InputResolver', () => {
|
||||
},
|
||||
{
|
||||
id: 'function-1',
|
||||
metadata: { id: 'function', name: 'Function 1' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function 1' },
|
||||
position: { x: 200, y: 200 },
|
||||
config: { tool: 'function', params: {} },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'function-2',
|
||||
metadata: { id: 'function', name: 'Function 2' },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function 2' },
|
||||
position: { x: 300, y: 300 },
|
||||
config: { tool: 'function', params: {} },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
@@ -1711,7 +1720,7 @@ describe('InputResolver', () => {
|
||||
}
|
||||
})
|
||||
// Always allow starter block access
|
||||
const starterBlock = loopWorkflow.blocks.find((b) => b.metadata?.id === 'starter')
|
||||
const starterBlock = loopWorkflow.blocks.find((b) => b.metadata?.id === BlockType.STARTER)
|
||||
if (starterBlock) {
|
||||
accessibleBlocks.add(starterBlock.id)
|
||||
}
|
||||
@@ -1735,7 +1744,7 @@ describe('InputResolver', () => {
|
||||
}
|
||||
})
|
||||
// Always allow starter block access
|
||||
const starterBlock = loopWorkflow.blocks.find((b) => b.metadata?.id === 'starter')
|
||||
const starterBlock = loopWorkflow.blocks.find((b) => b.metadata?.id === BlockType.STARTER)
|
||||
if (starterBlock) {
|
||||
accessibleBlocks.add(starterBlock.id)
|
||||
}
|
||||
@@ -1746,7 +1755,7 @@ describe('InputResolver', () => {
|
||||
const testBlock: SerializedBlock = {
|
||||
...loopWorkflow.blocks[2],
|
||||
config: {
|
||||
tool: 'function',
|
||||
tool: BlockType.FUNCTION,
|
||||
params: {
|
||||
code: 'return <function-2.result>', // function-1 can reference function-2 (same loop)
|
||||
},
|
||||
@@ -1,9 +1,9 @@
|
||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { VariableManager } from '@/lib/variables/variable-manager'
|
||||
import type { LoopManager } from '@/executor/loops/loops'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
import type { LoopManager } from './loops'
|
||||
import type { ExecutionContext } from './types'
|
||||
|
||||
const logger = createLogger('InputResolver')
|
||||
|
||||
145
apps/sim/executor/routing/routing.test.ts
Normal file
145
apps/sim/executor/routing/routing.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { BlockCategory, Routing } from '@/executor/routing/routing'
|
||||
|
||||
describe('Routing', () => {
|
||||
describe('getCategory', () => {
|
||||
it.concurrent('should categorize flow control blocks correctly', () => {
|
||||
expect(Routing.getCategory(BlockType.PARALLEL)).toBe(BlockCategory.FLOW_CONTROL)
|
||||
expect(Routing.getCategory(BlockType.LOOP)).toBe(BlockCategory.FLOW_CONTROL)
|
||||
})
|
||||
|
||||
it.concurrent('should categorize routing blocks correctly', () => {
|
||||
expect(Routing.getCategory(BlockType.ROUTER)).toBe(BlockCategory.ROUTING_BLOCK)
|
||||
expect(Routing.getCategory(BlockType.CONDITION)).toBe(BlockCategory.ROUTING_BLOCK)
|
||||
})
|
||||
|
||||
it.concurrent('should categorize regular blocks correctly', () => {
|
||||
expect(Routing.getCategory(BlockType.FUNCTION)).toBe(BlockCategory.REGULAR_BLOCK)
|
||||
expect(Routing.getCategory(BlockType.AGENT)).toBe(BlockCategory.REGULAR_BLOCK)
|
||||
expect(Routing.getCategory(BlockType.API)).toBe(BlockCategory.REGULAR_BLOCK)
|
||||
expect(Routing.getCategory(BlockType.STARTER)).toBe(BlockCategory.REGULAR_BLOCK)
|
||||
})
|
||||
|
||||
it.concurrent('should default to regular block for unknown types', () => {
|
||||
expect(Routing.getCategory('unknown')).toBe(BlockCategory.REGULAR_BLOCK)
|
||||
expect(Routing.getCategory('')).toBe(BlockCategory.REGULAR_BLOCK)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldActivateDownstream', () => {
|
||||
it.concurrent('should return true for routing blocks', () => {
|
||||
expect(Routing.shouldActivateDownstream(BlockType.ROUTER)).toBe(true)
|
||||
expect(Routing.shouldActivateDownstream(BlockType.CONDITION)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should return false for flow control blocks', () => {
|
||||
expect(Routing.shouldActivateDownstream(BlockType.PARALLEL)).toBe(false)
|
||||
expect(Routing.shouldActivateDownstream(BlockType.LOOP)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should return true for regular blocks', () => {
|
||||
expect(Routing.shouldActivateDownstream(BlockType.FUNCTION)).toBe(true)
|
||||
expect(Routing.shouldActivateDownstream(BlockType.AGENT)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should handle empty/undefined block types', () => {
|
||||
expect(Routing.shouldActivateDownstream('')).toBe(true)
|
||||
expect(Routing.shouldActivateDownstream(undefined as any)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('requiresActivePathCheck', () => {
|
||||
it.concurrent('should return true for flow control blocks', () => {
|
||||
expect(Routing.requiresActivePathCheck(BlockType.PARALLEL)).toBe(true)
|
||||
expect(Routing.requiresActivePathCheck(BlockType.LOOP)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should return false for routing blocks', () => {
|
||||
expect(Routing.requiresActivePathCheck(BlockType.ROUTER)).toBe(false)
|
||||
expect(Routing.requiresActivePathCheck(BlockType.CONDITION)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should return false for regular blocks', () => {
|
||||
expect(Routing.requiresActivePathCheck(BlockType.FUNCTION)).toBe(false)
|
||||
expect(Routing.requiresActivePathCheck(BlockType.AGENT)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should handle empty/undefined block types', () => {
|
||||
expect(Routing.requiresActivePathCheck('')).toBe(false)
|
||||
expect(Routing.requiresActivePathCheck(undefined as any)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldSkipInSelectiveActivation', () => {
|
||||
it.concurrent('should return true for flow control blocks', () => {
|
||||
expect(Routing.shouldSkipInSelectiveActivation(BlockType.PARALLEL)).toBe(true)
|
||||
expect(Routing.shouldSkipInSelectiveActivation(BlockType.LOOP)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should return false for routing blocks', () => {
|
||||
expect(Routing.shouldSkipInSelectiveActivation(BlockType.ROUTER)).toBe(false)
|
||||
expect(Routing.shouldSkipInSelectiveActivation(BlockType.CONDITION)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should return false for regular blocks', () => {
|
||||
expect(Routing.shouldSkipInSelectiveActivation(BlockType.FUNCTION)).toBe(false)
|
||||
expect(Routing.shouldSkipInSelectiveActivation(BlockType.AGENT)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldSkipConnection', () => {
|
||||
it.concurrent('should skip flow control blocks', () => {
|
||||
expect(Routing.shouldSkipConnection(undefined, BlockType.PARALLEL)).toBe(true)
|
||||
expect(Routing.shouldSkipConnection('source', BlockType.LOOP)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should skip flow control specific connections', () => {
|
||||
expect(Routing.shouldSkipConnection('parallel-start-source', BlockType.FUNCTION)).toBe(true)
|
||||
expect(Routing.shouldSkipConnection('parallel-end-source', BlockType.AGENT)).toBe(true)
|
||||
expect(Routing.shouldSkipConnection('loop-start-source', BlockType.API)).toBe(true)
|
||||
expect(Routing.shouldSkipConnection('loop-end-source', BlockType.EVALUATOR)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should not skip regular connections to regular blocks', () => {
|
||||
expect(Routing.shouldSkipConnection('source', BlockType.FUNCTION)).toBe(false)
|
||||
expect(Routing.shouldSkipConnection('source', BlockType.AGENT)).toBe(false)
|
||||
expect(Routing.shouldSkipConnection(undefined, BlockType.API)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should not skip routing connections', () => {
|
||||
expect(Routing.shouldSkipConnection('condition-test-if', BlockType.FUNCTION)).toBe(false)
|
||||
expect(Routing.shouldSkipConnection('condition-test-else', BlockType.AGENT)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should handle empty/undefined types', () => {
|
||||
expect(Routing.shouldSkipConnection('', '')).toBe(false)
|
||||
expect(Routing.shouldSkipConnection(undefined, '')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getBehavior', () => {
|
||||
it.concurrent('should return correct behavior for each category', () => {
|
||||
const flowControlBehavior = Routing.getBehavior(BlockType.PARALLEL)
|
||||
expect(flowControlBehavior).toEqual({
|
||||
shouldActivateDownstream: false,
|
||||
requiresActivePathCheck: true,
|
||||
skipInSelectiveActivation: true,
|
||||
})
|
||||
|
||||
const routingBehavior = Routing.getBehavior(BlockType.ROUTER)
|
||||
expect(routingBehavior).toEqual({
|
||||
shouldActivateDownstream: true,
|
||||
requiresActivePathCheck: false,
|
||||
skipInSelectiveActivation: false,
|
||||
})
|
||||
|
||||
const regularBehavior = Routing.getBehavior(BlockType.FUNCTION)
|
||||
expect(regularBehavior).toEqual({
|
||||
shouldActivateDownstream: true,
|
||||
requiresActivePathCheck: false,
|
||||
skipInSelectiveActivation: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
97
apps/sim/executor/routing/routing.ts
Normal file
97
apps/sim/executor/routing/routing.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { BlockType } from '@/executor/consts'
|
||||
|
||||
export enum BlockCategory {
|
||||
ROUTING_BLOCK = 'routing', // router, condition - make routing decisions
|
||||
FLOW_CONTROL = 'flow-control', // parallel, loop - control execution flow
|
||||
REGULAR_BLOCK = 'regular', // function, agent, etc. - regular execution
|
||||
}
|
||||
|
||||
export interface RoutingBehavior {
|
||||
shouldActivateDownstream: boolean
|
||||
requiresActivePathCheck: boolean
|
||||
skipInSelectiveActivation: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Centralized routing strategy that defines how different block types
|
||||
* should behave in the execution path system.
|
||||
*/
|
||||
export class Routing {
|
||||
private static readonly BEHAVIOR_MAP: Record<BlockCategory, RoutingBehavior> = {
|
||||
[BlockCategory.ROUTING_BLOCK]: {
|
||||
shouldActivateDownstream: true,
|
||||
requiresActivePathCheck: false,
|
||||
skipInSelectiveActivation: false,
|
||||
},
|
||||
[BlockCategory.FLOW_CONTROL]: {
|
||||
shouldActivateDownstream: false,
|
||||
requiresActivePathCheck: true,
|
||||
skipInSelectiveActivation: true,
|
||||
},
|
||||
[BlockCategory.REGULAR_BLOCK]: {
|
||||
shouldActivateDownstream: true,
|
||||
requiresActivePathCheck: false,
|
||||
skipInSelectiveActivation: false,
|
||||
},
|
||||
}
|
||||
|
||||
private static readonly BLOCK_TYPE_TO_CATEGORY: Record<string, BlockCategory> = {
|
||||
// Flow control blocks
|
||||
[BlockType.PARALLEL]: BlockCategory.FLOW_CONTROL,
|
||||
[BlockType.LOOP]: BlockCategory.FLOW_CONTROL,
|
||||
|
||||
// Routing blocks
|
||||
[BlockType.ROUTER]: BlockCategory.ROUTING_BLOCK,
|
||||
[BlockType.CONDITION]: BlockCategory.ROUTING_BLOCK,
|
||||
|
||||
// Regular blocks (default category)
|
||||
[BlockType.FUNCTION]: BlockCategory.REGULAR_BLOCK,
|
||||
[BlockType.AGENT]: BlockCategory.REGULAR_BLOCK,
|
||||
[BlockType.API]: BlockCategory.REGULAR_BLOCK,
|
||||
[BlockType.EVALUATOR]: BlockCategory.REGULAR_BLOCK,
|
||||
[BlockType.RESPONSE]: BlockCategory.REGULAR_BLOCK,
|
||||
[BlockType.WORKFLOW]: BlockCategory.REGULAR_BLOCK,
|
||||
[BlockType.STARTER]: BlockCategory.REGULAR_BLOCK,
|
||||
}
|
||||
|
||||
static getCategory(blockType: string): BlockCategory {
|
||||
return Routing.BLOCK_TYPE_TO_CATEGORY[blockType] || BlockCategory.REGULAR_BLOCK
|
||||
}
|
||||
|
||||
static getBehavior(blockType: string): RoutingBehavior {
|
||||
const category = Routing.getCategory(blockType)
|
||||
return Routing.BEHAVIOR_MAP[category]
|
||||
}
|
||||
|
||||
static shouldActivateDownstream(blockType: string): boolean {
|
||||
return Routing.getBehavior(blockType).shouldActivateDownstream
|
||||
}
|
||||
|
||||
static requiresActivePathCheck(blockType: string): boolean {
|
||||
return Routing.getBehavior(blockType).requiresActivePathCheck
|
||||
}
|
||||
|
||||
static shouldSkipInSelectiveActivation(blockType: string): boolean {
|
||||
return Routing.getBehavior(blockType).skipInSelectiveActivation
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a connection should be skipped during selective activation
|
||||
*/
|
||||
static shouldSkipConnection(sourceHandle: string | undefined, targetBlockType: string): boolean {
|
||||
// Skip flow control blocks
|
||||
if (Routing.shouldSkipInSelectiveActivation(targetBlockType)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip flow control specific connections
|
||||
const flowControlHandles = [
|
||||
'parallel-start-source',
|
||||
'parallel-end-source',
|
||||
'loop-start-source',
|
||||
'loop-end-source',
|
||||
]
|
||||
|
||||
return flowControlHandles.includes(sourceHandle || '')
|
||||
}
|
||||
}
|
||||
227
apps/sim/executor/tests/executor-layer-validation.test.ts
Normal file
227
apps/sim/executor/tests/executor-layer-validation.test.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { Executor } from '@/executor'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import type { SerializedWorkflow } from '@/serializer/types'
|
||||
|
||||
describe('Full Executor Test', () => {
|
||||
let workflow: SerializedWorkflow
|
||||
let executor: Executor
|
||||
|
||||
beforeEach(() => {
|
||||
workflow = {
|
||||
version: '2.0',
|
||||
blocks: [
|
||||
{
|
||||
id: 'bd9f4f7d-8aed-4860-a3be-8bebd1931b19',
|
||||
position: { x: 0, y: 0 },
|
||||
metadata: { id: BlockType.STARTER, name: 'Start' },
|
||||
config: { tool: BlockType.STARTER, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'f29a40b7-125a-45a7-a670-af14a1498f94',
|
||||
position: { x: 100, y: 0 },
|
||||
metadata: { id: BlockType.ROUTER, name: 'Router 1' },
|
||||
config: {
|
||||
tool: BlockType.ROUTER,
|
||||
params: {
|
||||
prompt: 'if x then function 1\nif y then parallel\n\ninput: x',
|
||||
model: 'gpt-4o',
|
||||
},
|
||||
},
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b',
|
||||
position: { x: 200, y: -50 },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function 1' },
|
||||
config: { tool: BlockType.FUNCTION, params: { code: "return 'one'" } },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'a62902db-fd8d-4851-aa88-acd5e7667497',
|
||||
position: { x: 200, y: 50 },
|
||||
metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' },
|
||||
config: { tool: BlockType.PARALLEL, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: '0494cf56-2520-4e29-98ad-313ea55cf142',
|
||||
position: { x: 300, y: -50 },
|
||||
metadata: { id: 'condition', name: 'Condition 1' },
|
||||
config: { tool: 'condition', params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: '033ea142-3002-4a68-9e12-092b10b8c9c8',
|
||||
position: { x: 400, y: -100 },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function 2' },
|
||||
config: { tool: BlockType.FUNCTION, params: { code: "return 'two'" } },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: '037140a8-fda3-44e2-896c-6adea53ea30f',
|
||||
position: { x: 400, y: 0 },
|
||||
metadata: { id: BlockType.PARALLEL, name: 'Parallel 2' },
|
||||
config: { tool: BlockType.PARALLEL, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'a91e3a02-b884-4823-8197-30ae498ac94c',
|
||||
position: { x: 300, y: 100 },
|
||||
metadata: { id: 'agent', name: 'Agent 1' },
|
||||
config: { tool: 'agent', params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: '97974a42-cdf4-4810-9caa-b5e339f42ab0',
|
||||
position: { x: 500, y: 0 },
|
||||
metadata: { id: 'agent', name: 'Agent 2' },
|
||||
config: { tool: 'agent', params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
connections: [
|
||||
{
|
||||
source: 'bd9f4f7d-8aed-4860-a3be-8bebd1931b19',
|
||||
target: 'f29a40b7-125a-45a7-a670-af14a1498f94',
|
||||
},
|
||||
{
|
||||
source: 'f29a40b7-125a-45a7-a670-af14a1498f94',
|
||||
target: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b',
|
||||
},
|
||||
{
|
||||
source: 'f29a40b7-125a-45a7-a670-af14a1498f94',
|
||||
target: 'a62902db-fd8d-4851-aa88-acd5e7667497',
|
||||
},
|
||||
{
|
||||
source: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b',
|
||||
target: '0494cf56-2520-4e29-98ad-313ea55cf142',
|
||||
},
|
||||
{
|
||||
source: '0494cf56-2520-4e29-98ad-313ea55cf142',
|
||||
target: '033ea142-3002-4a68-9e12-092b10b8c9c8',
|
||||
sourceHandle: 'condition-0494cf56-2520-4e29-98ad-313ea55cf142-if',
|
||||
},
|
||||
{
|
||||
source: '0494cf56-2520-4e29-98ad-313ea55cf142',
|
||||
target: '037140a8-fda3-44e2-896c-6adea53ea30f',
|
||||
sourceHandle: 'condition-0494cf56-2520-4e29-98ad-313ea55cf142-else',
|
||||
},
|
||||
{
|
||||
source: 'a62902db-fd8d-4851-aa88-acd5e7667497',
|
||||
target: 'a91e3a02-b884-4823-8197-30ae498ac94c',
|
||||
sourceHandle: 'parallel-start-source',
|
||||
},
|
||||
{
|
||||
source: '037140a8-fda3-44e2-896c-6adea53ea30f',
|
||||
target: '97974a42-cdf4-4810-9caa-b5e339f42ab0',
|
||||
sourceHandle: 'parallel-start-source',
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
parallels: {
|
||||
'a62902db-fd8d-4851-aa88-acd5e7667497': {
|
||||
id: 'a62902db-fd8d-4851-aa88-acd5e7667497',
|
||||
nodes: ['a91e3a02-b884-4823-8197-30ae498ac94c'],
|
||||
distribution: ['item1', 'item2'],
|
||||
},
|
||||
'037140a8-fda3-44e2-896c-6adea53ea30f': {
|
||||
id: '037140a8-fda3-44e2-896c-6adea53ea30f',
|
||||
nodes: ['97974a42-cdf4-4810-9caa-b5e339f42ab0'],
|
||||
distribution: ['item1', 'item2'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
executor = new Executor(workflow)
|
||||
})
|
||||
|
||||
it('should test the full executor flow and see what happens', async () => {
|
||||
// Mock the necessary functions to avoid actual API calls
|
||||
const mockInput = {}
|
||||
|
||||
try {
|
||||
// Execute the workflow
|
||||
const result = await executor.execute('test-workflow-id')
|
||||
|
||||
// Check if it's an ExecutionResult (not StreamingExecution)
|
||||
if ('success' in result) {
|
||||
// Check if there are any logs that might indicate what happened
|
||||
if (result.logs) {
|
||||
}
|
||||
|
||||
// The test itself doesn't need to assert anything specific
|
||||
// We just want to see what the executor does
|
||||
expect(result.success).toBeDefined()
|
||||
} else {
|
||||
expect(result).toBeDefined()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Execution error:', error)
|
||||
// Log the error but don't fail the test - we want to see what happens
|
||||
}
|
||||
})
|
||||
|
||||
it('should test the executor getNextExecutionLayer method directly', async () => {
|
||||
// Create a mock context in the exact state after the condition executes
|
||||
const context = (executor as any).createExecutionContext('test-workflow', new Date())
|
||||
|
||||
// Set up the state as it would be after the condition executes
|
||||
context.executedBlocks.add('bd9f4f7d-8aed-4860-a3be-8bebd1931b19') // Start
|
||||
context.executedBlocks.add('f29a40b7-125a-45a7-a670-af14a1498f94') // Router 1
|
||||
context.executedBlocks.add('d09b0a90-2c59-4a2c-af15-c30321e36d9b') // Function 1
|
||||
context.executedBlocks.add('0494cf56-2520-4e29-98ad-313ea55cf142') // Condition 1
|
||||
context.executedBlocks.add('033ea142-3002-4a68-9e12-092b10b8c9c8') // Function 2
|
||||
|
||||
// Set router decision
|
||||
context.decisions.router.set(
|
||||
'f29a40b7-125a-45a7-a670-af14a1498f94',
|
||||
'd09b0a90-2c59-4a2c-af15-c30321e36d9b'
|
||||
)
|
||||
|
||||
// Set condition decision to if path (Function 2)
|
||||
context.decisions.condition.set(
|
||||
'0494cf56-2520-4e29-98ad-313ea55cf142',
|
||||
'0494cf56-2520-4e29-98ad-313ea55cf142-if'
|
||||
)
|
||||
|
||||
// Set up active execution path as it should be after condition
|
||||
context.activeExecutionPath.add('bd9f4f7d-8aed-4860-a3be-8bebd1931b19')
|
||||
context.activeExecutionPath.add('f29a40b7-125a-45a7-a670-af14a1498f94')
|
||||
context.activeExecutionPath.add('d09b0a90-2c59-4a2c-af15-c30321e36d9b')
|
||||
context.activeExecutionPath.add('0494cf56-2520-4e29-98ad-313ea55cf142')
|
||||
context.activeExecutionPath.add('033ea142-3002-4a68-9e12-092b10b8c9c8')
|
||||
|
||||
// Get the next execution layer
|
||||
const nextLayer = (executor as any).getNextExecutionLayer(context)
|
||||
|
||||
// Check if Parallel 2 is in the next execution layer
|
||||
const hasParallel2 = nextLayer.includes('037140a8-fda3-44e2-896c-6adea53ea30f')
|
||||
|
||||
// Check if Agent 2 is in the next execution layer
|
||||
const hasAgent2 = nextLayer.includes('97974a42-cdf4-4810-9caa-b5e339f42ab0')
|
||||
|
||||
// The key test: Parallel 2 should NOT be in the next execution layer
|
||||
expect(nextLayer).not.toContain('037140a8-fda3-44e2-896c-6adea53ea30f')
|
||||
expect(nextLayer).not.toContain('97974a42-cdf4-4810-9caa-b5e339f42ab0')
|
||||
})
|
||||
})
|
||||
307
apps/sim/executor/tests/nested-router-condition.test.ts
Normal file
307
apps/sim/executor/tests/nested-router-condition.test.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { PathTracker } from '@/executor/path/path'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedWorkflow } from '@/serializer/types'
|
||||
|
||||
describe('Nested Routing Fix - Router → Condition → Target', () => {
|
||||
let workflow: SerializedWorkflow
|
||||
let pathTracker: PathTracker
|
||||
let mockContext: ExecutionContext
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a workflow similar to the screenshot: Router → Condition → Function/Parallel
|
||||
workflow = {
|
||||
version: '2.0',
|
||||
blocks: [
|
||||
{
|
||||
id: 'starter',
|
||||
position: { x: 0, y: 0 },
|
||||
metadata: { id: BlockType.STARTER, name: 'Start' },
|
||||
config: { tool: BlockType.STARTER, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'router-1',
|
||||
position: { x: 100, y: 0 },
|
||||
metadata: { id: BlockType.ROUTER, name: 'Router 1' },
|
||||
config: { tool: BlockType.ROUTER, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'function-2',
|
||||
position: { x: 200, y: -100 },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function 2' },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'condition-1',
|
||||
position: { x: 200, y: 100 },
|
||||
metadata: { id: BlockType.CONDITION, name: 'Condition 1' },
|
||||
config: { tool: BlockType.CONDITION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'function-4',
|
||||
position: { x: 350, y: 50 },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function 4' },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'parallel-block',
|
||||
position: { x: 350, y: 150 },
|
||||
metadata: { id: BlockType.PARALLEL, name: 'Parallel Block' },
|
||||
config: { tool: BlockType.PARALLEL, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-inside-parallel',
|
||||
position: { x: 450, y: 150 },
|
||||
metadata: { id: BlockType.AGENT, name: 'Agent Inside Parallel' },
|
||||
config: { tool: BlockType.AGENT, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
connections: [
|
||||
{ source: 'starter', target: 'router-1' },
|
||||
{ source: 'router-1', target: 'function-2' },
|
||||
{ source: 'router-1', target: 'condition-1' },
|
||||
{
|
||||
source: 'condition-1',
|
||||
target: 'function-4',
|
||||
sourceHandle: 'condition-b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-if',
|
||||
},
|
||||
{
|
||||
source: 'condition-1',
|
||||
target: 'parallel-block',
|
||||
sourceHandle: 'condition-b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-else',
|
||||
},
|
||||
{
|
||||
source: 'parallel-block',
|
||||
target: 'agent-inside-parallel',
|
||||
sourceHandle: 'parallel-start-source',
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
parallels: {
|
||||
'parallel-block': {
|
||||
id: 'parallel-block',
|
||||
nodes: ['agent-inside-parallel'],
|
||||
distribution: ['item1', 'item2'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pathTracker = new PathTracker(workflow)
|
||||
|
||||
mockContext = {
|
||||
workflowId: 'test-workflow',
|
||||
blockStates: new Map(),
|
||||
blockLogs: [],
|
||||
metadata: { duration: 0 },
|
||||
environmentVariables: {},
|
||||
decisions: { router: new Map(), condition: new Map() },
|
||||
loopIterations: new Map(),
|
||||
loopItems: new Map(),
|
||||
completedLoops: new Set(),
|
||||
executedBlocks: new Set(),
|
||||
activeExecutionPath: new Set(),
|
||||
workflow,
|
||||
}
|
||||
|
||||
// Initialize starter as executed and in active path
|
||||
mockContext.executedBlocks.add('starter')
|
||||
mockContext.activeExecutionPath.add('starter')
|
||||
mockContext.activeExecutionPath.add('router-1')
|
||||
})
|
||||
|
||||
it('should handle nested routing: router selects condition, condition selects function', () => {
|
||||
// Step 1: Router selects the condition path (not function-2)
|
||||
mockContext.blockStates.set('router-1', {
|
||||
output: {
|
||||
selectedPath: {
|
||||
blockId: 'condition-1',
|
||||
blockType: BlockType.CONDITION,
|
||||
blockTitle: 'Condition 1',
|
||||
},
|
||||
},
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
mockContext.executedBlocks.add('router-1')
|
||||
|
||||
// Update paths after router execution
|
||||
pathTracker.updateExecutionPaths(['router-1'], mockContext)
|
||||
|
||||
// Verify router decision
|
||||
expect(mockContext.decisions.router.get('router-1')).toBe('condition-1')
|
||||
|
||||
// After router execution, condition should be active but not function-2
|
||||
expect(mockContext.activeExecutionPath.has('condition-1')).toBe(true)
|
||||
expect(mockContext.activeExecutionPath.has('function-2')).toBe(false)
|
||||
|
||||
// CRITICAL: Parallel block should NOT be activated yet
|
||||
expect(mockContext.activeExecutionPath.has('parallel-block')).toBe(false)
|
||||
expect(mockContext.activeExecutionPath.has('agent-inside-parallel')).toBe(false)
|
||||
|
||||
// Step 2: Condition executes and selects function-4 (not parallel)
|
||||
mockContext.blockStates.set('condition-1', {
|
||||
output: {
|
||||
result: 'two',
|
||||
stdout: '',
|
||||
conditionResult: true,
|
||||
selectedPath: {
|
||||
blockId: 'function-4',
|
||||
blockType: BlockType.FUNCTION,
|
||||
blockTitle: 'Function 4',
|
||||
},
|
||||
selectedConditionId: 'b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-if',
|
||||
},
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
mockContext.executedBlocks.add('condition-1')
|
||||
|
||||
// Update paths after condition execution
|
||||
pathTracker.updateExecutionPaths(['condition-1'], mockContext)
|
||||
|
||||
// Verify condition decision
|
||||
expect(mockContext.decisions.condition.get('condition-1')).toBe(
|
||||
'b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-if'
|
||||
)
|
||||
|
||||
// After condition execution, function-4 should be active
|
||||
expect(mockContext.activeExecutionPath.has('function-4')).toBe(true)
|
||||
|
||||
// CRITICAL: Parallel block should still NOT be activated
|
||||
expect(mockContext.activeExecutionPath.has('parallel-block')).toBe(false)
|
||||
expect(mockContext.activeExecutionPath.has('agent-inside-parallel')).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle nested routing: router selects condition, condition selects parallel', () => {
|
||||
// Step 1: Router selects the condition path
|
||||
mockContext.blockStates.set('router-1', {
|
||||
output: {
|
||||
selectedPath: {
|
||||
blockId: 'condition-1',
|
||||
blockType: BlockType.CONDITION,
|
||||
blockTitle: 'Condition 1',
|
||||
},
|
||||
},
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
mockContext.executedBlocks.add('router-1')
|
||||
|
||||
pathTracker.updateExecutionPaths(['router-1'], mockContext)
|
||||
|
||||
// Step 2: Condition executes and selects parallel-block (not function-4)
|
||||
mockContext.blockStates.set('condition-1', {
|
||||
output: {
|
||||
result: 'else',
|
||||
stdout: '',
|
||||
conditionResult: false,
|
||||
selectedPath: {
|
||||
blockId: 'parallel-block',
|
||||
blockType: BlockType.PARALLEL,
|
||||
blockTitle: 'Parallel Block',
|
||||
},
|
||||
selectedConditionId: 'b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-else',
|
||||
},
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
mockContext.executedBlocks.add('condition-1')
|
||||
|
||||
pathTracker.updateExecutionPaths(['condition-1'], mockContext)
|
||||
|
||||
// Verify condition decision
|
||||
expect(mockContext.decisions.condition.get('condition-1')).toBe(
|
||||
'b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-else'
|
||||
)
|
||||
|
||||
// After condition execution, parallel-block should be active
|
||||
expect(mockContext.activeExecutionPath.has('parallel-block')).toBe(true)
|
||||
|
||||
// Function-4 should NOT be activated
|
||||
expect(mockContext.activeExecutionPath.has('function-4')).toBe(false)
|
||||
|
||||
// The agent inside parallel should NOT be automatically activated
|
||||
// It should only be activated when the parallel block executes
|
||||
expect(mockContext.activeExecutionPath.has('agent-inside-parallel')).toBe(false)
|
||||
})
|
||||
|
||||
it('should prevent parallel blocks from executing when not selected by nested routing', () => {
|
||||
// This test simulates the exact scenario from the bug report
|
||||
|
||||
// Step 1: Router selects condition path
|
||||
mockContext.blockStates.set('router-1', {
|
||||
output: {
|
||||
selectedPath: {
|
||||
blockId: 'condition-1',
|
||||
blockType: BlockType.CONDITION,
|
||||
blockTitle: 'Condition 1',
|
||||
},
|
||||
},
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
mockContext.executedBlocks.add('router-1')
|
||||
pathTracker.updateExecutionPaths(['router-1'], mockContext)
|
||||
|
||||
// Step 2: Condition selects function-4 (NOT parallel)
|
||||
mockContext.blockStates.set('condition-1', {
|
||||
output: {
|
||||
result: 'two',
|
||||
stdout: '',
|
||||
conditionResult: true,
|
||||
selectedPath: {
|
||||
blockId: 'function-4',
|
||||
blockType: BlockType.FUNCTION,
|
||||
blockTitle: 'Function 4',
|
||||
},
|
||||
selectedConditionId: 'b8f0a33c-a57f-4a36-ac7a-dc9f2b5e6c07-if',
|
||||
},
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
mockContext.executedBlocks.add('condition-1')
|
||||
pathTracker.updateExecutionPaths(['condition-1'], mockContext)
|
||||
|
||||
// Step 3: Simulate what the executor's getNextExecutionLayer would do
|
||||
const blocksToExecute = workflow.blocks.filter(
|
||||
(block) =>
|
||||
mockContext.activeExecutionPath.has(block.id) && !mockContext.executedBlocks.has(block.id)
|
||||
)
|
||||
|
||||
const blockIds = blocksToExecute.map((b) => b.id)
|
||||
|
||||
// Should only include function-4, NOT parallel-block
|
||||
expect(blockIds).toContain('function-4')
|
||||
expect(blockIds).not.toContain('parallel-block')
|
||||
expect(blockIds).not.toContain('agent-inside-parallel')
|
||||
|
||||
// Verify that parallel block is not in active path
|
||||
expect(mockContext.activeExecutionPath.has('parallel-block')).toBe(false)
|
||||
|
||||
// Verify that isInActivePath also returns false for parallel block
|
||||
const isParallelActive = pathTracker.isInActivePath('parallel-block', mockContext)
|
||||
expect(isParallelActive).toBe(false)
|
||||
})
|
||||
})
|
||||
206
apps/sim/executor/tests/parallel-handler-routing.test.ts
Normal file
206
apps/sim/executor/tests/parallel-handler-routing.test.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { ParallelBlockHandler } from '@/executor/handlers/parallel/parallel-handler'
|
||||
import { PathTracker } from '@/executor/path/path'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedWorkflow } from '@/serializer/types'
|
||||
|
||||
describe('Parallel Handler Integration with PathTracker', () => {
|
||||
let workflow: SerializedWorkflow
|
||||
let pathTracker: PathTracker
|
||||
let parallelHandler: ParallelBlockHandler
|
||||
let mockContext: ExecutionContext
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a simplified workflow with condition → parallel scenario
|
||||
workflow = {
|
||||
version: '2.0',
|
||||
blocks: [
|
||||
{
|
||||
id: 'condition-1',
|
||||
position: { x: 0, y: 0 },
|
||||
metadata: { id: BlockType.CONDITION, name: 'Condition 1' },
|
||||
config: { tool: BlockType.CONDITION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'function-2',
|
||||
position: { x: 100, y: -50 },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function 2' },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'parallel-2',
|
||||
position: { x: 100, y: 50 },
|
||||
metadata: { id: BlockType.PARALLEL, name: 'Parallel 2' },
|
||||
config: { tool: BlockType.PARALLEL, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'agent-2',
|
||||
position: { x: 200, y: 50 },
|
||||
metadata: { id: BlockType.AGENT, name: 'Agent 2' },
|
||||
config: { tool: BlockType.AGENT, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
connections: [
|
||||
// Condition → Function 2 (if path)
|
||||
{
|
||||
source: 'condition-1',
|
||||
target: 'function-2',
|
||||
sourceHandle: 'condition-test-if',
|
||||
},
|
||||
// Condition → Parallel 2 (else path)
|
||||
{
|
||||
source: 'condition-1',
|
||||
target: 'parallel-2',
|
||||
sourceHandle: 'condition-test-else',
|
||||
},
|
||||
// Parallel 2 → Agent 2
|
||||
{
|
||||
source: 'parallel-2',
|
||||
target: 'agent-2',
|
||||
sourceHandle: 'parallel-start-source',
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
parallels: {
|
||||
'parallel-2': {
|
||||
id: 'parallel-2',
|
||||
nodes: ['agent-2'],
|
||||
distribution: ['item1', 'item2'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pathTracker = new PathTracker(workflow)
|
||||
parallelHandler = new ParallelBlockHandler(undefined, pathTracker)
|
||||
|
||||
mockContext = {
|
||||
workflowId: 'test-workflow',
|
||||
blockStates: new Map(),
|
||||
blockLogs: [],
|
||||
metadata: { duration: 0 },
|
||||
environmentVariables: {},
|
||||
decisions: { router: new Map(), condition: new Map() },
|
||||
loopIterations: new Map(),
|
||||
loopItems: new Map(),
|
||||
completedLoops: new Set(),
|
||||
executedBlocks: new Set(),
|
||||
activeExecutionPath: new Set(),
|
||||
workflow,
|
||||
}
|
||||
})
|
||||
|
||||
it('should not allow parallel block to execute when not in active path', async () => {
|
||||
// Set up scenario where condition selected function-2 (if path), not parallel-2 (else path)
|
||||
mockContext.decisions.condition.set('condition-1', 'test-if')
|
||||
mockContext.executedBlocks.add('condition-1')
|
||||
mockContext.activeExecutionPath.add('condition-1')
|
||||
mockContext.activeExecutionPath.add('function-2') // Only function-2 should be active
|
||||
|
||||
// Parallel-2 should NOT be in active path
|
||||
expect(mockContext.activeExecutionPath.has('parallel-2')).toBe(false)
|
||||
|
||||
// Test PathTracker's isInActivePath method
|
||||
const isParallel2Active = pathTracker.isInActivePath('parallel-2', mockContext)
|
||||
expect(isParallel2Active).toBe(false)
|
||||
|
||||
// Get the parallel block
|
||||
const parallelBlock = workflow.blocks.find((b) => b.id === 'parallel-2')!
|
||||
|
||||
// Try to execute the parallel block
|
||||
const result = await parallelHandler.execute(parallelBlock, {}, mockContext)
|
||||
|
||||
// The parallel block should execute (return started: true) but should NOT activate its children
|
||||
expect(result).toMatchObject({
|
||||
parallelId: 'parallel-2',
|
||||
started: true,
|
||||
})
|
||||
|
||||
// CRITICAL: Agent 2 should NOT be activated because parallel-2 is not in active path
|
||||
expect(mockContext.activeExecutionPath.has('agent-2')).toBe(false)
|
||||
})
|
||||
|
||||
it('should allow parallel block to execute and activate children when in active path', async () => {
|
||||
// Set up scenario where condition selected parallel-2 (else path)
|
||||
mockContext.decisions.condition.set('condition-1', 'test-else')
|
||||
mockContext.executedBlocks.add('condition-1')
|
||||
mockContext.activeExecutionPath.add('condition-1')
|
||||
mockContext.activeExecutionPath.add('parallel-2') // Parallel-2 should be active
|
||||
|
||||
// Parallel-2 should be in active path
|
||||
expect(mockContext.activeExecutionPath.has('parallel-2')).toBe(true)
|
||||
|
||||
// Test PathTracker's isInActivePath method
|
||||
const isParallel2Active = pathTracker.isInActivePath('parallel-2', mockContext)
|
||||
expect(isParallel2Active).toBe(true)
|
||||
|
||||
// Get the parallel block
|
||||
const parallelBlock = workflow.blocks.find((b) => b.id === 'parallel-2')!
|
||||
|
||||
// Try to execute the parallel block
|
||||
const result = await parallelHandler.execute(parallelBlock, {}, mockContext)
|
||||
|
||||
// The parallel block should execute and activate its children
|
||||
expect(result).toMatchObject({
|
||||
parallelId: 'parallel-2',
|
||||
started: true,
|
||||
})
|
||||
|
||||
// Agent 2 should be activated because parallel-2 is in active path
|
||||
expect(mockContext.activeExecutionPath.has('agent-2')).toBe(true)
|
||||
})
|
||||
|
||||
it('should test the routing failure scenario with parallel block', async () => {
|
||||
// Step 1: Condition 1 selects Function 2 (if path)
|
||||
mockContext.blockStates.set('condition-1', {
|
||||
output: {
|
||||
result: 'one',
|
||||
stdout: '',
|
||||
conditionResult: true,
|
||||
selectedPath: {
|
||||
blockId: 'function-2',
|
||||
blockType: 'function',
|
||||
blockTitle: 'Function 2',
|
||||
},
|
||||
selectedConditionId: 'test-if',
|
||||
},
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
mockContext.executedBlocks.add('condition-1')
|
||||
mockContext.activeExecutionPath.add('condition-1')
|
||||
|
||||
// Update paths after condition execution
|
||||
pathTracker.updateExecutionPaths(['condition-1'], mockContext)
|
||||
|
||||
// Verify condition selected if path
|
||||
expect(mockContext.decisions.condition.get('condition-1')).toBe('test-if')
|
||||
expect(mockContext.activeExecutionPath.has('function-2')).toBe(true)
|
||||
expect(mockContext.activeExecutionPath.has('parallel-2')).toBe(false)
|
||||
|
||||
// Step 2: Try to execute parallel-2 (should not activate children)
|
||||
const parallelBlock = workflow.blocks.find((b) => b.id === 'parallel-2')!
|
||||
const result = await parallelHandler.execute(parallelBlock, {}, mockContext)
|
||||
|
||||
// Parallel should execute but not activate children
|
||||
expect(result).toMatchObject({
|
||||
parallelId: 'parallel-2',
|
||||
started: true,
|
||||
})
|
||||
|
||||
// CRITICAL: Agent 2 should NOT be activated
|
||||
expect(mockContext.activeExecutionPath.has('agent-2')).toBe(false)
|
||||
})
|
||||
})
|
||||
318
apps/sim/executor/tests/router-parallel-execution.test.ts
Normal file
318
apps/sim/executor/tests/router-parallel-execution.test.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { BlockType } from '@/executor/consts'
|
||||
import { PathTracker } from '@/executor/path/path'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedWorkflow } from '@/serializer/types'
|
||||
|
||||
describe('Router and Condition Block Path Selection in Complex Workflows', () => {
|
||||
let workflow: SerializedWorkflow
|
||||
let pathTracker: PathTracker
|
||||
let mockContext: ExecutionContext
|
||||
|
||||
beforeEach(() => {
|
||||
workflow = {
|
||||
version: '2.0',
|
||||
blocks: [
|
||||
{
|
||||
id: 'bd9f4f7d-8aed-4860-a3be-8bebd1931b19',
|
||||
position: { x: 0, y: 0 },
|
||||
metadata: { id: BlockType.STARTER, name: 'Start' },
|
||||
config: { tool: BlockType.STARTER, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'f29a40b7-125a-45a7-a670-af14a1498f94',
|
||||
position: { x: 100, y: 0 },
|
||||
metadata: { id: BlockType.ROUTER, name: 'Router 1' },
|
||||
config: { tool: BlockType.ROUTER, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b',
|
||||
position: { x: 200, y: -50 },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function 1' },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'a62902db-fd8d-4851-aa88-acd5e7667497',
|
||||
position: { x: 200, y: 50 },
|
||||
metadata: { id: BlockType.PARALLEL, name: 'Parallel 1' },
|
||||
config: { tool: BlockType.PARALLEL, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: '0494cf56-2520-4e29-98ad-313ea55cf142',
|
||||
position: { x: 300, y: -50 },
|
||||
metadata: { id: BlockType.CONDITION, name: 'Condition 1' },
|
||||
config: { tool: BlockType.CONDITION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: '033ea142-3002-4a68-9e12-092b10b8c9c8',
|
||||
position: { x: 400, y: -100 },
|
||||
metadata: { id: BlockType.FUNCTION, name: 'Function 2' },
|
||||
config: { tool: BlockType.FUNCTION, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: '037140a8-fda3-44e2-896c-6adea53ea30f',
|
||||
position: { x: 400, y: 0 },
|
||||
metadata: { id: BlockType.PARALLEL, name: 'Parallel 2' },
|
||||
config: { tool: BlockType.PARALLEL, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'a91e3a02-b884-4823-8197-30ae498ac94c',
|
||||
position: { x: 300, y: 100 },
|
||||
metadata: { id: BlockType.AGENT, name: 'Agent 1' },
|
||||
config: { tool: BlockType.AGENT, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: '97974a42-cdf4-4810-9caa-b5e339f42ab0',
|
||||
position: { x: 500, y: 0 },
|
||||
metadata: { id: BlockType.AGENT, name: 'Agent 2' },
|
||||
config: { tool: BlockType.AGENT, params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
connections: [
|
||||
// Start → Router 1
|
||||
{
|
||||
source: 'bd9f4f7d-8aed-4860-a3be-8bebd1931b19',
|
||||
target: 'f29a40b7-125a-45a7-a670-af14a1498f94',
|
||||
},
|
||||
// Router 1 → Function 1
|
||||
{
|
||||
source: 'f29a40b7-125a-45a7-a670-af14a1498f94',
|
||||
target: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b',
|
||||
},
|
||||
// Router 1 → Parallel 1
|
||||
{
|
||||
source: 'f29a40b7-125a-45a7-a670-af14a1498f94',
|
||||
target: 'a62902db-fd8d-4851-aa88-acd5e7667497',
|
||||
},
|
||||
// Function 1 → Condition 1
|
||||
{
|
||||
source: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b',
|
||||
target: '0494cf56-2520-4e29-98ad-313ea55cf142',
|
||||
},
|
||||
// Condition 1 → Function 2 (if path)
|
||||
{
|
||||
source: '0494cf56-2520-4e29-98ad-313ea55cf142',
|
||||
target: '033ea142-3002-4a68-9e12-092b10b8c9c8',
|
||||
sourceHandle: 'condition-0494cf56-2520-4e29-98ad-313ea55cf142-if',
|
||||
},
|
||||
// Condition 1 → Parallel 2 (else path)
|
||||
{
|
||||
source: '0494cf56-2520-4e29-98ad-313ea55cf142',
|
||||
target: '037140a8-fda3-44e2-896c-6adea53ea30f',
|
||||
sourceHandle: 'condition-0494cf56-2520-4e29-98ad-313ea55cf142-else',
|
||||
},
|
||||
// Parallel 1 → Agent 1
|
||||
{
|
||||
source: 'a62902db-fd8d-4851-aa88-acd5e7667497',
|
||||
target: 'a91e3a02-b884-4823-8197-30ae498ac94c',
|
||||
sourceHandle: 'parallel-start-source',
|
||||
},
|
||||
// Parallel 2 → Agent 2
|
||||
{
|
||||
source: '037140a8-fda3-44e2-896c-6adea53ea30f',
|
||||
target: '97974a42-cdf4-4810-9caa-b5e339f42ab0',
|
||||
sourceHandle: 'parallel-start-source',
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
parallels: {
|
||||
'a62902db-fd8d-4851-aa88-acd5e7667497': {
|
||||
id: 'a62902db-fd8d-4851-aa88-acd5e7667497',
|
||||
nodes: ['a91e3a02-b884-4823-8197-30ae498ac94c'],
|
||||
distribution: ['item1', 'item2'],
|
||||
},
|
||||
'037140a8-fda3-44e2-896c-6adea53ea30f': {
|
||||
id: '037140a8-fda3-44e2-896c-6adea53ea30f',
|
||||
nodes: ['97974a42-cdf4-4810-9caa-b5e339f42ab0'],
|
||||
distribution: ['item1', 'item2'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pathTracker = new PathTracker(workflow)
|
||||
|
||||
mockContext = {
|
||||
workflowId: 'test-workflow',
|
||||
blockStates: new Map(),
|
||||
blockLogs: [],
|
||||
metadata: { duration: 0 },
|
||||
environmentVariables: {},
|
||||
decisions: { router: new Map(), condition: new Map() },
|
||||
loopIterations: new Map(),
|
||||
loopItems: new Map(),
|
||||
completedLoops: new Set(),
|
||||
executedBlocks: new Set(),
|
||||
activeExecutionPath: new Set(),
|
||||
workflow,
|
||||
}
|
||||
|
||||
// Initialize execution state
|
||||
mockContext.executedBlocks.add('bd9f4f7d-8aed-4860-a3be-8bebd1931b19') // Start
|
||||
mockContext.activeExecutionPath.add('bd9f4f7d-8aed-4860-a3be-8bebd1931b19') // Start
|
||||
mockContext.activeExecutionPath.add('f29a40b7-125a-45a7-a670-af14a1498f94') // Router 1
|
||||
})
|
||||
|
||||
it('should reproduce the exact router and condition block path selection scenario', () => {
|
||||
// Step 1: Router 1 executes and selects Function 1 (not Parallel 1)
|
||||
mockContext.blockStates.set('f29a40b7-125a-45a7-a670-af14a1498f94', {
|
||||
output: {
|
||||
selectedPath: {
|
||||
blockId: 'd09b0a90-2c59-4a2c-af15-c30321e36d9b',
|
||||
blockType: BlockType.FUNCTION,
|
||||
blockTitle: 'Function 1',
|
||||
},
|
||||
},
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
mockContext.executedBlocks.add('f29a40b7-125a-45a7-a670-af14a1498f94')
|
||||
|
||||
pathTracker.updateExecutionPaths(['f29a40b7-125a-45a7-a670-af14a1498f94'], mockContext)
|
||||
|
||||
// Verify router selected Function 1
|
||||
expect(mockContext.decisions.router.get('f29a40b7-125a-45a7-a670-af14a1498f94')).toBe(
|
||||
'd09b0a90-2c59-4a2c-af15-c30321e36d9b'
|
||||
)
|
||||
expect(mockContext.activeExecutionPath.has('d09b0a90-2c59-4a2c-af15-c30321e36d9b')).toBe(true) // Function 1
|
||||
|
||||
// Parallel 1 should NOT be in active path (not selected by router)
|
||||
expect(mockContext.activeExecutionPath.has('a62902db-fd8d-4851-aa88-acd5e7667497')).toBe(false) // Parallel 1
|
||||
expect(mockContext.activeExecutionPath.has('a91e3a02-b884-4823-8197-30ae498ac94c')).toBe(false) // Agent 1
|
||||
|
||||
// Step 2: Function 1 executes and returns "one"
|
||||
mockContext.blockStates.set('d09b0a90-2c59-4a2c-af15-c30321e36d9b', {
|
||||
output: {
|
||||
result: 'one',
|
||||
stdout: '',
|
||||
},
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
mockContext.executedBlocks.add('d09b0a90-2c59-4a2c-af15-c30321e36d9b')
|
||||
|
||||
pathTracker.updateExecutionPaths(['d09b0a90-2c59-4a2c-af15-c30321e36d9b'], mockContext)
|
||||
|
||||
// Function 1 should activate Condition 1
|
||||
expect(mockContext.activeExecutionPath.has('0494cf56-2520-4e29-98ad-313ea55cf142')).toBe(true) // Condition 1
|
||||
|
||||
// Parallel 2 should NOT be in active path yet
|
||||
expect(mockContext.activeExecutionPath.has('037140a8-fda3-44e2-896c-6adea53ea30f')).toBe(false) // Parallel 2
|
||||
expect(mockContext.activeExecutionPath.has('97974a42-cdf4-4810-9caa-b5e339f42ab0')).toBe(false) // Agent 2
|
||||
|
||||
// Step 3: Condition 1 executes and selects Function 2 (if path, not else/parallel path)
|
||||
mockContext.blockStates.set('0494cf56-2520-4e29-98ad-313ea55cf142', {
|
||||
output: {
|
||||
result: 'one',
|
||||
stdout: '',
|
||||
conditionResult: true,
|
||||
selectedPath: {
|
||||
blockId: '033ea142-3002-4a68-9e12-092b10b8c9c8',
|
||||
blockType: BlockType.FUNCTION,
|
||||
blockTitle: 'Function 2',
|
||||
},
|
||||
selectedConditionId: '0494cf56-2520-4e29-98ad-313ea55cf142-if',
|
||||
},
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
mockContext.executedBlocks.add('0494cf56-2520-4e29-98ad-313ea55cf142')
|
||||
|
||||
pathTracker.updateExecutionPaths(['0494cf56-2520-4e29-98ad-313ea55cf142'], mockContext)
|
||||
|
||||
// Verify condition selected the if path (Function 2)
|
||||
expect(mockContext.decisions.condition.get('0494cf56-2520-4e29-98ad-313ea55cf142')).toBe(
|
||||
'0494cf56-2520-4e29-98ad-313ea55cf142-if'
|
||||
)
|
||||
expect(mockContext.activeExecutionPath.has('033ea142-3002-4a68-9e12-092b10b8c9c8')).toBe(true) // Function 2
|
||||
|
||||
// CRITICAL: Parallel 2 should NOT be in active path (condition selected if, not else)
|
||||
expect(mockContext.activeExecutionPath.has('037140a8-fda3-44e2-896c-6adea53ea30f')).toBe(false) // Parallel 2
|
||||
expect(mockContext.activeExecutionPath.has('97974a42-cdf4-4810-9caa-b5e339f42ab0')).toBe(false) // Agent 2
|
||||
|
||||
// Step 4: Function 2 executes (this should be the end of the workflow)
|
||||
mockContext.blockStates.set('033ea142-3002-4a68-9e12-092b10b8c9c8', {
|
||||
output: {
|
||||
result: 'two',
|
||||
stdout: '',
|
||||
},
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
mockContext.executedBlocks.add('033ea142-3002-4a68-9e12-092b10b8c9c8')
|
||||
|
||||
pathTracker.updateExecutionPaths(['033ea142-3002-4a68-9e12-092b10b8c9c8'], mockContext)
|
||||
|
||||
// Final verification: Parallel 2 and Agent 2 should NEVER be in active path
|
||||
expect(mockContext.activeExecutionPath.has('037140a8-fda3-44e2-896c-6adea53ea30f')).toBe(false) // Parallel 2
|
||||
expect(mockContext.activeExecutionPath.has('97974a42-cdf4-4810-9caa-b5e339f42ab0')).toBe(false) // Agent 2
|
||||
|
||||
// Simulate what executor's getNextExecutionLayer would return
|
||||
const blocksToExecute = workflow.blocks.filter(
|
||||
(block) =>
|
||||
mockContext.activeExecutionPath.has(block.id) && !mockContext.executedBlocks.has(block.id)
|
||||
)
|
||||
const blockIds = blocksToExecute.map((b) => b.id)
|
||||
|
||||
// Should be empty (no more blocks to execute)
|
||||
expect(blockIds).toHaveLength(0)
|
||||
|
||||
// Should NOT include Parallel 2 or Agent 2
|
||||
expect(blockIds).not.toContain('037140a8-fda3-44e2-896c-6adea53ea30f') // Parallel 2
|
||||
expect(blockIds).not.toContain('97974a42-cdf4-4810-9caa-b5e339f42ab0') // Agent 2
|
||||
})
|
||||
|
||||
it('should test the isInActivePath method for Parallel 2', () => {
|
||||
// Set up the same execution state as above
|
||||
mockContext.executedBlocks.add('f29a40b7-125a-45a7-a670-af14a1498f94') // Router 1
|
||||
mockContext.executedBlocks.add('d09b0a90-2c59-4a2c-af15-c30321e36d9b') // Function 1
|
||||
mockContext.executedBlocks.add('0494cf56-2520-4e29-98ad-313ea55cf142') // Condition 1
|
||||
|
||||
// Set router decision
|
||||
mockContext.decisions.router.set(
|
||||
'f29a40b7-125a-45a7-a670-af14a1498f94',
|
||||
'd09b0a90-2c59-4a2c-af15-c30321e36d9b'
|
||||
)
|
||||
|
||||
// Set condition decision to if path (not else path)
|
||||
mockContext.decisions.condition.set(
|
||||
'0494cf56-2520-4e29-98ad-313ea55cf142',
|
||||
'0494cf56-2520-4e29-98ad-313ea55cf142-if'
|
||||
)
|
||||
|
||||
// Test isInActivePath for Parallel 2
|
||||
const isParallel2Active = pathTracker.isInActivePath(
|
||||
'037140a8-fda3-44e2-896c-6adea53ea30f',
|
||||
mockContext
|
||||
)
|
||||
|
||||
// Should be false because condition selected if path, not else path
|
||||
expect(isParallel2Active).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { StreamingResponseFormatProcessor, streamingResponseFormatProcessor } from './utils'
|
||||
import {
|
||||
StreamingResponseFormatProcessor,
|
||||
streamingResponseFormatProcessor,
|
||||
} from '@/executor/utils'
|
||||
|
||||
vi.mock('@/lib/logs/console-logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import type { ResponseFormatStreamProcessor } from './types'
|
||||
import type { ResponseFormatStreamProcessor } from '@/executor/types'
|
||||
|
||||
const logger = createLogger('ExecutorUtils')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user