UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

80 lines • 3.88 kB
/** * Constraining a turn's ANSWER to a schema. See `AgentLoopDeps.outputSchema` for where this sits in * the loop and why it is a separate model call rather than a constraint on the turn's own calls. */ /** * The turn produced an answer that does not satisfy `outputSchema`, and the bounded repair attempts * did not fix it. A DEFINED outcome of asking for structured output, not a crash: it carries the * validation issues and the text that failed them, so a caller can log what the model actually said * instead of guessing from a parse error. */ export class StructuredOutputError extends Error { /** Why it failed the schema. Empty only when the text was not JSON at all. */ issues; /** The last text the model produced, verbatim. */ text; /** How many model calls were spent trying (1 = the formatting pass, no repairs). */ attempts; constructor(issues, text, attempts) { const detail = issues.length > 0 ? issues.map((issue) => issue.message).join('; ') : 'the model did not reply with JSON'; super(`Structured output invalid after ${attempts} attempt(s): ${detail}`); this.name = 'StructuredOutputError'; this.issues = issues; this.text = text; this.attempts = attempts; } } /** * Pull a JSON value out of a model reply, tolerating the code fences and lead-in prose a provider * that cannot constrain its own generation still emits. `undefined` when there is no JSON in there * at all — distinct from a JSON `null`, which is a value the schema may well accept. */ export function extractJson(text) { const candidates = [text, text.match(/\{[\s\S]*\}/)?.[0], text.match(/\[[\s\S]*\]/)?.[0]]; for (const candidate of candidates) { if (candidate === undefined) { continue; } try { return JSON.parse(candidate); } catch { /* try the next shape */ } } return undefined; } /** * Validate one attempt. The provider's own parsed `object` is preferred when it reported one (a * provider that constrained generation already did the parse), but it is validated all the same — * "the provider says it matched" is not the same claim as "it matches", and a provider that ignored * `outputSchema` entirely must fail here rather than downstream. */ export async function validateStructured(args) { const candidate = args.reported !== undefined ? args.reported : extractJson(args.text); if (candidate === undefined) { return { ok: false, issues: [] }; } const result = await args.schema['~standard'].validate(candidate); return result.issues !== undefined ? { ok: false, issues: result.issues } : { ok: true, value: result.value }; } /** * What the formatting pass is told to do. Written for a model that is being shown a finished answer * and asked to restate it — never to extend or improve it, which would make the structured result * disagree with the prose the user already read. */ export const DEFAULT_STRUCTURED_OUTPUT_INSTRUCTION = "Restate the assistant's final answer as a single JSON value matching the required schema. Use only information already present in the conversation — add nothing, and answer nothing that was not asked. Reply with ONLY the JSON: no prose, no explanation, no code fences."; /** Appends the previous attempt's validation issues, so a repair call knows what to fix. */ export function repairInstruction(args) { const detail = args.issues.length > 0 ? args.issues .map((issue) => `${(issue.path ?? []).join('.') || '(root)'}: ${issue.message}`) .join('\n') : 'the previous reply was not valid JSON'; return `${args.instruction}\n\nYour previous reply was rejected:\n${detail}`; } //# sourceMappingURL=structured-output.js.map