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
JavaScript
// 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;
}