export type PlayPrimitive = string | number | boolean | null; export type PlayValueTemplate = | PlayPrimitive | { $ref: string } | { [key: string]: PlayValueTemplate } | PlayValueTemplate[]; export type SerializedPlayJavascript = { kind: 'serialized_javascript'; source: string; displayName?: string; }; export type PlayJavascriptExecutorContext = { row: Record; steps: Record; input: Record; index: number; }; export type PlayJavascriptExecutor = ( context: PlayJavascriptExecutorContext, ) => TResult | Promise; export type PlayStructuredStep = | { type: 'tool'; alias: string; toolId: string; input: Record; description?: string; } | { type: 'waterfall'; alias: string; tool: string; input: Record; providers?: string[]; description?: string; } | { type: 'run_javascript'; alias: string; execute: SerializedPlayJavascript; input?: Record; description?: string; displayAs?: 'run_javascript'; }; export type PlayStructuredDefinition = { version: 1; map: { key: string; steps: PlayStructuredStep[]; }; result?: { mode?: 'rows'; }; }; export function serializeExecutableJs( execute: PlayJavascriptExecutor, displayName?: string, ): SerializedPlayJavascript { const source = execute.toString().trim(); if (!source) { throw new Error('Executable JavaScript function source must be non-empty.'); } return { kind: 'serialized_javascript', source, displayName, }; } export function runJavascriptStep(input: { alias: string; execute: PlayJavascriptExecutor; description?: string; input?: Record; }): Extract { return { type: 'run_javascript', alias: input.alias, execute: serializeExecutableJs(input.execute, input.alias), ...(input.description ? { description: input.description } : {}), ...(input.input ? { input: input.input } : {}), displayAs: 'run_javascript', }; } export function definePlayDefinition( definition: PlayStructuredDefinition, ): PlayStructuredDefinition { return definition; } function formatTemplate(template: PlayValueTemplate): string { if ( template === null || typeof template === 'string' || typeof template === 'number' || typeof template === 'boolean' ) { return JSON.stringify(template); } if (Array.isArray(template)) { return `[${template.map((value) => formatTemplate(value)).join(', ')}]`; } if ('$ref' in template && typeof template.$ref === 'string') { return `ref(${JSON.stringify(template.$ref)})`; } return `{ ${Object.entries(template) .map(([key, value]) => `${JSON.stringify(key)}: ${formatTemplate(value)}`) .join(', ')} }`; } export function renderPlayDefinitionSource( definition: PlayStructuredDefinition, ): string { const lines: string[] = []; lines.push('definePlayDefinition({'); lines.push(' version: 1,'); lines.push(' map: {'); lines.push(` key: ${JSON.stringify(definition.map.key)},`); lines.push(' steps: ['); for (const step of definition.map.steps) { if (step.type === 'tool') { lines.push(' {'); lines.push(' type: "tool",'); lines.push(` alias: ${JSON.stringify(step.alias)},`); lines.push(` toolId: ${JSON.stringify(step.toolId)},`); lines.push(` input: ${formatTemplate(step.input)},`); if (step.description) { lines.push(` description: ${JSON.stringify(step.description)},`); } lines.push(' },'); continue; } if (step.type === 'waterfall') { lines.push(' {'); lines.push(' type: "waterfall",'); lines.push(` alias: ${JSON.stringify(step.alias)},`); lines.push(` tool: ${JSON.stringify(step.tool)},`); lines.push(` input: ${formatTemplate(step.input)},`); if (step.providers?.length) { lines.push(` providers: ${JSON.stringify(step.providers)},`); } if (step.description) { lines.push(` description: ${JSON.stringify(step.description)},`); } lines.push(' },'); continue; } lines.push(` runJavascriptStep({ alias: ${JSON.stringify(step.alias)}, execute: ${step.execute.source}, ${step.input ? `input: ${formatTemplate(step.input)},` : ''} }),`); } lines.push(' ],'); lines.push(' },'); lines.push('});'); return lines.join('\n'); } export function validatePlayStructuredDefinition(definition: unknown): { valid: boolean; errors: string[]; } { const errors: string[] = []; if (!definition || typeof definition !== 'object') { return { valid: false, errors: ['Definition must be an object.'] }; } const candidate = definition as Partial; if (candidate.version !== 1) { errors.push('Definition version must be 1.'); } if (typeof candidate.map?.key !== 'string' || !candidate.map.key.trim()) { errors.push('Definition map.key must be a non-empty string.'); } if ( !Array.isArray(candidate.map?.steps) || candidate.map.steps.length === 0 ) { errors.push('Definition must contain at least one step.'); } for (const [index, step] of (candidate.map?.steps ?? []).entries()) { if (!step || typeof step !== 'object') { errors.push(`Step ${index} must be an object.`); continue; } const typedStep = step as PlayStructuredStep; if (!typedStep.alias?.trim()) { errors.push(`Step ${index} requires a non-empty alias.`); } if (typedStep.type === 'tool' && !typedStep.toolId?.trim()) { errors.push(`Tool step ${typedStep.alias || index} requires toolId.`); } if (typedStep.type === 'waterfall') { errors.push( `Waterfall step ${typedStep.alias || index} is no longer supported. Use explicit tool steps or a steps(...) program.`, ); } if (typedStep.type === 'run_javascript') { if (!typedStep.execute?.source?.trim()) { errors.push( `run_javascript step ${typedStep.alias || index} requires executable source.`, ); continue; } try { new Function(`return (${typedStep.execute.source});`); } catch (error) { errors.push( `run_javascript step ${typedStep.alias || index} parse error: ${ error instanceof Error ? error.message : String(error) }`, ); } } } return { valid: errors.length === 0, errors }; } function getPathValue(value: unknown, path: string): unknown { if (!path) return value; const normalized = path.replace(/^\$?/, ''); const parts = normalized.split('.').filter(Boolean); let current = value; for (const part of parts) { if (current == null || typeof current !== 'object') return undefined; current = (current as Record)[part]; } return current; } export function resolvePlayValueTemplate( template: PlayValueTemplate, context: PlayJavascriptExecutorContext, ): unknown { if ( template === null || typeof template === 'string' || typeof template === 'number' || typeof template === 'boolean' ) { return template; } if (Array.isArray(template)) { return template.map((item) => resolvePlayValueTemplate(item, context)); } if ('$ref' in template && typeof template.$ref === 'string') { const [root, ...rest] = template.$ref.split('.'); const path = rest.join('.'); if (root === 'row') return getPathValue(context.row, path); if (root === 'steps') return getPathValue(context.steps, path); if (root === 'input') return getPathValue(context.input, path); if (root === 'index') return context.index; return undefined; } return Object.fromEntries( Object.entries(template).map(([key, value]) => [ key, resolvePlayValueTemplate(value, context), ]), ); }