import { ContractParseError, ContractValidationError } from "./errors.js"; import { normalizeIssues } from "./issues.js"; import { parseJsonObject } from "./json.js"; import type { ContractIssue, ContractOptions, ContractReplay, ContractReplayAttempt, ContractResult, RepairPromptInput } from "./types.js"; export async function generateContract(options: ContractOptions): Promise> { const maxAttempts = Math.max(1, (options.retries ?? 0) + 1); const replay: ContractReplay = { prompt: options.prompt, attempts: [], createdAt: new Date().toISOString() }; let prompt = options.prompt; let lastIssues: ContractIssue[] = []; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { options.onEvent?.({ type: "attempt", attempt, prompt }); const previous = replay.attempts.at(-1); const response = await options.model.generate(prompt, { attempt, signal: options.signal, previous }); const rawText = typeof response === "string" ? response : response.text; const replayAttempt: ContractReplayAttempt = { attempt, prompt, rawText }; replay.attempts.push(replayAttempt); let parsed: unknown; try { parsed = parseJsonObject(rawText); replayAttempt.parsed = parsed; } catch (error) { const message = error instanceof Error ? error.message : "Unknown JSON parse error."; replayAttempt.error = message; options.onEvent?.({ type: "parse_error", attempt, rawText, error: message }); prompt = buildRepairPrompt({ originalPrompt: options.prompt, lastAttempt: replayAttempt, issues: [{ path: "", message }] }, options.repairPrompt); continue; } try { const data = options.schema.parse(parsed); options.onEvent?.({ type: "success", attempt, rawText, data }); return { data, attempts: attempt, rawText, replay }; } catch (error) { const issues = normalizeIssues(error); lastIssues = issues; replayAttempt.issues = issues; options.onEvent?.({ type: "validation_error", attempt, rawText, parsed, issues }); prompt = buildRepairPrompt({ originalPrompt: options.prompt, lastAttempt: replayAttempt, issues }, options.repairPrompt); } } options.onEvent?.({ type: "failure", attempts: replay.attempts.length, replay }); const lastAttempt = replay.attempts.at(-1); if (lastAttempt?.parsed === undefined) { throw new ContractParseError(replay); } throw new ContractValidationError(replay, lastIssues); } function buildRepairPrompt( input: RepairPromptInput, customRepairPrompt: ContractOptions["repairPrompt"] ): string { if (customRepairPrompt) { return customRepairPrompt(input); } const issueText = input.issues .map((issue) => `- ${issue.path || "(root)"}: ${issue.message}`) .join("\n"); return [ input.originalPrompt, "", "The previous response failed the output contract.", "Return only corrected JSON. Do not include markdown or explanatory text.", "", "Issues:", issueText, "", "Previous response:", input.lastAttempt.rawText ?? input.lastAttempt.error ?? "" ].join("\n"); }