Files
simstudioai--sim/apps/sim/executor/utils/iteration-context.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

166 lines
5.7 KiB
TypeScript

import { DEFAULTS } from '@/executor/constants'
import type { NodeMetadata } from '@/executor/dag/types'
import type { IterationContext, ParentIteration } from '@/executor/execution/types'
import type { ExecutionContext } from '@/executor/types'
import { findEffectiveContainerId } from '@/executor/utils/subflow-utils'
/** Maximum ancestor depth to prevent runaway traversal in deeply nested subflows. */
const MAX_PARENT_DEPTH = DEFAULTS.MAX_NESTING_DEPTH
/**
* Subset of {@link NodeMetadata} needed for iteration context resolution.
* Compatible with both DAGNode.metadata and inline metadata objects.
*/
export type IterationNodeMetadata = Pick<
NodeMetadata,
'subflowType' | 'subflowId' | 'branchIndex' | 'branchTotal' | 'isLoopNode'
>
/**
* Resolves the iteration context for a node based on its metadata and execution state.
* Handles both parallel (branch) and loop iteration contexts, including cross-type
* nesting (loop-in-parallel, parallel-in-loop) via the unified subflow parent map.
*/
export function getIterationContext(
ctx: ExecutionContext,
metadata: IterationNodeMetadata | undefined
): IterationContext | undefined {
if (!metadata) return undefined
if (metadata.branchIndex !== undefined && metadata.branchTotal !== undefined) {
const parallelId = metadata.subflowType === 'parallel' ? metadata.subflowId : undefined
const parentIterations = parallelId ? buildUnifiedParentIterations(ctx, parallelId) : []
return {
iterationCurrent: metadata.branchIndex,
iterationTotal: metadata.branchTotal,
iterationType: 'parallel',
iterationContainerId: parallelId,
...(parentIterations.length > 0 && { parentIterations }),
}
}
const loopId = metadata.subflowType === 'loop' ? metadata.subflowId : undefined
if (metadata.isLoopNode && loopId) {
const loopScope = ctx.loopExecutions?.get(loopId)
if (loopScope && loopScope.iteration !== undefined) {
const parentIterations = buildUnifiedParentIterations(ctx, loopId)
return {
iterationCurrent: loopScope.iteration,
iterationTotal: loopScope.maxIterations,
iterationType: 'loop',
iterationContainerId: loopId,
...(parentIterations.length > 0 && { parentIterations }),
}
}
}
return undefined
}
/**
* Builds a single-level iteration context for a container (loop/parallel) that is
* nested inside a parent subflow. Used by orchestrators when emitting onBlockComplete
* for container sentinel nodes.
*/
export function buildContainerIterationContext(
ctx: ExecutionContext,
containerId: string
): IterationContext | undefined {
const parentEntry = ctx.subflowParentMap?.get(containerId)
if (!parentEntry) return undefined
if (parentEntry.parentType === 'parallel') {
if (parentEntry.branchIndex !== undefined) {
const parentScope = ctx.parallelExecutions?.get(parentEntry.parentId)
if (!parentScope) return undefined
return {
iterationCurrent: parentEntry.branchIndex,
iterationTotal: parentScope.totalBranches,
iterationType: 'parallel',
iterationContainerId: parentEntry.parentId,
}
}
} else if (parentEntry.parentType === 'loop') {
const effectiveParentId = ctx.loopExecutions
? findEffectiveContainerId(parentEntry.parentId, containerId, ctx.loopExecutions)
: parentEntry.parentId
const parentScope = ctx.loopExecutions?.get(effectiveParentId)
if (parentScope && parentScope.iteration !== undefined) {
return {
iterationCurrent: parentScope.iteration,
iterationTotal: parentScope.maxIterations,
iterationType: 'loop',
iterationContainerId: effectiveParentId,
}
}
}
return undefined
}
/**
* Walks the unified subflow parent map to build the full ancestor iteration chain,
* handling all nesting combinations (loop-in-loop, parallel-in-parallel,
* loop-in-parallel, parallel-in-loop).
*
* Returns an array of parent iteration contexts, ordered from outermost to innermost.
*/
export function buildUnifiedParentIterations(
ctx: ExecutionContext,
subflowId: string
): ParentIteration[] {
if (!ctx.subflowParentMap) {
return []
}
const parents: ParentIteration[] = []
const visited = new Set<string>()
let currentId = subflowId
while (
ctx.subflowParentMap.has(currentId) &&
!visited.has(currentId) &&
visited.size < MAX_PARENT_DEPTH
) {
visited.add(currentId)
const entry = ctx.subflowParentMap.get(currentId)!
const { parentId, parentType } = entry
if (parentType === 'loop') {
// Resolve the effective (possibly cloned) loop ID — at runtime the scope
// may live under a cloned ID like `mid-loop__obranch-2` rather than `mid-loop`
const effectiveParentId = ctx.loopExecutions
? findEffectiveContainerId(parentId, currentId, ctx.loopExecutions)
: parentId
const parentScope = ctx.loopExecutions?.get(effectiveParentId)
if (parentScope && parentScope.iteration !== undefined) {
parents.unshift({
iterationCurrent: parentScope.iteration,
iterationTotal: parentScope.maxIterations,
iterationType: 'loop',
iterationContainerId: effectiveParentId,
})
}
} else {
if (entry.branchIndex === undefined) {
currentId = parentId
continue
}
const effectiveParentId = parentId
const parentScope = ctx.parallelExecutions?.get(effectiveParentId)
if (parentScope) {
parents.unshift({
iterationCurrent: entry.branchIndex,
iterationTotal: parentScope.totalBranches,
iterationType: 'parallel',
iterationContainerId: effectiveParentId,
})
}
}
currentId = parentId
}
return parents
}