import fs from 'fs/promises';
import path from 'path';
import yaml from 'js-yaml';
import { z } from 'zod';
/*
Flow configuration parser for Decision Flow Orchestration.
Supported input formats: YAML and JSON.
Basic schema:
{
flows: {
<flowName>: {
steps: [ { id?, name?, model?, tool?, prompt?, system_prompt?, condition?, memory? } ]
}
}
}
This module exposes helpers to parse text or files and returns a validated shape.
*/
const MemoryActionSchema = z.object({
type: z.literal('store').optional(),
key: z.string().optional(),
from: z.string().optional(),
});
const FlowStepSchema = z
.object({
id: z.string().optional(),
name: z.string().optional(),
model: z.string().optional(),
tool: z.string().optional(),
prompt: z.string().optional(),
system_prompt: z.string().optional(),
condition: z.string().optional(),
memory: z.array(MemoryActionSchema).optional(),
})
.passthrough();
const FlowSchema = z
.object({
steps: z.array(FlowStepSchema).min(1),
})
.passthrough();
const TopSchema = z
.object({
flows: z.record(FlowSchema),
})
.passthrough();
export type MemoryAction = z.infer<typeof MemoryActionSchema>;
export type FlowStep = z.infer<typeof FlowStepSchema>;
export type FlowDefinition = z.infer<typeof FlowSchema>;
export type FlowConfig = z.infer<typeof TopSchema>;
export function parseFlowConfigText(text: string, format?: 'yaml' | 'json'): FlowConfig {
let raw: unknown;
try {
if (format === 'json') {
raw = JSON.parse(text);
} else if (format === 'yaml') {
raw = yaml.load(text);
} else {
// try to auto-detect: JSON if it starts with { or [, otherwise YAML
const trimmed = text.trimLeft();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
raw = JSON.parse(text);
} else {
raw = yaml.load(text);
}
}
} catch (_err) {
const err = _err;
throw new Error(`Failed to parse config (${format ?? 'auto'}): ${(err as Error).message}`);
}
try {
const parsed = TopSchema.parse(raw);
return parsed;
} catch (_err) {
const err = _err;
if (err instanceof z.ZodError) {
throw new Error(`Flow config validation error: ${err.message}`);
}
throw err;
}
}
export async function parseFlowConfigFile(filePath: string): Promise<FlowConfig> {
const data = await fs.readFile(filePath, 'utf-8');
const ext = path.extname(filePath).toLowerCase();
if (ext === '.json') return parseFlowConfigText(data, 'json');
return parseFlowConfigText(data, 'yaml');
}
export function tryParseFlowConfigText(
text: string,
format?: 'yaml' | 'json'
): { success: true; config: FlowConfig } | { success: false; error: string } {
try {
const cfg = parseFlowConfigText(text, format);
return { success: true, config: cfg };
} catch (_e) {
return { success: false, error: _e instanceof Error ? _e.message : String(_e) };
}
}
export default { parseFlowConfigText, parseFlowConfigFile, tryParseFlowConfigText };