// ultra — interpolation resolver (Task 3). // // A closed, deliberately tiny mini-syntax: NOT a general expression evaluator // and NO `eval`. Every token is parsed against a fixed grammar; anything // outside it raises a `SpecValidationError`. See the design's // "Interpolation mini-syntax" section for the authoritative semantics. // // {args.NAME} — workflow input value // {item} — current fanout item (string verbatim; else compact JSON) // {PHASE.results} — a prior phase's results array (nulls RETAINED positionally) // {PHASE.failures} — a prior phase's typed failure array // {PHASE.results[].FIELD} — project each result's FIELD (null/missing values SKIPPED) // {PHASE.results[].FIELD[]} — flatten: concat each result's FIELD array (null elems / missing FIELDs SKIPPED) // {EXPR | where FIELD} — keep elements whose boolean FIELD is truthy (null elems / missing FIELD SKIPPED) // {EXPR | where FIELD == "VALUE"} — keep elements whose string FIELD exactly equals VALUE // {EXPR | groupBy FIELD} — group records by exact string FIELD value // // Pure module: no SDK imports. /** Raised when a template/selector falls outside the documented v1 grammar. */ export class SpecValidationError extends Error { constructor(message: string) { super(message); this.name = "SpecValidationError"; } } /** * Resolution context. This shape is the contract the phase engine (Task 6) * populates: `args` from the workflow inputs, `item` for the current fanout * element, and `results` mapping each prior phase id to its results array. * The three namespaces are segregated so a phase named `args` or `item` * cannot collide with the reserved roots. */ export interface InterpSource { phase: string; index: number; } export interface InterpContext { args?: Record; item?: unknown; itemSources?: readonly InterpSource[]; results?: Record; failures?: Record; } export interface SourcedValue { value: unknown; sources: InterpSource[]; } interface ResolvedNode extends SourcedValue { elements?: ResolvedNode[]; } const IDENT = "[A-Za-z_][A-Za-z0-9_]*"; const RE_ARGS = new RegExp(`^args\\.(${IDENT})$`); const RE_RESULTS = new RegExp(`^(${IDENT})\\.results$`); const RE_FAILURES = new RegExp(`^(${IDENT})\\.failures$`); const RE_FLATTEN = new RegExp( `^(${IDENT})\\.results\\[\\]\\.(${IDENT})\\[\\]$`, ); const RE_PROJECT = new RegExp(`^(${IDENT})\\.results\\[\\]\\.(${IDENT})$`); const JSON_STRING = String.raw`"(?:\\.|[^"\\])*"`; const RE_WHERE = new RegExp( `^where\\s+(${IDENT})(?:\\s*==\\s*(${JSON_STRING}))?$`, ); const RE_GROUP_BY = new RegExp(`^groupBy\\s+(${IDENT})$`); // A selector value (`over`/`when`/`return`/`report`) is exactly one braced token, no nesting. const RE_SINGLE_TOKEN = /^\{([^{}]*)\}$/; // Template token scanner: every `{...}` (no nesting in v1). const RE_TOKEN = /\{([^{}]*)\}/g; type Base = | { kind: "args"; name: string } | { kind: "item" } | { kind: "results"; phase: string } | { kind: "failures"; phase: string } | { kind: "flatten"; phase: string; field: string } | { kind: "project"; phase: string; field: string }; type Expr = | Base | { kind: "where"; base: Base; field: string; equals?: string } | { kind: "groupBy"; base: Base; field: string }; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function parseBase(raw: string): Base { const s = raw.trim(); if (s === "item") return { kind: "item" }; const args = s.match(RE_ARGS); if (args) return { kind: "args", name: args[1] }; const results = s.match(RE_RESULTS); if (results) return { kind: "results", phase: results[1] }; const failures = s.match(RE_FAILURES); if (failures) return { kind: "failures", phase: failures[1] }; const flatten = s.match(RE_FLATTEN); if (flatten) { return { kind: "flatten", phase: flatten[1], field: flatten[2] }; } const project = s.match(RE_PROJECT); if (project) { return { kind: "project", phase: project[1], field: project[2] }; } throw new SpecValidationError( `unknown interpolation token "{${raw}}" (outside the v1 grammar; allowed: args.NAME, item, PHASE.results, PHASE.failures, PHASE.results[].FIELD, PHASE.results[].FIELD[], and selector operators where/groupBy)`, ); } function selectorPipeIndex(input: string, start = 0): number { let quoted = false; let escaped = false; for (let index = start; index < input.length; index++) { const char = input[index]; if (quoted) { if (escaped) escaped = false; else if (char === "\\") escaped = true; else if (char === '"') quoted = false; } else if (char === '"') quoted = true; else if (char === "|") return index; } return -1; } function parseExpr(inner: string): Expr { const trimmed = inner.trim(); if (trimmed === "") { throw new SpecValidationError('empty interpolation token "{}"'); } const pipeIdx = selectorPipeIndex(trimmed); if (pipeIdx === -1) return parseBase(trimmed); const left = trimmed.slice(0, pipeIdx).trim(); const right = trimmed.slice(pipeIdx + 1).trim(); if (selectorPipeIndex(trimmed, pipeIdx + 1) !== -1) { throw new SpecValidationError( `only a single selector operator is allowed in "{${trimmed}}"`, ); } const base = parseBase(left); const groupBy = right.match(RE_GROUP_BY); if (groupBy) return { kind: "groupBy", base, field: groupBy[1] }; const filter = right.match(RE_WHERE); if (filter) { let equals: string | undefined; if (filter[2] !== undefined) { try { equals = JSON.parse(filter[2]) as string; } catch { throw new SpecValidationError( `where equality requires a valid JSON string in "{${trimmed}}"`, ); } } return { kind: "where", base, field: filter[1], ...(equals !== undefined ? { equals } : {}), }; } throw new SpecValidationError( `unsupported selector operator "${right}" in "{${trimmed}}" (use "| where FIELD", '| where FIELD == "VALUE"', or "| groupBy FIELD")`, ); } function getResults(ctx: InterpContext, phase: string): unknown[] { const results = ctx.results?.[phase]; if (results === undefined) { throw new SpecValidationError( `interpolation references unknown phase "${phase}"`, ); } if (!Array.isArray(results)) { throw new SpecValidationError(`phase "${phase}" results is not an array`); } return results; } function getFailures(ctx: InterpContext, phase: string): unknown[] { const failures = ctx.failures?.[phase]; if (failures === undefined) { throw new SpecValidationError( `interpolation references unknown phase "${phase}"`, ); } if (!Array.isArray(failures)) { throw new SpecValidationError(`phase "${phase}" failures is not an array`); } return failures; } function uniqueSources(sources: readonly InterpSource[]): InterpSource[] { const seen = new Set(); const out: InterpSource[] = []; for (const source of sources) { const key = `${source.phase}#${source.index}`; if (seen.has(key)) continue; seen.add(key); out.push(source); } return out; } function node( value: unknown, sources: readonly InterpSource[] = [], ): ResolvedNode { const ownSources = uniqueSources(sources); return { value, sources: ownSources, ...(Array.isArray(value) ? { elements: value.map((element) => node(element, ownSources)) } : {}), }; } function arrayNode(elements: ResolvedNode[]): ResolvedNode { return { value: elements.map((element) => element.value), sources: uniqueSources(elements.flatMap((element) => element.sources)), elements, }; } function resultNode(ctx: InterpContext, phase: string): ResolvedNode { return arrayNode( getResults(ctx, phase).map((value, index) => node( value, value === null || value === undefined ? [] : [{ phase, index }], ), ), ); } function resolveBaseNode(base: Base, ctx: InterpContext): ResolvedNode { switch (base.kind) { case "item": return node(ctx.item, ctx.itemSources); case "args": return node(ctx.args?.[base.name]); case "results": // Retain nulls positionally — callers depend on length. return resultNode(ctx, base.phase); case "failures": // Failure records are emitted by Ultra, not authored by the failed agent. return node(getFailures(ctx, base.phase)); case "flatten": { const out: ResolvedNode[] = []; for (const element of resultNode(ctx, base.phase).elements ?? []) { if (element.value === null || element.value === undefined) continue; if (!isRecord(element.value)) continue; const field = element.value[base.field]; // Only arrays contribute; missing / null / non-array FIELDs skipped. if (!Array.isArray(field)) continue; out.push(...field.map((value) => node(value, element.sources))); } return arrayNode(out); } case "project": { const out: ResolvedNode[] = []; for (const element of resultNode(ctx, base.phase).elements ?? []) { if (element.value === null || element.value === undefined) continue; if (!isRecord(element.value)) continue; const field = element.value[base.field]; // Missing / null FIELDs contribute no projected value. if (field !== undefined && field !== null) out.push(node(field, element.sources)); } return arrayNode(out); } } } function resolveExprNode(expr: Expr, ctx: InterpContext): ResolvedNode { if (expr.kind === "where") { const source = resolveBaseNode(expr.base, ctx); if (!Array.isArray(source.value)) { throw new SpecValidationError( `"| where ${expr.field}" requires an array on its left-hand side`, ); } const out: ResolvedNode[] = []; for (const element of source.elements ?? []) { if (element.value === null || element.value === undefined) continue; if (!isRecord(element.value)) continue; const value = element.value[expr.field]; if ( expr.equals !== undefined ? typeof value === "string" && value === expr.equals : Boolean(value) ) out.push(element); } return arrayNode(out); } if (expr.kind === "groupBy") { const source = resolveBaseNode(expr.base, ctx); if (!Array.isArray(source.value)) { throw new SpecValidationError( `"| groupBy ${expr.field}" requires an array on its left-hand side`, ); } const groups: Array<{ key: string; items: unknown[]; sources: InterpSource[]; }> = []; const byKey = new Map(); for (const element of source.elements ?? []) { if (!isRecord(element.value)) continue; const value = element.value[expr.field]; if (typeof value !== "string") continue; const group = byKey.get(value); if (group) { group.items.push(element.value); group.sources = uniqueSources([...group.sources, ...element.sources]); continue; } const created = { key: value, items: [element.value], sources: [...element.sources], }; byKey.set(value, created); groups.push(created); } return arrayNode( groups.map((group) => node({ key: group.key, items: group.items }, group.sources), ), ); } return resolveBaseNode(expr, ctx); } function resolveExpr(expr: Expr, ctx: InterpContext): unknown { return resolveExprNode(expr, ctx).value; } /** Render a resolved value into template text: strings verbatim, else compact JSON. */ function renderValue(value: unknown): string { if (typeof value === "string") return value; if (value === undefined) return ""; return JSON.stringify(value); } /** * Substitute every `{...}` token in a prompt template. Each token is resolved * against the grammar and rendered (strings verbatim, objects/arrays/scalars as * compact JSON). An out-of-grammar token throws. */ export function resolveTemplate( template: string, ctx: InterpContext, render: (value: unknown) => string = renderValue, ): string { return template.replace(RE_TOKEN, (_match, inner: string) => render(resolveExpr(parseExpr(inner), ctx)), ); } /** Resolve prompt substitutions with immediate producer metadata while leaving * ordinary workflow arguments and literal fanout items unattributed. */ export function resolveTemplateWithSources( template: string, ctx: InterpContext, render: (content: string, sources: readonly InterpSource[]) => string, ): string { return template.replace(RE_TOKEN, (_match, inner: string) => { const resolved = resolveExprNode(parseExpr(inner), ctx); return render(renderValue(resolved.value), resolved.sources); }); } /** * Resolve a single braced selector expression (`over` / `when` / `return` / * `report`) to its raw * value. The whole input must be exactly one `{...}` token. */ export function resolveSelector(selector: string, ctx: InterpContext): unknown { const match = selector.trim().match(RE_SINGLE_TOKEN); if (!match) { throw new SpecValidationError( `selector must be a single brace-wrapped token, got "${selector}"`, ); } return resolveExpr(parseExpr(match[1]), ctx); } /** * Resolve a `fanout` `over`: an array literal is returned as-is; a string is an * interpolation expression that must resolve to an array. */ export function resolveOver(over: unknown, ctx: InterpContext): unknown[] { return resolveOverWithSources(over, ctx).map((item) => item.value); } /** Resolve fanout items together with immediate producer metadata. */ export function resolveOverWithSources( over: unknown, ctx: InterpContext, ): SourcedValue[] { if (Array.isArray(over)) return over.map((value) => ({ value, sources: [] })); if (typeof over === "string") { const match = over.trim().match(RE_SINGLE_TOKEN); if (!match) { throw new SpecValidationError( `selector must be a single brace-wrapped token, got "${over}"`, ); } const resolved = resolveExprNode(parseExpr(match[1]), ctx); if (!Array.isArray(resolved.value)) { throw new SpecValidationError( `"over" interpolation "${over}" did not resolve to an array`, ); } return (resolved.elements ?? []).map(({ value, sources }) => ({ value, sources, })); } throw new SpecValidationError( `"over" must be an array literal or an interpolation string, got ${typeof over}`, ); }