(graph: WorkflowGraphDto)
| 59 | } |
| 60 | |
| 61 | export function compileWorkflowGraph(graph: WorkflowGraphDto): WorkflowDefinition { |
| 62 | // Filter out UI-only nodes (like text-block) that shouldn't be executed |
| 63 | const executableNodes = graph.nodes.filter((node: WorkflowNodeDto) => { |
| 64 | const component = componentRegistry.get(node.type); |
| 65 | if (!component) { |
| 66 | return true; // Let validation catch unknown components |
| 67 | } |
| 68 | // Skip UI-only components (they're for documentation/notes, not execution) |
| 69 | const isUiOnly = (component.ui as any)?.uiOnly === true; |
| 70 | return !isUiOnly; |
| 71 | }); |
| 72 | |
| 73 | const nodeIds = executableNodes.map((node: WorkflowNodeDto) => node.id); |
| 74 | |
| 75 | // Ensure all executable nodes reference registered components. |
| 76 | for (const node of executableNodes) { |
| 77 | if (!componentRegistry.get(node.type)) { |
| 78 | throw new Error(`Component not registered: ${node.type}`); |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | const orderedIds = topoSort(nodeIds, graph.edges); |
| 83 | const incomingEdges = new Map<string, Set<string>>(); |
| 84 | type GraphEdge = (typeof graph.edges)[number]; |
| 85 | const edgesByTarget = new Map<string, GraphEdge[]>(); |
| 86 | for (const nodeId of nodeIds) { |
| 87 | incomingEdges.set(nodeId, new Set()); |
| 88 | edgesByTarget.set(nodeId, []); |
| 89 | } |
| 90 | for (const edge of graph.edges) { |
| 91 | incomingEdges.get(edge.target)?.add(edge.source); |
| 92 | edgesByTarget.get(edge.target)?.push(edge); |
| 93 | } |
| 94 | |
| 95 | const nodesMetadata: Record<string, WorkflowNodeMetadata> = {}; |
| 96 | for (const node of executableNodes) { |
| 97 | const config = (node.data?.config ?? {}) as Record<string, unknown>; |
| 98 | const joinStrategyValue = config.joinStrategy; |
| 99 | const joinStrategy = |
| 100 | typeof joinStrategyValue === 'string' && ['all', 'any', 'first'].includes(joinStrategyValue) |
| 101 | ? (joinStrategyValue as WorkflowNodeMetadata['joinStrategy']) |
| 102 | : undefined; |
| 103 | |
| 104 | const streamIdValue = config.streamId; |
| 105 | const groupIdValue = config.groupId; |
| 106 | const maxConcurrencyValue = config.maxConcurrency; |
| 107 | |
| 108 | const mode = (config.mode as WorkflowNodeMetadata['mode']) ?? 'normal'; |
| 109 | const toolConfig = config.toolConfig as WorkflowNodeMetadata['toolConfig']; |
| 110 | |
| 111 | const connectedToolNodeIds = edgesByTarget |
| 112 | .get(node.id) |
| 113 | ?.filter((edge) => edge.targetHandle === 'tools') |
| 114 | .map((edge) => edge.source); |
| 115 | |
| 116 | nodesMetadata[node.id] = { |
| 117 | ref: node.id, |
| 118 | mode, |
no test coverage detected