UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

80 lines (79 loc) • 4.17 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * `runStructured<T>()` — transport-free structured-output core (P0.5 seam). * * akm has two structured-output paths that share no code: the LLM HTTP path * (`llm/structured-call.ts` + `responseSchema`) and the agent JSON path * (`agent/spawn.ts` `parseOutput: "json"` — embedded-JSON scan, no schema, * no retry). This module is the ONE validation-driven retry loop both are * meant to converge on: the workflow engine's schema units compose it with a * dispatch adapter per runner (llm = `chatCompletion` + `responseSchema`; * agent/sdk = `runAgent`/`runOpencodeSdk` + prompt-injected schema; * native-schema CLIs pass the schema through and still validate). * * Layering: this file lives in `core/` — NOT `llm/` — because the agent path * must be able to reach it and `agent/ ⇏ llm/` is an enforced invariant * (`tests/integration/architecture/agent-no-llm-sdk-guard.test.ts`). The transport is * always injected; this module never performs IO of its own. * * Retry semantics: only parse/validation misses are retried (with corrective * feedback appended to the re-dispatch). Transport errors PROPAGATE — the * transports own their own retry discipline (`chatCompletion` has a bounded * jittered retry; `runAgent` has timeout/abort semantics). */ import { parseEmbeddedJsonResponse } from "./parse.js"; /** * Append the one structured-output instruction to a prompt. Agent lowering * appends it to every schema-bearing request, alongside a harness's native * channel where one exists (codex `--output-schema`); the workflow engine * appends it for a direct-LLM unit. Resumed workflow runs depend on these * exact bytes, so do not reword it. */ export function withSchemaInstruction(prompt, schema) { return `${prompt}\n\nRespond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(schema)}`; } export function defaultFeedback(failure) { if (failure.reason === "parse_error") { return "Your previous response contained no parseable JSON. Respond with ONLY a JSON value that matches the requested schema — no prose, no code fences."; } return `Your previous JSON response failed validation:\n- ${failure.errors.join("\n- ")}\nRespond again with ONLY a corrected JSON value.`; } /** * Run the dispatch→parse→validate loop, re-dispatching with corrective * feedback on parse/validation misses, up to `maxAttempts` total attempts. * Never throws for structure problems (returns a typed failure); transport * throws propagate untouched. */ export async function runStructured(options) { const parse = options.parse ?? ((raw) => parseEmbeddedJsonResponse(raw)); const buildFeedback = options.buildFeedback ?? defaultFeedback; const maxAttempts = Math.max(1, options.maxAttempts ?? 2); let feedback; let lastFailure; for (let attempt = 1; attempt <= maxAttempts; attempt++) { const raw = await options.dispatch(feedback); const candidate = parse(raw); if (candidate === undefined) { lastFailure = { ok: false, reason: "parse_error", errors: ["no JSON structure found in response"], attempts: attempt, raw, }; feedback = buildFeedback({ reason: "parse_error", errors: lastFailure.errors, raw }); continue; } const verdict = options.validate(candidate); if (verdict.ok) { return { ok: true, value: verdict.value, attempts: attempt }; } lastFailure = { ok: false, reason: "validation_error", errors: verdict.errors, attempts: attempt, raw }; feedback = buildFeedback({ reason: "validation_error", errors: verdict.errors, raw }); } // maxAttempts >= 1 guarantees at least one loop iteration set lastFailure // (a success would have returned), so the non-null assertion is safe. return lastFailure; }