-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Expand file tree
/
Copy pathsubflow-utils.ts
More file actions
336 lines (298 loc) · 10.3 KB
/
Copy pathsubflow-utils.ts
File metadata and controls
336 lines (298 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { DEFAULTS } from '@/executor/constants'
import type { ContextExtensions } from '@/executor/execution/types'
import { type BlockLog, type ExecutionContext, getNextExecutionOrder } from '@/executor/types'
import { buildContainerIterationContext } from '@/executor/utils/iteration-context'
import { SubflowNodeIdCodec } from '@/executor/utils/subflow-node-id-codec'
import type { SerializedWorkflow } from '@/serializer/types'
const logger = createLogger('SubflowUtils')
/**
* Builds the loop sentinel-start node ID for a container.
*/
export function buildSentinelStartId(loopId: string): string {
return SubflowNodeIdCodec.buildLoopSentinelStartId(loopId)
}
/**
* Builds the loop sentinel-end node ID for a container.
*/
export function buildSentinelEndId(loopId: string): string {
return SubflowNodeIdCodec.buildLoopSentinelEndId(loopId)
}
export function buildParallelSentinelStartId(parallelId: string): string {
return SubflowNodeIdCodec.buildParallelSentinelStartId(parallelId)
}
export function buildParallelSentinelEndId(parallelId: string): string {
return SubflowNodeIdCodec.buildParallelSentinelEndId(parallelId)
}
export function isLoopSentinelNodeId(nodeId: string): boolean {
return SubflowNodeIdCodec.isLoopSentinelNodeId(nodeId)
}
export function isParallelSentinelNodeId(nodeId: string): boolean {
return SubflowNodeIdCodec.isParallelSentinelNodeId(nodeId)
}
export function isSentinelNodeId(nodeId: string): boolean {
return isLoopSentinelNodeId(nodeId) || isParallelSentinelNodeId(nodeId)
}
export function extractLoopIdFromSentinel(sentinelId: string): string | null {
return SubflowNodeIdCodec.extractLoopIdFromSentinel(sentinelId)
}
export function extractParallelIdFromSentinel(sentinelId: string): string | null {
return SubflowNodeIdCodec.extractParallelIdFromSentinel(sentinelId)
}
/**
* Build branch node ID with subscript notation
* Example: ("blockId", 2) → "blockId₍2₎"
*/
export function buildBranchNodeId(baseId: string, branchIndex: number): string {
return SubflowNodeIdCodec.buildBranchNodeId(baseId, branchIndex)
}
export function extractBaseBlockId(branchNodeId: string): string {
return SubflowNodeIdCodec.extractBaseBlockId(branchNodeId)
}
export function extractBranchIndex(branchNodeId: string): number | null {
return SubflowNodeIdCodec.extractBranchIndex(branchNodeId)
}
export function isBranchNodeId(nodeId: string): boolean {
return SubflowNodeIdCodec.isBranchNodeId(nodeId)
}
/**
* Extracts the outer branch index from a cloned subflow ID.
* Cloned IDs follow the pattern `{originalId}__obranch-{index}`.
* Returns undefined if the ID is not a clone.
*/
export function extractOuterBranchIndex(clonedId: string): number | undefined {
return SubflowNodeIdCodec.extractOuterBranchIndex(clonedId)
}
export function extractInnermostOuterBranchIndex(clonedId: string): number | undefined {
return SubflowNodeIdCodec.extractInnermostOuterBranchIndex(clonedId)
}
/**
* Strips all clone suffixes (`__obranch-N`) and branch subscripts (`₍N₎`)
* from a node ID, returning the original workflow-level block ID.
*/
export function stripCloneSuffixes(nodeId: string): string {
return SubflowNodeIdCodec.stripCloneSuffixes(nodeId)
}
/**
* Builds a stable ID for an output scoped to a global outer parallel branch.
*/
export function buildOuterBranchScopedId(originalId: string, branchIndex: number): string {
return SubflowNodeIdCodec.buildOuterBranchScopedId(originalId, branchIndex)
}
/**
* Builds a cloned subflow ID from an original ID and outer branch index.
*/
export function buildClonedSubflowId(originalId: string, branchIndex: number): string {
return SubflowNodeIdCodec.buildOuterBranchScopedId(originalId, branchIndex)
}
/**
* Strips outer-branch clone suffixes (`__obranch-N`) from an ID,
* returning the original workflow-level subflow ID.
*/
export function stripOuterBranchSuffix(id: string): string {
return SubflowNodeIdCodec.stripOuterBranchSuffix(id)
}
/**
* Finds the effective (possibly cloned) container ID for a subflow,
* given the current node's ID and an execution map (loopExecutions or parallelExecutions).
*
* When inside a cloned subflow (e.g., loop-1__obranch-2), the execution scope is
* stored under the cloned ID, not the original. This function extracts the `__obranch-N`
* suffix from the current node ID, constructs the candidate cloned container ID, and
* checks if it exists in the execution map.
*
* Returns the effective ID (cloned or original) that exists in the map.
*/
export function findEffectiveContainerId(
originalId: string,
currentNodeId: string,
executionMap: Map<string, unknown>,
mappedBranchIndex?: number
): string {
return SubflowNodeIdCodec.findEffectiveContainerId(
originalId,
currentNodeId,
executionMap,
mappedBranchIndex
)
}
export function normalizeNodeId(nodeId: string): string {
return SubflowNodeIdCodec.normalizeNodeId(nodeId)
}
type SubflowContainerType = 'loop' | 'parallel'
function getSubflowNodes(
workflow: Pick<SerializedWorkflow, 'loops' | 'parallels'>,
type: SubflowContainerType,
id: string
): string[] | undefined {
return type === 'loop' ? workflow.loops?.[id]?.nodes : workflow.parallels?.[id]?.nodes
}
export function subflowContainsBlock(
workflow: Pick<SerializedWorkflow, 'loops' | 'parallels'>,
containerType: SubflowContainerType,
containerId: string,
baseBlockId: string,
visited = new Set<string>()
): boolean {
const visitKey = `${containerType}:${containerId}`
if (visited.has(visitKey)) return false
visited.add(visitKey)
const nodes = getSubflowNodes(workflow, containerType, containerId)
if (!nodes) return false
for (const nodeId of nodes) {
if (nodeId === baseBlockId) return true
if (workflow.loops?.[nodeId]) {
if (subflowContainsBlock(workflow, 'loop', nodeId, baseBlockId, visited)) return true
} else if (workflow.parallels?.[nodeId]) {
if (subflowContainsBlock(workflow, 'parallel', nodeId, baseBlockId, visited)) return true
}
}
return false
}
export function isSubflowNestedInside(
workflow: Pick<SerializedWorkflow, 'loops' | 'parallels'>,
childType: SubflowContainerType,
childId: string,
ancestorType: SubflowContainerType,
ancestorId: string,
visited = new Set<string>()
): boolean {
const visitKey = `${ancestorType}:${ancestorId}`
if (visited.has(visitKey)) return false
visited.add(visitKey)
const nodes = getSubflowNodes(workflow, ancestorType, ancestorId)
if (!nodes) return false
for (const nodeId of nodes) {
if (
nodeId === childId &&
(childType === 'loop' ? workflow.loops?.[childId] : workflow.parallels?.[childId])
) {
return true
}
if (workflow.loops?.[nodeId]) {
if (isSubflowNestedInside(workflow, childType, childId, 'loop', nodeId, visited)) {
return true
}
} else if (workflow.parallels?.[nodeId]) {
if (isSubflowNestedInside(workflow, childType, childId, 'parallel', nodeId, visited)) {
return true
}
}
}
return false
}
/**
* Creates and logs an error for a subflow (loop or parallel).
*/
export async function addSubflowErrorLog(
ctx: ExecutionContext,
blockId: string,
blockType: 'loop' | 'parallel',
errorMessage: string,
inputData: Record<string, any>,
contextExtensions: ContextExtensions | null
): Promise<void> {
const now = new Date().toISOString()
const execOrder = getNextExecutionOrder(ctx)
const block = ctx.workflow?.blocks?.find((b) => b.id === blockId)
const blockName = block?.metadata?.name || (blockType === 'loop' ? 'Loop' : 'Parallel')
const blockLog: BlockLog = {
blockId,
blockName,
blockType,
startedAt: now,
executionOrder: execOrder,
endedAt: now,
durationMs: 0,
success: false,
error: errorMessage,
input: inputData,
output: { error: errorMessage },
...(blockType === 'loop' ? { loopId: blockId } : { parallelId: blockId }),
}
ctx.blockLogs.push(blockLog)
if (contextExtensions?.onBlockStart) {
try {
await contextExtensions.onBlockStart(blockId, blockName, blockType, execOrder)
} catch (error) {
logger.warn('Subflow error start callback failed', {
blockId,
blockType,
error: toError(error).message,
})
}
}
if (contextExtensions?.onBlockComplete) {
try {
await contextExtensions.onBlockComplete(blockId, blockName, blockType, {
input: inputData,
output: { error: errorMessage },
executionTime: 0,
startedAt: now,
executionOrder: execOrder,
endedAt: now,
})
} catch (error) {
logger.warn('Subflow error completion callback failed', {
blockId,
blockType,
error: toError(error).message,
})
}
}
}
/**
* Emits the BlockLog + onBlockComplete callback for a loop/parallel container that
* finished successfully. Without this, successful container runs produce no top-level BlockLog,
* which forces the trace-span builder to fall back
* to generic counter-based names ("Loop 1", "Parallel 1") instead of the user-configured
* block name.
*/
export async function emitSubflowSuccessEvents(
ctx: ExecutionContext,
blockId: string,
blockType: 'loop' | 'parallel',
output: { results: unknown },
contextExtensions: ContextExtensions | null
): Promise<void> {
const now = new Date().toISOString()
const executionOrder = getNextExecutionOrder(ctx)
const block = ctx.workflow?.blocks.find((b) => b.id === blockId)
const blockName = block?.metadata?.name ?? blockType
const iterationContext = buildContainerIterationContext(ctx, blockId)
ctx.blockLogs.push({
blockId,
blockName,
blockType,
startedAt: now,
endedAt: now,
durationMs: DEFAULTS.EXECUTION_TIME,
success: true,
output,
executionOrder,
})
if (contextExtensions?.onBlockComplete) {
try {
await contextExtensions.onBlockComplete(
blockId,
blockName,
blockType,
{
output,
executionTime: DEFAULTS.EXECUTION_TIME,
startedAt: now,
executionOrder,
endedAt: now,
},
iterationContext
)
} catch (error) {
logger.warn('Subflow success completion callback failed', {
blockId,
blockType,
error: toError(error).message,
})
}
}
}