contract-kit / src /contract.ts
edgemindroboticslabs's picture
Initial contract-kit library
6dec997 verified
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<T>(options: ContractOptions<T>): Promise<ContractResult<T>> {
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<unknown>["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");
}