New @boocode/ion package (v0.0.1) for inference optimization network. .codesight/ wiki artifacts for codebase documentation. .omo/ work plans for openspec cleanup and enhanced file panel.
122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
/**
|
|
* Node output reference resolution for the Ion workflow engine.
|
|
*
|
|
* Resolves `$nodeId.field` references in workflow conditions and prompts,
|
|
* with strict schema-aware validation and descriptive errors.
|
|
*/
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Output reference result
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type OutputRefKind = 'value' | 'empty';
|
|
|
|
export interface OutputRefResult {
|
|
/** Whether the field had a value or was empty. */
|
|
kind: OutputRefKind;
|
|
/** The resolved value (empty string for missing optional fields). */
|
|
value: string;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// OutputRefError
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export class OutputRefError extends Error {
|
|
public readonly nodeId: string;
|
|
public readonly field: string;
|
|
|
|
constructor(nodeId: string, field: string, message: string) {
|
|
super(`Output reference error for node "${nodeId}".${field}: ${message}`);
|
|
this.name = 'OutputRefError';
|
|
this.nodeId = nodeId;
|
|
this.field = field;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Schema helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Extract declared field names from an output_format schema.
|
|
*
|
|
* The output_format can be:
|
|
* - A JSON Schema object with `properties` (standard)
|
|
* - A string describing the format (treated as having no declared fields)
|
|
* - Undefined (no schema)
|
|
*
|
|
* Returns a Set of field names that are declared in the schema.
|
|
*/
|
|
export function declaredFieldsFromSchema(
|
|
outputFormat: Record<string, unknown> | string | undefined,
|
|
): Set<string> {
|
|
if (!outputFormat || typeof outputFormat === 'string') {
|
|
return new Set();
|
|
}
|
|
|
|
const properties = outputFormat['properties'];
|
|
if (properties && typeof properties === 'object' && properties !== null) {
|
|
return new Set(Object.keys(properties as Record<string, unknown>));
|
|
}
|
|
|
|
return new Set();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Node output resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Resolve a specific field from a node's output.
|
|
*
|
|
* Behavior:
|
|
* - If the field is present in the output, returns `{ kind: 'value', value }`.
|
|
* - If the field is declared in the schema but not present in the output,
|
|
* returns `{ kind: 'empty', value: '' }` (optional field not set).
|
|
* - If the field is NOT declared in the schema AND not in the output,
|
|
* throws `OutputRefError` (undeclared reference).
|
|
* - If the field is NOT declared in the schema but IS in the output,
|
|
* returns `{ kind: 'value', value }` (dynamic output).
|
|
*
|
|
* The `declaredFields` parameter should come from `declaredFieldsFromSchema()`.
|
|
*/
|
|
export function resolveNodeOutputField(
|
|
nodeOutput: Record<string, unknown>,
|
|
nodeId: string,
|
|
field: string,
|
|
declaredFields?: Set<string>,
|
|
): OutputRefResult {
|
|
// Check if the field exists in the output.
|
|
if (field in nodeOutput) {
|
|
const rawValue = nodeOutput[field];
|
|
|
|
// Convert the value to a string.
|
|
if (rawValue === null || rawValue === undefined) {
|
|
// Field key exists but value is nullish — treat as empty.
|
|
return { kind: 'empty', value: '' };
|
|
}
|
|
|
|
if (typeof rawValue === 'string') {
|
|
return { kind: 'value', value: rawValue };
|
|
}
|
|
|
|
// Non-string values are JSON-serialized.
|
|
return { kind: 'value', value: JSON.stringify(rawValue) };
|
|
}
|
|
|
|
// Field is not in the output. Check if it was declared in the schema.
|
|
const isDeclared = declaredFields?.has(field) ?? false;
|
|
|
|
if (isDeclared) {
|
|
// Declared but not present — optional field not set.
|
|
return { kind: 'empty', value: '' };
|
|
}
|
|
|
|
// Not declared and not present — this is an error.
|
|
throw new OutputRefError(
|
|
nodeId,
|
|
field,
|
|
`Field "${field}" is not declared in the output schema and is not present in the node output. Available fields: ${Object.keys(nodeOutput).join(', ') || '(none)'}`,
|
|
);
|
|
} |