import { AUTOMATION_LOG_MAX_ENTRIES, CALLBACK_PROTECTION_QUERY_PARAMETER, type GenerationResult, type RuntimeActionAttemptCompletion, type RuntimeActionAttemptStart, type RuntimeActionCompletion, type RuntimeCoordinationOffer, type RuntimeRunContext as ContractRuntimeRunContext, type RuntimeTargetAction, type RuntimeValueContext as ContractRuntimeValueContext, type SignalDerivation, type runtimeContract, } from "@automate.ax/api-contract/runtime" import { createAutomateClient } from "@automate.ax/client" import { codecEnvelopeSerializer, decodeCodecEnvelope, encodeCodecEnvelope, } from "@automate.ax/codec/rpc" import { decode, type Encodable } from "@automate.ax/codec" import { createORPCClient } from "@orpc/client" import { RPCLink } from "@orpc/client/fetch" import type { ContractRouterClient } from "@orpc/contract" import { AsyncLocalStorage } from "node:async_hooks" import { joinURL } from "ufo" import { EMAIL_ORIGIN_TOKEN } from "../lib/email-origin" import type { ActionRuntime } from "./actions" import type { AutomationDescriptor } from "./automation" import { validateActionContextBoundaries } from "./context-boundary-planning" import type { IntegrationAccountConnectionOption } from "./integrations" import { setRuntimeAutomateClient } from "./runtime-management" import { FailedSignalError, normalizeAutomationError, type AutomationError, type Signal, } from "./signal-protocol" const HOOK_SCOPE_STACKS = new WeakMap() const DECLARATION_SLOTS = new WeakMap() interface HookScopeFrame { nextHookSlot: number path: number[] } /** Stable nested namespace containing one durable hook slot. */ export interface HookLocation { readonly scopePath: readonly number[] readonly slot: number } interface AutomationInvocationEnvironment { appOrigin: string automationId: string parameterValues?: Readonly> projectId: string } interface RuntimeContext extends AutomationInvocationEnvironment { apiOrigin: string contextId: string emailOriginToken: string phase: "run" token: string } /** Read-only durable identity of the active automation execution. */ export interface AutomationExecutionIdentity { readonly automationId: string readonly contextId: string } /** Raised when replayed automation structure no longer matches durable state. */ export class AutomationCompatibilityError extends Error { override name = "AutomationCompatibilityError" } export interface PlanningContext { accountDeclarations: { binding: string connectionOptionGroups?: readonly (readonly IntegrationAccountConnectionOption[])[] serviceId: string }[] accountUses: ( | { binding: string connectionOptions: readonly IntegrationAccountConnectionOption[] dynamic: false serviceId: string } | { connectionOptions: readonly IntegrationAccountConnectionOption[] dynamic: true serviceId: string } )[] nextHookSlot: number parameterIssues?: string[] parameters?: import("@automate.ax/catalog").FormField[] scopes: { name?: string path: number[] presentation: "collapsed" | "expanded" | "hidden" }[] subscriptions: { config: unknown eventType: string hookSlot: number scopePath?: number[] }[] usesMarkSignificant?: boolean } type RuntimeClient = ContractRouterClient /** Codec-enveloped runtime state returned by the transport contract. */ type RuntimeTransportLoadResult = Awaited> /** Codec-enveloped value context returned by the transport contract. */ type RuntimeTransportValueContext = Awaited< ReturnType > interface RuntimeValueClient { loadValues( input: Parameters[0], ): Promise } /** Runtime procedures available while one action handler is executing. */ type ActionRuntimeClient = Pick< RuntimeClient, | "createCallbackToken" | "evaluate" | "generate" | "invokeAutomation" | "sendEmail" | "sendLog" | "sendOutput" | "startImageGeneration" | "startWebScrape" > interface RuntimeLoadResult { context: RuntimeRunContext targetAction?: RuntimeTargetAction } export type RuntimeRunContext = ContractRuntimeRunContext export type RuntimeValueContext = ContractRuntimeValueContext type RuntimeCommitInput = Parameters[0] /** One action candidate accepted by the runtime commit contract. */ type CommittedActionInvocation = RuntimeCommitInput["actionInvocations"][number] type CommittedCoordinationOffer = RuntimeCoordinationOffer type CommittedRaceOffer = Extract< CommittedCoordinationOffer, { policy: "race" } > type CommittedCorrelationOffer = Extract< CommittedCoordinationOffer, { policy: "correlate" } > type CommittedCollectionOffer = Extract< CommittedCoordinationOffer, { policy: "collect" } > type CommittedFanoutOffer = Extract< CommittedCoordinationOffer, { policy: "fanout" } > type CommittedFunnelOffer = Extract< CommittedCoordinationOffer, { policy: "funnel" } > /** Merge offer retained separately for planned evaluation adaptation. */ type CommittedMergeOffer = Extract< CommittedCoordinationOffer, { policy: "merge" } > type CommittedSerializationOffer = Extract< CommittedCoordinationOffer, { policy: "serialize" } > type CommittedConcurrencyOffer = Extract< CommittedCoordinationOffer, { policy: "concurrent" } > type CommittedTakeOffer = Extract< CommittedCoordinationOffer, { policy: "take" } > type CommittedRateLimitOffer = Extract< CommittedCoordinationOffer, { policy: "rateLimit" } > /** Defers literal input hashing while symbolic traversal continues. */ type PlannedActionInvocation = T extends { inputHash: string } ? Omit & { inputHash: Promise } : never export type SignalEvaluationOutcome = | { status: "closed" } | { failure: AutomationError; status: "failed" } | { status: "open" } export interface PlannedSignalEvaluation { actionDependencyIds: string[] derivation: SignalDerivation | null evaluate(context: RuntimeValueContext): SignalEvaluationOutcome eventDependencyIds: string[] scopePath: number[] signalDependencyIds: string[] slot: number } export interface PlannedSignificanceEvaluation { actionDependencyIds: string[] evaluate(context: RuntimeValueContext): boolean eventDependencyIds: string[] signalDependencyIds: string[] } export interface PlannedCorrelationEvaluation extends Omit { evaluate( this: void, context: RuntimeValueContext, ): Pick scopePath: number[] } export interface PlannedCollectionEvaluation extends Omit< CommittedCollectionOffer, "count" | "key" | "position" | "scopePath" > { evaluate( this: void, context: RuntimeValueContext, ): Pick scopePath: number[] } export interface PlannedFanoutEvaluation extends Omit { evaluate( this: void, context: RuntimeValueContext, ): Pick scopePath: number[] } export interface PlannedFunnelEvaluation extends Omit< CommittedFunnelOffer, | "key" | "maxBurstDuration" | "minGap" | "minQuietPeriod" | "scopePath" | "until" > { evaluate( this: void, context: RuntimeValueContext, ): Pick< CommittedFunnelOffer, "key" | "maxBurstDuration" | "minGap" | "minQuietPeriod" | "until" > scopePath: number[] } export interface PlannedMergeEvaluation extends Omit { evaluate(this: void, context: RuntimeValueContext): void scopePath: number[] } export interface PlannedSerializationEvaluation extends Omit { evaluate( this: void, context: RuntimeValueContext, ): Pick scopePath: number[] } export interface PlannedConcurrencyEvaluation extends Omit { evaluate( this: void, context: RuntimeValueContext, ): Pick scopePath: number[] } export interface PlannedTakeEvaluation extends Omit { evaluate( this: void, context: RuntimeValueContext, ): Pick scopePath: number[] } export interface PlannedRateLimitEvaluation extends Omit { evaluate( this: void, context: RuntimeValueContext, ): Pick scopePath: number[] } type PlannedTerminalRaceEvaluation = Extract< CommittedRaceOffer, { status: "closed" | "failed" } > export type PlannedRaceEvaluation = | (Extract & { evaluate(this: void, context: RuntimeValueContext): void }) | PlannedTerminalRaceEvaluation export type PlannedCoordinationEvaluation = | PlannedCollectionEvaluation | PlannedConcurrencyEvaluation | PlannedCorrelationEvaluation | PlannedFanoutEvaluation | PlannedFunnelEvaluation | PlannedMergeEvaluation | PlannedRateLimitEvaluation | PlannedRaceEvaluation | PlannedSerializationEvaluation | PlannedTakeEvaluation export interface AutomationRunState { actionDeferred: boolean actionExecution?: () => Promise actionInvocations: PlannedActionInvocation[] compatibilityChecks: Promise[] completeAction(input: RuntimeActionCompletion): Promise completeActionAttempt(input: RuntimeActionAttemptCompletion): Promise context: RuntimeRunContext coordinationEvaluations: PlannedCoordinationEvaluation[] googleAdsRequest?: RuntimeClient["googleAdsRequest"] initiatingSubscriptionSignals?: Signal[] nextHookSlot: number nonInitiatingSubscriptionOrigins?: Set phase: "run" resolveAccountInput: RuntimeClient["resolveAccountInput"] runtime: ActionRuntime signalEvaluations: PlannedSignalEvaluation[] signalInvocations: RuntimeCommitInput["signalInvocations"] significanceEvaluations: PlannedSignificanceEvaluation[] startActionAttempt( replaySafety: "safe" | "unsafe", ): Promise targetAction?: Extract< NonNullable, { status: "pending" } > } interface InvocationStore { invocationEnvironment?: AutomationInvocationEnvironment planningContext?: PlanningContext runtimeContext?: RuntimeContext runtimeState?: AutomationRunState } export type AutomationRuntimeState = NonNullable< InvocationStore["runtimeState"] > export type AutomationInvocationInput = | (AutomationInvocationEnvironment & { phase: "planning" }) | RuntimeContext export interface AutomationInvocationResult { exitCode: number files: Record stderr: string stdout: string } export interface AutomationInvocationModule { default: AutomationDescriptor } const invocationStorage = new AsyncLocalStorage() let planningContextForTest: PlanningContext | undefined let runtimeStateForTest: AutomationRuntimeState | undefined /** * Runs one automation phase inside an invocation-scoped runtime context. * * Warm Lambda environments may reuse the loaded descriptor, while mutable * platform state remains isolated to this asynchronous invocation. * * @param input - Planning or authenticated runtime phase details. * @param loadAutomation - Bundled automation loader evaluated in the context. */ export async function runAutomationInvocation( input: AutomationInvocationInput, loadAutomation: () => Promise, ): Promise { try { if (input.phase === "planning") { const planningContext: PlanningContext = { accountDeclarations: [], accountUses: [], nextHookSlot: 0, parameterIssues: [], parameters: [], scopes: [], subscriptions: [], usesMarkSignificant: false, } let descriptor: AutomationDescriptor | undefined await invocationStorage.run( { invocationEnvironment: input, planningContext }, async () => { descriptor = (await loadAutomation()).default await descriptor(await decodeParameterValues(input.parameterValues)) validateActionContextBoundaries(planningContext) }, ) return { exitCode: 0, files: { "plan-result.json": JSON.stringify({ accountDeclarations: planningContext.accountDeclarations, accountUses: planningContext.accountUses, description: descriptor?.description || "No description", parameterIssues: planningContext.parameterIssues ?? [], parameters: planningContext.parameters ?? [], scopes: planningContext.scopes, subscriptions: planningContext.subscriptions, usesMarkSignificant: planningContext.usesMarkSignificant ?? false, }), }, stderr: "", stdout: "", } } await invocationStorage.run( { invocationEnvironment: input, runtimeContext: input }, async () => { await ( await loadAutomation() ).default(await decodeParameterValues(input.parameterValues)) }, ) return { exitCode: 0, files: {}, stderr: "", stdout: "" } } catch (error) { return { exitCode: 1, files: {}, stderr: formatInvocationError(error), stdout: "", } } } /** * Decodes invocation-scoped parameter assignments from transport-safe bytes. * * @param values - Base64-encoded codec values keyed by parameter name. */ async function decodeParameterValues( values: Readonly> | undefined, ): Promise> { return Object.fromEntries( await Promise.all( Object.entries(values ?? {}).map(async ([name, value]) => [ name, await decode(Buffer.from(value, "base64")), ]), ), ) } /** * Runs an automation body locally or advances one durable runtime context. * * A runtime invocation executes at most one targeted action, then keeps * replaying inside the same sandbox while terminal decisions expose more work. * * @param fn - Automation body whose hook calls should be evaluated. * @throws When a pending target action has no matching hook slot. */ export async function runAutomationFunction(fn: () => void) { const runtimeContext = invocationStorage.getStore()?.runtimeContext if (!runtimeContext) { fn() return } const client = createRuntimeClient(runtimeContext) const runtime = createActionRuntime(runtimeContext, client) for (;;) { const loaded = await decodeRuntimeLoadResult(await client.load()) const state: AutomationRunState = { actionDeferred: false, actionInvocations: [], compatibilityChecks: [], completeAction: async (completion) => await client.completeAction( completion.status === "succeeded" ? { ...completion, output: await encodeCodecEnvelope(completion.output), } : completion, ), completeActionAttempt: async (completion) => await client.completeActionAttempt( completion.status === "succeeded" ? { ...completion, output: await encodeCodecEnvelope(completion.output), } : completion, ), context: loaded.context, coordinationEvaluations: [], googleAdsRequest: client.googleAdsRequest, initiatingSubscriptionSignals: [], nextHookSlot: 0, nonInitiatingSubscriptionOrigins: new Set(), phase: "run", resolveAccountInput: client.resolveAccountInput, runtime, signalEvaluations: [], signalInvocations: [], significanceEvaluations: [], startActionAttempt: async (replaySafety) => await client.startActionAttempt({ replaySafety }), ...(loaded.targetAction?.status === "pending" && { targetAction: loaded.targetAction, }), } await invocationStorage.run( { ...invocationStorage.getStore(), runtimeState: state }, async () => { fn() await Promise.all(state.compatibilityChecks) if (state.targetAction && !state.actionExecution) { throw new Error( `No action matched hook ${formatHookLocation(state.targetAction)}.`, ) } await state.actionExecution?.() }, ) if (state.actionDeferred) return const { coordinationOffers, significant, signalInvocations } = await evaluateBoundaries(state, { loadValues: async (input) => await decodeRuntimeValueContext(await client.loadValues(input)), }) const actionInvocations = await Promise.all( state.actionInvocations.map(async (invocation) => ({ ...invocation, inputHash: await invocation.inputHash, })), ) // Terminal work changes what the same sandbox can discover on replay. const hasTerminalWork = !!state.actionExecution || signalInvocations.length > 0 || actionInvocations.some((invocation) => invocation.status === "skipped") await client.commit({ actionInvocations, coordinationOffers: await Promise.all( coordinationOffers.map(encodeCoordinationOffer), ), observedFrontier: observeRuntimeFrontier(loaded.context), significant, settled: !hasTerminalWork, signalInvocations, }) if (!hasTerminalWork) return } } /** * Returns the exact append-only frontier observed by one runtime load. * * @param context - Runtime context loaded for the current traversal. */ function observeRuntimeFrontier(context: RuntimeRunContext) { return { boundaryCount: context.actions.filter((action) => "outcomeSeq" in action).length + context.events.length + context.signalOutputs.length, outcomeSeq: Math.max( 0, ...context.events.map((event) => event.outcomeSeq), ...context.signalOutputs.map((signal) => signal.outcomeSeq), ...context.actions.flatMap((action) => "outcomeSeq" in action ? [action.outcomeSeq] : [], ), ), } } /** * Creates the bounded action capabilities backed by one runtime client. * * @param runtimeContext - Durable identity and signed email provenance. * @param client - Authenticated runtime transport used by action effects. */ export function createActionRuntime( runtimeContext: Pick< RuntimeContext, "appOrigin" | "automationId" | "contextId" > & Partial>, client: ActionRuntimeClient, ): ActionRuntime { let nextOutputIndex = 0 let nextLogIndex = 0 const runtime: ActionRuntime = { [EMAIL_ORIGIN_TOKEN]: runtimeContext.emailOriginToken, automationId: runtimeContext.automationId, contextId: runtimeContext.contextId, createCallbackUrl: async (endpoint) => { const callback = new URL(endpoint) const pathMatch = /^\/x\/([^/]+)\/?$/.exec(callback.pathname) if ( callback.origin !== new URL(runtimeContext.appOrigin).origin || callback.username || callback.password || callback.search || callback.hash || !pathMatch ) { throw new TypeError( "Callback endpoint must be an unmodified Automate.ax HTTP trigger URL.", ) } callback.searchParams.set( CALLBACK_PROTECTION_QUERY_PARAMETER, await client.createCallbackToken({ endpointKey: decodeURIComponent(pathMatch[1]!), }), ) return callback.toString() }, evaluate: async (input) => await client.evaluate(input), generate: async (input) => await decodeGenerationResult(await client.generate(input)), startImageGeneration: async (input) => await client.startImageGeneration({ ...input, prompt: await encodeCodecEnvelope(input.prompt), }), startWebScrape: async (input) => await client.startWebScrape(input), invokeAutomation: async (input) => await client.invokeAutomation({ ...input, payload: await encodeCodecEnvelope(input.payload), }), log: async ({ sensitive, ...input }) => { if (nextLogIndex >= AUTOMATION_LOG_MAX_ENTRIES) { throw new Error( `An action invocation can write at most ${AUTOMATION_LOG_MAX_ENTRIES} log entries.`, ) } const { fields, ...entry } = input await client.sendLog({ ...entry, ...(fields !== undefined && { fields: await encodeCodecEnvelope(fields), }), logIndex: nextLogIndex++, ...(sensitive && { sensitivity: sensitive }), timestamp: new Date(), }) }, sendOutput: async (output, options) => { await client.sendOutput({ output: { ...output, data: await encodeCodecEnvelope(output.data), }, outputIndex: nextOutputIndex++, ...(options && { sensitivity: options.sensitive }), }) }, sendEmail: async (input) => await client.sendEmail(input), } if (runtimeContext.apiOrigin && runtimeContext.token) { const runtimeToken = runtimeContext.token const originHeaders = { "x-automate-origin-automation-id": runtimeContext.automationId, "x-automate-origin-context-id": runtimeContext.contextId, } const rpcUrl = joinURL(runtimeContext.apiOrigin, "/api/rpc") setRuntimeAutomateClient( runtime, createAutomateClient({ headers: originHeaders, rpcUrl, runtimeToken, }), (apiKey) => createAutomateClient({ apiKey, headers: { ...originHeaders, "x-automate-runtime-token": runtimeToken, }, rpcUrl, }), ) } return runtime } /** Returns the planning state associated with the current async invocation. */ export function getAutomationPlanningContext() { return invocationStorage.getStore()?.planningContext ?? planningContextForTest } /** Returns the runtime state associated with the current async invocation. */ export function getAutomationRuntimeState() { return invocationStorage.getStore()?.runtimeState ?? runtimeStateForTest } /** Returns endpoint identity available throughout planning and execution. */ export function getAutomationInvocationEnvironment() { return invocationStorage.getStore()?.invocationEnvironment } /** * Returns the active automation and context IDs during runtime execution. * * Planning has no execution context, so this returns undefined in that phase. */ export function getAutomationExecutionIdentity(): | AutomationExecutionIdentity | undefined { const context = invocationStorage.getStore()?.runtimeContext return context ? { automationId: context.automationId, contextId: context.contextId } : undefined } /** * Returns the durable identity that the next hook will consume without * reserving it, or undefined outside an active automation traversal. */ export function getNextHookLocation(): HookLocation | undefined { const context = getAutomationRuntimeState() ?? getAutomationPlanningContext() if (!context) return const scope = HOOK_SCOPE_STACKS.get(context)?.at(-1) return { scopePath: [...(scope?.path ?? [])], slot: scope?.nextHookSlot ?? context.nextHookSlot, } } /** Returns the active nested hook namespace. */ export function getCurrentHookScopePath(): readonly number[] { const context = getAutomationRuntimeState() ?? getAutomationPlanningContext() return context ? (HOOK_SCOPE_STACKS.get(context)?.at(-1)?.path ?? []) : [] } /** * Reserves the next deterministic hook slot in the active planning or runtime * context. */ export function consumeHookSlot() { const context = getAutomationRuntimeState() ?? getAutomationPlanningContext() if (!context) return -1 const scope = HOOK_SCOPE_STACKS.get(context)?.at(-1) if (scope) return scope.nextHookSlot++ return context.nextHookSlot++ } /** Reserves a deterministic declaration slot without shifting durable hooks. */ export function consumeDeclarationSlot() { const context = getAutomationRuntimeState() ?? getAutomationPlanningContext() if (!context) return -1 const slot = DECLARATION_SLOTS.get(context) ?? 0 DECLARATION_SLOTS.set(context, slot + 1) return slot } /** Reserves the next durable hook identity in the active scope. */ export function consumeHookLocation(): HookLocation { return { scopePath: getCurrentHookScopePath(), slot: consumeHookSlot(), } } /** * Tests whether a durable record belongs to one hook location. * * @param candidate - Durable record location. * @param location - Expected hook location. */ export function isSameHookLocation( candidate: HookLocation, location: HookLocation, ) { return ( candidate.slot === location.slot && candidate.scopePath.length === location.scopePath.length && candidate.scopePath.every( (segment, index) => segment === location.scopePath[index], ) ) } /** * Formats one nested hook location for diagnostics and signal origins. * * @param location - Hook location to format. */ export function formatHookLocation(location: HookLocation) { return [...location.scopePath, location.slot].join(".") } /** * Traverses a callback in a child hook namespace reserved from its parent. * * @param fn - Synchronous declaration callback. */ export function withHookScope(fn: () => TResult): TResult { const context = getAutomationRuntimeState() ?? getAutomationPlanningContext() if (!context) return fn() const scopes = HOOK_SCOPE_STACKS.get(context) ?? [] if (scopes.length === 0) HOOK_SCOPE_STACKS.set(context, scopes) scopes.push({ nextHookSlot: 0, path: [...getCurrentHookScopePath(), consumeHookSlot()], }) try { return fn() } finally { scopes.pop() } } /** * Sets planning state for SDK tests that exercise hooks directly. * * @param planningContext - Test planning state, or undefined to clear it. */ export function setAutomationPlanningContextForTest( planningContext: PlanningContext | undefined, ) { planningContextForTest = planningContext } /** * Sets runtime state for SDK tests that exercise hooks directly. * * @param runtimeState - Test runtime state, or undefined to clear it. */ export function setAutomationRuntimeStateForTest( runtimeState: AutomationRuntimeState | undefined, ) { runtimeStateForTest = runtimeState } /** * Hydrates and evaluates every pure durable boundary discovered by one pass. * * @param state - Traversal state containing exact value demands. * @param client - Authenticated runtime transport. */ export async function evaluateBoundaries( state: AutomationRunState, client: RuntimeValueClient, ) { const valueEvaluations = [ ...state.signalEvaluations, ...state.significanceEvaluations, ...state.coordinationEvaluations.filter( (evaluation) => "evaluate" in evaluation, ), ] if ( valueEvaluations.length === 0 && state.coordinationEvaluations.length === 0 ) { return { coordinationOffers: [], significant: false, signalInvocations: state.signalInvocations, } } if (valueEvaluations.length === 0) { return { coordinationOffers: state.coordinationEvaluations.filter( (evaluation): evaluation is PlannedTerminalRaceEvaluation => evaluation.policy === "race" && !("evaluate" in evaluation), ), significant: false, signalInvocations: state.signalInvocations, } } const values = await client.loadValues({ actionDependencyIds: [ ...new Set( valueEvaluations.flatMap( (evaluation) => evaluation.actionDependencyIds, ), ), ], eventDependencyIds: [ ...new Set( valueEvaluations.flatMap((evaluation) => evaluation.eventDependencyIds), ), ], signalDependencyIds: [ ...new Set( valueEvaluations.flatMap( (evaluation) => evaluation.signalDependencyIds, ), ), ], }) // Boundary evaluation failures join ordinary terminal signal checkpoints below. const signalInvocations = state.signalInvocations.concat( state.signalEvaluations.map((evaluation) => { const dependencies = { actionDependencyIds: evaluation.actionDependencyIds, derivation: evaluation.derivation, eventDependencyIds: evaluation.eventDependencyIds, scopePath: evaluation.scopePath, signalDependencyIds: evaluation.signalDependencyIds, slot: evaluation.slot, } try { return { ...dependencies, ...evaluation.evaluate(values) } } catch (error) { return { ...dependencies, failure: normalizeSignalEvaluationFailure(error), status: "failed" as const, } } }), ) const coordinationResults = state.coordinationEvaluations.map( (evaluation) => { try { switch (evaluation.policy) { case "race": { if (!("evaluate" in evaluation)) return { offer: evaluation } const { evaluate, ...offer } = evaluation evaluate(values) return { offer } } case "correlate": { const { evaluate, ...offer } = evaluation return { offer: { ...offer, ...evaluate(values) } } } case "collect": { const { evaluate, ...offer } = evaluation return { offer: { ...offer, ...evaluate(values) } } } case "fanout": { const { evaluate, ...offer } = evaluation return { offer: { ...offer, ...evaluate(values) } } } case "funnel": { const { evaluate, ...offer } = evaluation return { offer: { ...offer, ...evaluate(values) } } } case "take": { const { evaluate, ...offer } = evaluation return { offer: { ...offer, ...evaluate(values) } } } case "rateLimit": { const { evaluate, ...offer } = evaluation return { offer: { ...offer, ...evaluate(values) } } } case "merge": { const { evaluate, ...offer } = evaluation evaluate(values) return { offer } } case "serialize": { const { evaluate, ...offer } = evaluation return { offer: { ...offer, ...evaluate(values) } } } case "concurrent": { const { evaluate, ...offer } = evaluation return { offer: { ...offer, ...evaluate(values) } } } } } catch (error) { if (evaluation.policy === "race" && "evaluate" in evaluation) { const { evaluate: _, ...offer } = evaluation return { offer: { ...offer, failure: normalizeSignalEvaluationFailure(error), status: "failed" as const, }, } } return { failure: { actionDependencyIds: evaluation.actionDependencyIds, derivation: evaluation.derivation, eventDependencyIds: evaluation.eventDependencyIds, failure: normalizeSignalEvaluationFailure(error), scopePath: evaluation.scopePath, signalDependencyIds: evaluation.signalDependencyIds, slot: evaluation.slot, status: "failed" as const, }, } } }, ) return { coordinationOffers: coordinationResults.flatMap((result) => result.offer ? [result.offer] : [], ), significant: state.significanceEvaluations.some((evaluation) => evaluation.evaluate(values), ), signalInvocations: signalInvocations.concat( coordinationResults.flatMap((result) => result.failure ? [result.failure] : [], ), ), } } /** * Creates an authenticated API client for an automation runtime. * * @param context - Runtime connection details supplied by the sandbox. */ function createRuntimeClient(context: RuntimeContext): RuntimeClient { return createORPCClient( new RPCLink({ customJsonSerializers: [codecEnvelopeSerializer], headers: { "x-api-key": context.token }, url: joinURL(context.apiOrigin, "/api/rpc"), }), { path: ["runtime"] }, ) } /** * Decodes all codec-backed values loaded for one runtime traversal. * * @param context - Transport value context. */ async function decodeRuntimeValueContext( context: RuntimeTransportValueContext, ): Promise { return { ...context, actionOutputs: await Promise.all( context.actionOutputs.map(async (outcome) => { if (outcome.status !== "succeeded") return outcome const { output, ...result } = outcome return { ...result, output: await decodeCodecEnvelope(output) } }), ), events: await Promise.all( context.events.map(async (event) => ({ ...event, payload: await decodeCodecEnvelope(event.payload), })), ), signalOutputs: await Promise.all( context.signalOutputs.map(async (outcome) => { if (outcome.status !== "succeeded") return outcome const { output, ...result } = outcome return output === undefined ? result : { ...result, output: await decodeCodecEnvelope(output) } }), ), } } /** * Decodes the optional targeted action context in one runtime load. * * @param result - Transport runtime load result. */ async function decodeRuntimeLoadResult( result: RuntimeTransportLoadResult, ): Promise { const { targetAction, ...load } = result return { ...load, ...(targetAction && { targetAction: targetAction.status === "pending" ? { ...targetAction, context: await decodeRuntimeValueContext(targetAction.context), } : targetAction, }), } } /** * Encodes the optional coordinator key before committing a boundary offer. * * @param offer - Domain coordinator offer. */ async function encodeCoordinationOffer( offer: RuntimeCoordinationOffer, ): Promise { switch (offer.policy) { case "collect": case "concurrent": case "correlate": case "funnel": case "rateLimit": case "serialize": case "take": return { ...offer, key: await encodeCodecEnvelope(offer.key) } case "fanout": case "merge": case "race": return offer } } /** * Decodes codec-backed provider values returned by platform generation. * * @param result - Transport generation result. */ async function decodeGenerationResult( result: Awaited>, ): Promise { const [output, providerMetadata, raw] = await Promise.all([ decodeCodecEnvelope(result.output), decodeCodecEnvelope(result.providerMetadata), decodeCodecEnvelope(result.usage.raw), ]) return { ...result, output, providerMetadata, usage: { ...result.usage, raw }, } } /** * Formats one failed in-process invocation like command stderr. * * @param error - Thrown automation value. */ function formatInvocationError(error: unknown) { return error instanceof Error ? (error.stack ?? error.message) : String(error) } /** * Preserves propagated signal failures while normalizing evaluator exceptions. * * @param error - Propagated signal failure or evaluator exception. */ function normalizeSignalEvaluationFailure(error: unknown): AutomationError { return error instanceof FailedSignalError ? error.failure : normalizeAutomationError(error, "signal_evaluation_failed") }