UNPKG

agent-contracts-runtime

Version:

Runtime bridge for executing agent-contracts workflows on Agent SDKs

307 lines (302 loc) 10.7 kB
import { buildContractContext, buildGuardrailContext, renderTemplate } from "./chunk-LTZA6QWC.js"; // src/generator/index.ts import { readFile as readFile2, writeFile, mkdir, rm } from "fs/promises"; import { resolve as resolve2, join as join2, relative } from "path"; import { createHash } from "crypto"; import YAML2 from "yaml"; // src/generator/config.ts import { existsSync } from "fs"; import { readFile } from "fs/promises"; import { resolve, dirname, join, basename } from "path"; import { z } from "zod"; import YAML from "yaml"; var DEFAULT_CONFIG_PATH = "./agents.conf.yaml"; var LEGACY_CONFIG_FILENAME = "agent-runtime.config.yaml"; var RecoverySchema = z.object({ max_follow_ups: z.number().int().min(0).default(2), max_retries: z.number().int().min(0).default(0) }).default(() => ({ max_follow_ups: 2, max_retries: 0 })); var ModelClassEntrySchema = z.object({ adapter: z.string(), model: z.string().optional() }); var ModelMappingSchema = z.object({ fast: ModelClassEntrySchema.optional(), standard: ModelClassEntrySchema.optional(), thinking: ModelClassEntrySchema.optional() }).default({}); var ProgressLogSchema = z.object({ destination: z.enum(["stderr", "file", "both", "none"]).default("stderr"), file: z.string().optional(), naming: z.enum(["single", "per-invocation", "daily"]).default("single") }).default(() => ({ destination: "stderr", naming: "single" })).refine( (val) => { if ((val.destination === "file" || val.destination === "both") && !val.file) return false; return true; }, { message: "logging.progress_log.file is required when destination is 'file' or 'both'" } ); var LoggingSchema = z.object({ progress_log: ProgressLogSchema }).default(() => ({ progress_log: { destination: "stderr", naming: "single" } })); var RuntimeConfigSchema = z.object({ dsl: z.string().default("./agent-contracts.yaml"), generated_dir: z.string().default("./agent/generated"), bindings: z.array(z.string()).default([]), active_guardrail_policy: z.string().optional(), templates_dir: z.string().optional(), plugins: z.array(z.string()).default([]), recovery: RecoverySchema, model_mapping: ModelMappingSchema, logging: LoggingSchema }); var CliSectionSchema = z.object({ dsl: z.string().optional(), generated_dir: z.string().optional(), plugins: z.array(z.string()).optional(), bindings: z.array(z.string()).optional() }).strict(); var ProjectConfigSchema = z.object({ dsl: z.string().optional(), generated_dir: z.string().optional(), bindings: z.array(z.string()).optional(), active_guardrail_policy: z.string().optional(), templates_dir: z.string().optional(), plugins: z.array(z.string()).optional(), recovery: RecoverySchema, model_mapping: ModelMappingSchema, logging: LoggingSchema, cli: z.record(z.string(), CliSectionSchema).optional() }); function resolveConfigFilePath(configPath) { const absPath = resolve(configPath); if (existsSync(absPath)) { return absPath; } const dir = dirname(absPath); const base = basename(absPath); if (base === "agents.conf.yaml") { const legacyPath = join(dir, LEGACY_CONFIG_FILENAME); if (existsSync(legacyPath)) { return legacyPath; } } return absPath; } function buildResolvedConfig(config, configDir) { const pl = config.logging.progress_log; return { ...config, configDir, dslPath: resolve(configDir, config.dsl), generatedDir: resolve(configDir, config.generated_dir), bindingPaths: config.bindings.map((b) => resolve(configDir, b)), templatesDir: config.templates_dir ? resolve(configDir, config.templates_dir) : void 0, pluginPaths: config.plugins, resolvedLogging: { progressLog: { destination: pl.destination, file: pl.file ? resolve(configDir, pl.file) : void 0, naming: pl.naming } } }; } async function loadConfig(configPath) { const absPath = resolveConfigFilePath(configPath); const raw = await readFile(absPath, "utf8"); const parsed = YAML.parse(raw); const config = RuntimeConfigSchema.parse(parsed); const configDir = dirname(absPath); return buildResolvedConfig(config, configDir); } async function loadProjectConfig(configPath, cliName) { const absPath = resolveConfigFilePath(configPath); const raw = await readFile(absPath, "utf8"); const parsed = YAML.parse(raw); const projectConfig = ProjectConfigSchema.parse(parsed); const { cli: _cli, ...baseConfig } = projectConfig; let merged = { ...baseConfig }; if (cliName && projectConfig.cli?.[cliName]) { merged = { ...merged, ...projectConfig.cli[cliName] }; } const config = RuntimeConfigSchema.parse(merged); const configDir = dirname(absPath); return buildResolvedConfig(config, configDir); } // src/generator/index.ts var DO_NOT_EDIT_HEADER = `/** * AUTO-GENERATED by agent-contracts-runtime. DO NOT EDIT. * * Regenerate via: npx agent-runtime generate */ `; async function loadYaml(filePath) { const raw = await readFile2(filePath, "utf8"); return YAML2.parse(raw); } function computeHash(data) { return createHash("sha256").update(data).digest("hex"); } function sortDeep(value) { if (Array.isArray(value)) return value.map(sortDeep); if (value !== null && typeof value === "object") { return Object.fromEntries( Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, sortDeep(v)]) ); } return value; } function canonicalStringify(obj) { return JSON.stringify(sortDeep(obj)); } async function loadDsl(dslPath) { const { resolve: resolveDsl } = await import("agent-contracts"); const result = await resolveDsl(dslPath); return result.data; } var CONTRACT_TEMPLATES = [ { template: "agents.ts.hbs", output: "agents.ts" }, { template: "tasks.ts.hbs", output: "tasks.ts" }, { template: "workflows.ts.hbs", output: "workflows.ts" }, { template: "handoffs.ts.hbs", output: "handoffs.ts" }, { template: "contracts-index.ts.hbs", output: "index.ts" } ]; async function generate(options = {}) { const configPath = options.configPath ?? "./agents.conf.yaml"; const config = await loadConfig(configPath); const templatesDir = options.templates ?? config.templatesDir; const checkMode = options.check ?? false; const cleaned = options.clean ?? false; if (cleaned && !checkMode) { try { await rm(config.generatedDir, { recursive: true, force: true }); } catch { } } const dsl = await loadDsl(config.dslPath); const dslCanonical = canonicalStringify(dsl); const dslHash = computeHash(dslCanonical); const contractCtx = buildContractContext(dsl); const files_generated = []; const generatedContents = /* @__PURE__ */ new Map(); for (const { template, output } of CONTRACT_TEMPLATES) { const rendered = DO_NOT_EDIT_HEADER + await renderTemplate(template, contractCtx, templatesDir); const outPath = join2(config.generatedDir, output); files_generated.push(relative(config.configDir, outPath)); generatedContents.set(outPath, rendered); } let guardrailRules; if (config.bindingPaths.length > 0) { for (const bindingPath of config.bindingPaths) { const binding = await loadYaml(bindingPath); const guardrailCtx = buildGuardrailContext(dsl, binding, config.active_guardrail_policy); if (guardrailCtx.hasGuardrails) { guardrailRules = { commandRules: guardrailCtx.commandChecks.map((c) => ({ guardrail_id: c.guardrail_id, pattern: c.pattern, action: c.action, message: c.message })), fileRules: guardrailCtx.fileChecks.map((c) => ({ guardrail_id: c.guardrail_id, pattern: c.pattern, action: c.action, message: c.message, ...c.exclude_glob ? { exclude_glob: c.exclude_glob } : {} })), contentRules: guardrailCtx.contentChecks.map((c) => ({ guardrail_id: c.guardrail_id, pattern: c.pattern, action: c.action, message: c.message, ...c.file_glob ? { file_glob: c.file_glob } : {}, ...c.exclude_glob ? { exclude_glob: c.exclude_glob } : {} })) }; } } } { const dslWithRules = guardrailRules ? { ...dsl, _guardrailRules: guardrailRules } : dsl; const dslDataContent = DO_NOT_EDIT_HEADER + `export const resolvedDsl: Record<string, unknown> = ${JSON.stringify(dslWithRules, null, 2)} as const; `; const outPath = join2(config.generatedDir, "dsl-data.ts"); files_generated.push(relative(config.configDir, outPath)); generatedContents.set(outPath, dslDataContent); } const generationInputParts = [dslCanonical]; for (const bp of config.bindingPaths) { try { generationInputParts.push(await readFile2(bp, "utf8")); } catch { } } const generation_input_hash = computeHash(generationInputParts.join("\n---\n")); let has_differences = false; if (checkMode) { for (const [outPath, content] of generatedContents) { try { const existing = await readFile2(outPath, "utf8"); if (existing !== content) { has_differences = true; break; } } catch { has_differences = true; break; } } } else { for (const [outPath, content] of generatedContents) { const dir = resolve2(outPath, ".."); await mkdir(dir, { recursive: true }); await writeFile(outPath, content, "utf8"); } const manifest = { generated_at: (/* @__PURE__ */ new Date()).toISOString(), dsl_hash: dslHash, generation_input_hash, files: files_generated }; const manifestPath = join2(config.generatedDir, ".manifest.json"); await mkdir(resolve2(manifestPath, ".."), { recursive: true }); await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8"); } return { files_generated, dsl_hash: dslHash, generation_input_hash, cleaned, check_mode: checkMode, has_differences }; } async function checkFreshness(configPath) { const config = await loadConfig(configPath ?? "./agents.conf.yaml"); const manifestPath = join2(config.generatedDir, ".manifest.json"); try { const raw = await readFile2(manifestPath, "utf8"); const manifest = JSON.parse(raw); const dsl = await loadDsl(config.dslPath); const dslCanonical = canonicalStringify(dsl); const currentHash = computeHash(dslCanonical); return manifest.dsl_hash === currentHash; } catch { return false; } } export { DEFAULT_CONFIG_PATH, ProjectConfigSchema, loadConfig, loadProjectConfig, generate, checkFreshness }; //# sourceMappingURL=chunk-PEHJ3J5C.js.map