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
121 lines (120 loc) • 6.27 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/.
/**
* OpenHands CLI agent command builder (P2, plan §"The adapter contract"
* step 2 / §"Capability matrix").
*
* Translates a platform-agnostic {@link AgentDispatchRequest} into the exact
* headless argv the `openhands` CLI expects. Per the capability matrix the
* headless invocation is:
*
* openhands --headless -t "<p>" --json
*
* with `--json` producing the documented JSONL event stream (structured tier
* "via prompt+validate"), workspace-state resume (no session-id flag), and
* native MCP support.
*
* Platform-specific mapping decisions (all localized here, per the adapter
* contract):
*
* - **prompt** — `--task` is the long form of the matrix's `-t` (used for
* self-documenting argv, mirroring the aider builder's `--message`). Like
* aider — and unlike claude/codex/pi — there is NO positional prompt and NO
* `--` end-of-options separator to hide behind: the task is a *flag value*.
* The glued `--task=<payload>` form is therefore used so a payload that
* begins with `-`/`--` binds to the flag lexically and can never be parsed
* as a separate option by the CLI's argument parser.
* - **--headless / --json** — always emitted: `--headless` is what makes a
* captured, unattended run possible, and `--json` switches stdout to the
* JSONL event stream that `./result-extractor.ts` normalizes (the same
* "dispatch is the captured path" reasoning as the Claude builder's
* unconditional `--print`; both flags are the matrix's documented headless
* shape).
* - **systemPrompt** — OpenHands headless mode has no documented
* system-prompt flag (agent behaviour is shaped by its own agent configs
* and microagents). Folded into the task payload — system text first, blank
* line, then the task — mirroring the aider/codex treatment.
* - **model** — OpenHands does not take the model on the headless command
* line; its documented configuration channel is the `LLM_MODEL` environment
* variable (config.toml `[llm] model` equivalent). The alias is resolved via
* `resolveModel` with this platform id and returned on `BuiltCommand.env`
* (the seam exists precisely for platform-specific extras), NOT as an
* invented `--model` flag that would produce a silently broken command.
* - **schema** — the matrix places OpenHands in the "via prompt+validate"
* tier (plan §"Structured-output normalization", tier "native-json"): no
* Codex-style `--output-schema` flag exists, so NO temp schema file is
* written; the JSON Schema reaches it as the instruction the shared request
* lowering appends to the prompt. Downstream, the extractor pulls the
* final message out of the JSONL stream and the engine's shared
* retry-until-valid loop performs the actual validation.
* - **tools** — deliberately unconsumed. OpenHands has no per-tool allowlist
* flag; tool access is governed by its own runtime/sandbox configuration
* and (natively supported) MCP config. A restrictive policy is therefore
* dropped rather than approximated — never silently widened.
* - **resume/session** — NOT expressible: per the matrix OpenHands resumes
* from *workspace state*, not a session-id flag, so there is no flag-shaped
* resume to describe. The extractor still captures a
* conversation/session id opportunistically when the stream reveals one;
* akm's `workflow_run_units` remains the durable source of truth either way
* (plan §"Session, MCP, and identity across harnesses").
* - **inference** — not translated: the shared lowering reports each field of
* the request's inference as untranslated.
*
* Registered: `openhandsBuilder` is `OpenhandsHarness.agentBuilder`
* (`./index.ts`), one of the ten harnesses `HARNESS_REGISTRY` constructs
* (`harnesses/index.ts`); `agent/builders.ts` derives `BUILTIN_BUILDERS` from
* that registry, so this builder is reachable under the `"openhands"`
* platform name without any further wiring.
*/
import { resolveDispatchModel } from "../../agent/builder-shared.js";
import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
/** Canonical harness/platform id used for model-alias resolution. */
export const OPENHANDS_PLATFORM = "openhands";
/**
* Env var OpenHands reads its LLM model from — the platform's documented
* model-selection channel for headless runs (there is no headless model
* flag). Exported (re-exported from `./index.ts`) so this harness's tests
* share the one constant.
*/
export const OPENHANDS_MODEL_ENV = "LLM_MODEL";
/** Assemble the `--task` payload: optional system text, then the task prompt. */
function buildTaskPayload(req) {
const sections = [];
if (req.systemPrompt)
sections.push(req.systemPrompt);
sections.push(req.prompt);
return sections.join("\n\n");
}
/**
* OpenHands builder.
* Command shape:
* openhands --headless --json --task=<[system\n\n]prompt>
* with the resolved model (if any) carried on env as LLM_MODEL.
*/
export const openhandsBuilder = {
platform: OPENHANDS_PLATFORM,
personaChannel: "prompt",
lower: createAgentRequestLowerer({
adapter: OPENHANDS_PLATFORM,
personaChannel: "prompt",
tools: "none",
}),
build(profile, req) {
const args = [...profile.args];
// Headless essentials (matrix shape): non-interactive run, JSONL stdout
// for the extractor.
args.push("--headless");
args.push("--json");
// Glued form: the payload is a flag VALUE (no positional prompt exists),
// so `=` binding keeps a dash-leading payload from parsing as an option.
args.push(`--task=${buildTaskPayload(req)}`);
let env;
if (req.model) {
const resolved = resolveDispatchModel(req, profile, OPENHANDS_PLATFORM);
// Model travels via env, not argv — OpenHands' documented channel.
env = { [OPENHANDS_MODEL_ENV]: resolved };
}
return { argv: [profile.bin, ...args], ...(env ? { env } : {}) };
},
};