import type { ChainConfig, ChainStepConfig } from "./agents.ts"; import { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./identity.ts"; import { parseFrontmatter } from "./frontmatter.ts"; function parseStepBody(agent: string, sectionBody: string): ChainStepConfig { const lines = sectionBody.split("\n"); const blankIndex = lines.findIndex((line) => line.trim() === ""); const configLines = blankIndex === -1 ? lines : lines.slice(0, blankIndex); const task = (blankIndex === -1 ? "" : lines.slice(blankIndex + 1).join("\n")).trim(); const step: ChainStepConfig = { agent, task }; for (const line of configLines) { const match = line.match(/^([\w-]+):\s*(.*)$/); if (!match) continue; const key = match[1].trim().toLowerCase(); const rawValue = match[2].trim(); if (key === "output") { if (rawValue === "false") step.output = false; else if (rawValue) step.output = rawValue; continue; } if (key === "outputmode") { if (rawValue === "inline" || rawValue === "file-only") step.outputMode = rawValue; continue; } if (key === "reads") { if (rawValue === "false") { step.reads = false; } else { const reads = rawValue .split(",") .map((v) => v.trim()) .filter(Boolean); step.reads = reads.length > 0 ? reads : false; } continue; } if (key === "model") { if (rawValue) step.model = rawValue; continue; } if (key === "skills") { if (rawValue === "false") { step.skills = false; } else { const skills = rawValue .split(",") .map((v) => v.trim()) .filter(Boolean); step.skills = skills.length > 0 ? skills : false; } continue; } if (key === "progress") { if (rawValue === "true") step.progress = true; else if (rawValue === "false") step.progress = false; } } return step; } export function parseChain(content: string, source: "user" | "project", filePath: string): ChainConfig { const { frontmatter, body } = parseFrontmatter(content); if (!frontmatter.name || !frontmatter.description) { throw new Error("Chain frontmatter must include name and description"); } const matches = [...body.matchAll(/^##\s+(.+)[^\S\n]*$/gm)]; const steps: ChainStepConfig[] = []; for (let i = 0; i < matches.length; i++) { const match = matches[i]!; const agent = match[1]!.trim(); const lineEndOffset = body[match.index! + match[0].length] === "\n" ? 1 : 0; const sectionStart = match.index! + match[0].length + lineEndOffset; const sectionEnd = i + 1 < matches.length ? matches[i + 1]!.index! : body.length; const sectionBody = body.slice(sectionStart, sectionEnd).trimEnd(); steps.push(parseStepBody(agent, sectionBody)); } const localName = frontmatter.name; const parsedPackage = parsePackageName(frontmatter.package, `Chain '${localName}' package`); if (parsedPackage.error) throw new Error(parsedPackage.error); const packageName = parsedPackage.packageName; const extraFields: Record = {}; for (const [key, value] of Object.entries(frontmatter)) { if (key === "name" || key === "package" || key === "description") continue; extraFields[key] = value; } return { name: buildRuntimeName(localName, packageName), localName, packageName, description: frontmatter.description, source, filePath, steps, extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined, }; } export function serializeChain(config: ChainConfig): string { const lines: string[] = []; lines.push("---"); lines.push(`name: ${frontmatterNameForConfig(config)}`); if (config.packageName) lines.push(`package: ${config.packageName}`); lines.push(`description: ${config.description}`); if (config.extraFields) { for (const [key, value] of Object.entries(config.extraFields)) { lines.push(`${key}: ${value}`); } } lines.push("---"); lines.push(""); for (let i = 0; i < config.steps.length; i++) { const step = config.steps[i]!; lines.push(`## ${step.agent}`); if (step.output === false) lines.push("output: false"); else if (step.output) lines.push(`output: ${step.output}`); if (step.outputMode) lines.push(`outputMode: ${step.outputMode}`); if (step.reads === false) lines.push("reads: false"); else if (Array.isArray(step.reads) && step.reads.length > 0) lines.push(`reads: ${step.reads.join(", ")}`); if (step.model) lines.push(`model: ${step.model}`); if (step.skills === false) lines.push("skills: false"); else if (Array.isArray(step.skills) && step.skills.length > 0) lines.push(`skills: ${step.skills.join(", ")}`); if (step.progress !== undefined) lines.push(`progress: ${step.progress ? "true" : "false"}`); lines.push(""); lines.push(step.task ?? ""); if (i < config.steps.length - 1) lines.push(""); } return `${lines.join("\n")}\n`; }