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
111 lines (110 loc) • 4.89 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/.
/**
* OpenAI Codex CLI result extractor (P2, plan §"The adapter contract" step 3 /
* §"Structured-output normalization").
*
* Normalizes the raw {@link AgentRunResult} of a `codex exec --json` run into
* `{ text, sessionId? }` ({@link AgentResultExtraction}). Schema validation and
* the retry-until-valid loop stay in the engine, shared across harnesses —
* this module only strips transport framing.
*
* `--json` emits ONE JSON event per stdout line. Two documented event dialects
* exist across codex versions; the adapter contract localizes that churn here,
* so both are handled:
*
* Legacy protocol (envelope with an `msg` object):
* {"id":"0","msg":{"type":"session_configured","session_id":"<uuid>", ...}}
* {"id":"1","msg":{"type":"agent_message","message":"<text>"}}
* {"id":"1","msg":{"type":"task_complete","last_agent_message":"<text>"}}
*
* Newer experimental-JSON protocol (flat `type` field):
* {"type":"thread.started","thread_id":"<id>"}
* {"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"<text>"}}
* {"type":"turn.completed","usage":{...}}
*
* Extraction rules:
* - text: `task_complete.last_agent_message` wins when present (it is the
* harness's own "final answer" designation); otherwise the LAST
* agent-message event seen; otherwise the trimmed raw stdout (plain-text
* fallback for runs without `--json`, or unrecognized formats — the
* engine's `parseEmbeddedJsonResponse` tier still gets a fair input).
* - sessionId: `session_configured.session_id` / `thread.started.thread_id`,
* falling back to any sessionId the spawn layer already attached. Stored
* opportunistically on the unit row for `codex exec resume <id>`; akm
* never depends on it (plan §"Session, MCP, and identity").
*
* Non-event JSON lines (e.g. a bare JSON answer printed without framing) are
* ignored by the event scan and land in the raw-stdout fallback untouched.
*
* Registered: `codexResultExtractor` is `CodexHarness.resultExtractor`
* (`./index.ts`); the engine resolves it via `getHarness()` in
* `workflows/exec/native-executor.ts` to normalize agent-unit stdout before
* schema validation.
*/
import { asNonEmptyString, isRecord } from "../../../core/common.js";
/** Interpret one parsed JSONL event in either codex event dialect. */
function interpretEvent(event) {
// Legacy protocol: {"id":..., "msg":{"type": ...}}
const msg = event.msg;
if (isRecord(msg)) {
switch (msg.type) {
case "session_configured":
return { sessionId: asNonEmptyString(msg.session_id) };
case "agent_message":
return { agentMessage: asNonEmptyString(msg.message) };
case "task_complete":
return { finalMessage: asNonEmptyString(msg.last_agent_message) };
default:
return {};
}
}
// Newer protocol: flat {"type": "thread.started" | "item.completed" | ...}
switch (event.type) {
case "thread.started":
return { sessionId: asNonEmptyString(event.thread_id) };
case "item.completed": {
const item = event.item;
if (isRecord(item) && item.type === "agent_message") {
return { agentMessage: asNonEmptyString(item.text) };
}
return {};
}
default:
return {};
}
}
/**
* Codex result extractor: JSONL event stream (either dialect) → final agent
* message + opportunistic session id, with a plain-stdout fallback.
*/
export const codexResultExtractor = (result) => {
let sessionId = result.sessionId;
let lastAgentMessage;
let finalMessage;
for (const line of result.stdout.split("\n")) {
const trimmed = line.trim();
// Every codex event line is a JSON object; skip banner/noise lines cheaply.
if (!trimmed.startsWith("{"))
continue;
let parsed;
try {
parsed = JSON.parse(trimmed);
}
catch {
continue; // not a complete JSON line (e.g. wrapped text) — ignore
}
if (!isRecord(parsed))
continue;
const contribution = interpretEvent(parsed);
if (contribution.sessionId)
sessionId = contribution.sessionId;
if (contribution.agentMessage)
lastAgentMessage = contribution.agentMessage;
if (contribution.finalMessage)
finalMessage = contribution.finalMessage;
}
const text = finalMessage ?? lastAgentMessage ?? result.stdout.trim();
return sessionId === undefined ? { text } : { text, sessionId };
};