chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,406 @@
|
||||
import { assertNoLargeValueRefs } from '@/lib/execution/payloads/large-value-ref'
|
||||
import {
|
||||
isReference,
|
||||
normalizeName,
|
||||
parseReferencePath,
|
||||
SPECIAL_REFERENCE_PREFIXES,
|
||||
} from '@/executor/constants'
|
||||
import { getBlockSchema } from '@/executor/utils/block-data'
|
||||
import {
|
||||
InvalidFieldError,
|
||||
type OutputSchema,
|
||||
resolveBlockReference,
|
||||
resolveBlockReferenceAsync,
|
||||
} from '@/executor/utils/block-reference'
|
||||
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
|
||||
import { buildClonedSubflowId, extractOuterBranchIndex } from '@/executor/utils/subflow-utils'
|
||||
import {
|
||||
type AsyncPathNavigator,
|
||||
navigatePath,
|
||||
RESOLVED_EMPTY,
|
||||
type ResolutionContext,
|
||||
type Resolver,
|
||||
} from '@/executor/variables/resolvers/reference'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
|
||||
export class BlockResolver implements Resolver {
|
||||
private nameToBlockId: Map<string, string>
|
||||
private blockById: Map<string, SerializedBlock>
|
||||
private blockIdsInSubflows: Set<string>
|
||||
private subflowContainerIds: Set<string>
|
||||
|
||||
constructor(
|
||||
private workflow: SerializedWorkflow,
|
||||
private navigatePathAsync?: AsyncPathNavigator
|
||||
) {
|
||||
this.nameToBlockId = new Map()
|
||||
this.blockById = new Map()
|
||||
this.blockIdsInSubflows = new Set()
|
||||
this.subflowContainerIds = new Set([
|
||||
...Object.keys(workflow.loops ?? {}),
|
||||
...Object.keys(workflow.parallels ?? {}),
|
||||
])
|
||||
for (const block of workflow.blocks) {
|
||||
this.blockById.set(block.id, block)
|
||||
if (block.metadata?.name) {
|
||||
// Name uniqueness is enforced at the normalized level on create/rename,
|
||||
// but legacy workflows may contain names that collide only now that
|
||||
// normalizeName strips dots. Dot-free names keep ownership of the key so
|
||||
// previously working references never change targets.
|
||||
const normalizedName = normalizeName(block.metadata.name)
|
||||
const incumbentId = this.nameToBlockId.get(normalizedName)
|
||||
const incumbentName = incumbentId
|
||||
? this.blockById.get(incumbentId)?.metadata?.name
|
||||
: undefined
|
||||
if (!incumbentId || incumbentName?.includes('.')) {
|
||||
this.nameToBlockId.set(normalizedName, block.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const loop of Object.values(workflow.loops ?? {})) {
|
||||
for (const blockId of loop.nodes ?? []) {
|
||||
this.blockIdsInSubflows.add(blockId)
|
||||
}
|
||||
}
|
||||
for (const parallel of Object.values(workflow.parallels ?? {})) {
|
||||
for (const blockId of parallel.nodes ?? []) {
|
||||
this.blockIdsInSubflows.add(blockId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canResolve(reference: string): boolean {
|
||||
if (!isReference(reference)) {
|
||||
return false
|
||||
}
|
||||
const parts = parseReferencePath(reference)
|
||||
if (parts.length === 0) {
|
||||
return false
|
||||
}
|
||||
const [type] = parts
|
||||
return !(SPECIAL_REFERENCE_PREFIXES as readonly string[]).includes(type)
|
||||
}
|
||||
|
||||
resolve(reference: string, context: ResolutionContext): any {
|
||||
const parts = parseReferencePath(reference)
|
||||
if (parts.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
const [blockName, ...pathParts] = parts
|
||||
|
||||
const blockId = this.findBlockIdByName(blockName)
|
||||
if (!blockId) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const block = this.blockById.get(blockId)!
|
||||
const output = this.getBlockOutput(blockId, context)
|
||||
|
||||
const blockData: Record<string, unknown> = {}
|
||||
const blockOutputSchemas: Record<string, OutputSchema> = {}
|
||||
|
||||
if (output !== undefined) {
|
||||
blockData[blockId] = output
|
||||
}
|
||||
|
||||
const outputSchema = getBlockSchema(block)
|
||||
|
||||
if (outputSchema && Object.keys(outputSchema).length > 0) {
|
||||
blockOutputSchemas[blockId] = outputSchema
|
||||
}
|
||||
|
||||
try {
|
||||
const result = resolveBlockReference(
|
||||
blockName,
|
||||
pathParts,
|
||||
{
|
||||
blockNameMapping: Object.fromEntries(this.nameToBlockId),
|
||||
blockData,
|
||||
blockOutputSchemas,
|
||||
},
|
||||
{
|
||||
allowLargeValueRefs: context.allowLargeValueRefs,
|
||||
executionContext: context.executionContext,
|
||||
}
|
||||
)!
|
||||
|
||||
if (result.value !== undefined) {
|
||||
if (!context.allowLargeValueRefs) {
|
||||
assertNoLargeValueRefs(result.value)
|
||||
}
|
||||
return result.value
|
||||
}
|
||||
|
||||
const backwardsCompat = this.handleBackwardsCompatSync(block, output, pathParts)
|
||||
if (backwardsCompat !== undefined) {
|
||||
return backwardsCompat
|
||||
}
|
||||
|
||||
return RESOLVED_EMPTY
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidFieldError) {
|
||||
const fallback = this.handleBackwardsCompatSync(block, output, pathParts)
|
||||
if (fallback !== undefined) {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async resolveAsync(reference: string, context: ResolutionContext): Promise<any> {
|
||||
if (!this.navigatePathAsync) {
|
||||
return this.resolve(reference, context)
|
||||
}
|
||||
const parts = parseReferencePath(reference)
|
||||
if (parts.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
const [blockName, ...pathParts] = parts
|
||||
|
||||
const blockId = this.findBlockIdByName(blockName)
|
||||
if (!blockId) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const block = this.blockById.get(blockId)!
|
||||
const output = this.getBlockOutput(blockId, context)
|
||||
|
||||
const blockData: Record<string, unknown> = {}
|
||||
const blockOutputSchemas: Record<string, OutputSchema> = {}
|
||||
|
||||
if (output !== undefined) {
|
||||
blockData[blockId] = output
|
||||
}
|
||||
|
||||
const outputSchema = getBlockSchema(block)
|
||||
|
||||
if (outputSchema && Object.keys(outputSchema).length > 0) {
|
||||
blockOutputSchemas[blockId] = outputSchema
|
||||
}
|
||||
|
||||
try {
|
||||
const blockReferenceContext = {
|
||||
blockNameMapping: Object.fromEntries(this.nameToBlockId),
|
||||
blockData,
|
||||
blockOutputSchemas,
|
||||
}
|
||||
const result = (await resolveBlockReferenceAsync(
|
||||
blockName,
|
||||
pathParts,
|
||||
blockReferenceContext,
|
||||
context,
|
||||
this.navigatePathAsync
|
||||
))!
|
||||
|
||||
if (result.value !== undefined) {
|
||||
if (!context.allowLargeValueRefs) {
|
||||
assertNoLargeValueRefs(result.value)
|
||||
}
|
||||
return result.value
|
||||
}
|
||||
|
||||
const backwardsCompat = await this.handleBackwardsCompat(block, output, pathParts, context)
|
||||
if (backwardsCompat !== undefined) {
|
||||
return backwardsCompat
|
||||
}
|
||||
|
||||
return RESOLVED_EMPTY
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidFieldError) {
|
||||
const fallback = await this.handleBackwardsCompat(block, output, pathParts, context)
|
||||
if (fallback !== undefined) {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private handleBackwardsCompatSync(
|
||||
block: SerializedBlock,
|
||||
output: unknown,
|
||||
pathParts: string[]
|
||||
): unknown {
|
||||
if (output === undefined || pathParts.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (
|
||||
block.metadata?.id === 'response' &&
|
||||
pathParts[0] === 'response' &&
|
||||
(output as Record<string, unknown>)?.response === undefined
|
||||
) {
|
||||
const adjustedPathParts = pathParts.slice(1)
|
||||
if (adjustedPathParts.length === 0) {
|
||||
return output
|
||||
}
|
||||
const fallbackResult = navigatePath(output, adjustedPathParts)
|
||||
if (fallbackResult !== undefined) {
|
||||
return fallbackResult
|
||||
}
|
||||
}
|
||||
|
||||
const outputRecord = output as Record<string, unknown> | undefined
|
||||
if (
|
||||
(block.metadata?.id === 'workflow' || block.metadata?.id === 'workflow_input') &&
|
||||
pathParts[0] === 'result' &&
|
||||
pathParts[1] === 'response' &&
|
||||
outputRecord?.result !== undefined &&
|
||||
typeof outputRecord.result === 'object' &&
|
||||
outputRecord.result !== null &&
|
||||
(outputRecord.result as Record<string, unknown>)?.response === undefined
|
||||
) {
|
||||
const adjustedPathParts = ['result', ...pathParts.slice(2)]
|
||||
const fallbackResult = navigatePath(output, adjustedPathParts)
|
||||
if (fallbackResult !== undefined) {
|
||||
return fallbackResult
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
private async handleBackwardsCompat(
|
||||
block: SerializedBlock,
|
||||
output: unknown,
|
||||
pathParts: string[],
|
||||
context: ResolutionContext
|
||||
): Promise<unknown> {
|
||||
const navigatePathAsync = this.navigatePathAsync
|
||||
if (!navigatePathAsync) {
|
||||
return this.handleBackwardsCompatSync(block, output, pathParts)
|
||||
}
|
||||
|
||||
if (output === undefined || pathParts.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (
|
||||
block.metadata?.id === 'response' &&
|
||||
pathParts[0] === 'response' &&
|
||||
(output as Record<string, unknown>)?.response === undefined
|
||||
) {
|
||||
const adjustedPathParts = pathParts.slice(1)
|
||||
if (adjustedPathParts.length === 0) {
|
||||
return output
|
||||
}
|
||||
const fallbackResult = await navigatePathAsync(output, adjustedPathParts, context)
|
||||
if (fallbackResult !== undefined) {
|
||||
return fallbackResult
|
||||
}
|
||||
}
|
||||
|
||||
const isWorkflowBlock =
|
||||
block.metadata?.id === 'workflow' || block.metadata?.id === 'workflow_input'
|
||||
const outputRecord = output as Record<string, Record<string, unknown> | undefined>
|
||||
if (
|
||||
isWorkflowBlock &&
|
||||
pathParts[0] === 'result' &&
|
||||
pathParts[1] === 'response' &&
|
||||
outputRecord?.result?.response === undefined
|
||||
) {
|
||||
const adjustedPathParts = ['result', ...pathParts.slice(2)]
|
||||
const fallbackResult = await navigatePathAsync(output, adjustedPathParts, context)
|
||||
if (fallbackResult !== undefined) {
|
||||
return fallbackResult
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
private getBlockOutput(blockId: string, context: ResolutionContext): any {
|
||||
const outerBranchIndex = extractOuterBranchIndex(context.currentNodeId)
|
||||
const mappedBranchIndex =
|
||||
outerBranchIndex ??
|
||||
context.executionContext.parallelBlockMapping?.get(context.currentNodeId)?.iterationIndex
|
||||
const shouldResolveClonedSubflowOutput =
|
||||
mappedBranchIndex !== undefined &&
|
||||
mappedBranchIndex > 0 &&
|
||||
this.subflowContainerIds.has(blockId)
|
||||
|
||||
if (shouldResolveClonedSubflowOutput) {
|
||||
const clonedStateOutput = context.executionState.getBlockOutput(
|
||||
buildClonedSubflowId(blockId, mappedBranchIndex)
|
||||
)
|
||||
if (clonedStateOutput !== undefined) {
|
||||
return clonedStateOutput
|
||||
}
|
||||
}
|
||||
|
||||
const stateOutput = context.executionState.getBlockOutput(blockId, context.currentNodeId)
|
||||
if (stateOutput !== undefined) {
|
||||
return stateOutput
|
||||
}
|
||||
|
||||
if (
|
||||
shouldResolveClonedSubflowOutput ||
|
||||
(outerBranchIndex !== undefined && this.blockIdsInSubflows.has(blockId))
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const contextState = context.executionContext.blockStates?.get(blockId)
|
||||
if (contextState?.output) {
|
||||
return contextState.output
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
private findBlockIdByName(name: string): string | undefined {
|
||||
return this.nameToBlockId.get(normalizeName(name))
|
||||
}
|
||||
|
||||
public formatValueForBlock(value: any, blockType: string | undefined, language?: string): string {
|
||||
if (blockType === 'condition') {
|
||||
return this.stringifyForCondition(value)
|
||||
}
|
||||
|
||||
if (blockType === 'function') {
|
||||
return this.formatValueForCodeContext(value, language)
|
||||
}
|
||||
|
||||
if (blockType === 'response') {
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
return String(value)
|
||||
}
|
||||
|
||||
private stringifyForCondition(value: any): string {
|
||||
if (typeof value === 'string') {
|
||||
const sanitized = value
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
return `"${sanitized}"`
|
||||
}
|
||||
if (value === null) {
|
||||
return 'null'
|
||||
}
|
||||
if (value === undefined) {
|
||||
return 'undefined'
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
private formatValueForCodeContext(value: any, language?: string): string {
|
||||
return formatLiteralForCode(value, language === 'python' ? 'python' : 'javascript')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user