agent-contracts-runtime
Version:
Runtime bridge for executing agent-contracts workflows on Agent SDKs
307 lines (302 loc) • 10.7 kB
JavaScript
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