akm-cli 0.9.24 → 0.9.25-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +294 -0
- package/dist/commands/health/checks.js +10 -11
- package/dist/commands/improve/consolidate/pair-pass.js +3 -2
- package/dist/commands/improve/consolidate.js +3 -2
- package/dist/commands/improve/execution.js +4 -10
- package/dist/commands/improve/extract-prompt.js +4 -4
- package/dist/commands/improve/extract.js +10 -13
- package/dist/commands/improve/improve-cli.js +32 -33
- package/dist/commands/improve/improve-strategies.js +49 -43
- package/dist/commands/improve/improve-usage-report.js +8 -17
- package/dist/commands/improve/preparation.js +3 -1
- package/dist/commands/improve/reflect.js +108 -172
- package/dist/commands/improve/stage.js +69 -18
- package/dist/commands/proposal/drain.js +14 -2
- package/dist/commands/proposal/proposal-cli.js +1 -5
- package/dist/commands/proposal/propose-cli.js +2 -2
- package/dist/commands/proposal/propose.js +80 -83
- package/dist/commands/remember.js +3 -3
- package/dist/commands/sources/schema-repair.js +1 -1
- package/dist/core/config/config-schema.js +37 -60
- package/dist/core/config/engine-semantics.js +15 -11
- package/dist/core/config/schema/engines.js +33 -15
- package/dist/core/config/schema/improve-processes.js +2 -2
- package/dist/core/improve-result.js +3 -3
- package/dist/core/structured.js +10 -0
- package/dist/execution/source.js +14 -0
- package/dist/indexer/passes/memory-inference.js +2 -1
- package/dist/integrations/agent/builder-shared.js +15 -0
- package/dist/integrations/agent/config.js +2 -0
- package/dist/integrations/agent/engine-resolution.js +16 -31
- package/dist/integrations/agent/execution.js +46 -21
- package/dist/integrations/agent/index.js +1 -1
- package/dist/integrations/agent/model-map.js +16 -15
- package/dist/integrations/agent/prompts.js +53 -76
- package/dist/integrations/agent/request-lowering.js +20 -9
- package/dist/integrations/agent/runner-dispatch.js +103 -4
- package/dist/integrations/agent/runner.js +8 -2
- package/dist/integrations/harnesses/aider/agent-builder.js +11 -23
- package/dist/integrations/harnesses/aider/index.js +0 -5
- package/dist/integrations/harnesses/amazonq/agent-builder.js +10 -21
- package/dist/integrations/harnesses/amazonq/index.js +0 -5
- package/dist/integrations/harnesses/claude/agent-builder.js +61 -38
- package/dist/integrations/harnesses/claude/index.js +0 -14
- package/dist/integrations/harnesses/codex/agent-builder.js +4 -4
- package/dist/integrations/harnesses/codex/index.js +0 -4
- package/dist/integrations/harnesses/copilot/agent-builder.js +4 -13
- package/dist/integrations/harnesses/copilot/index.js +2 -7
- package/dist/integrations/harnesses/gemini/agent-builder.js +4 -13
- package/dist/integrations/harnesses/gemini/index.js +0 -5
- package/dist/integrations/harnesses/ids.js +18 -10
- package/dist/integrations/harnesses/opencode/agent-builder.js +52 -10
- package/dist/integrations/harnesses/opencode/index.js +0 -8
- package/dist/integrations/harnesses/opencode/model-config.js +80 -0
- package/dist/integrations/harnesses/opencode/model-work-agent.js +80 -0
- package/dist/integrations/harnesses/opencode-sdk/harness.js +6 -7
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +168 -24
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -21
- package/dist/integrations/harnesses/openhands/index.js +0 -5
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -21
- package/dist/integrations/harnesses/pi/index.js +0 -5
- package/dist/llm/client.js +5 -0
- package/dist/llm/feature-gate.js +10 -4
- package/dist/llm/index-passes.js +3 -6
- package/dist/llm/memory-infer.js +6 -5
- package/dist/llm/structured-call.js +33 -11
- package/dist/scripts/akm-migrate-node.js +298 -204
- package/dist/scripts/akm-migrate.js +298 -204
- package/dist/workflows/exec/step-work.js +6 -5
- package/dist/workflows/freeze/step-values.js +1 -1
- package/docs/reference/cli.md +29 -11
- package/docs/reference/configuration.md +168 -11
- package/docs/reference/workflow-schema.md +13 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +36 -0
|
@@ -30,11 +30,6 @@ export class AmazonqHarness extends BaseHarness {
|
|
|
30
30
|
agentBuilder = amazonqBuilder;
|
|
31
31
|
resultExtractor = amazonqResultExtractor;
|
|
32
32
|
// ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
|
|
33
|
-
// akm spawns `q chat` locally per unit ⇒ local-runner.
|
|
34
|
-
pattern = "local-runner";
|
|
35
|
-
// No documented structured output: akm injects the schema into the prompt
|
|
36
|
-
// and extracts embedded JSON from plain-text stdout.
|
|
37
|
-
structuredOutput = "none";
|
|
38
33
|
// No `identityEnv`: the matrix lists Q's identity markers as uncertain, and
|
|
39
34
|
// Q stamps no session var onto child processes.
|
|
40
35
|
capabilities = caps({
|
|
@@ -11,51 +11,64 @@
|
|
|
11
11
|
* in `agent/builders.ts`, which imports this builder back into
|
|
12
12
|
* `BUILTIN_BUILDERS`.
|
|
13
13
|
*
|
|
14
|
-
* ## Structured output
|
|
14
|
+
* ## Structured output
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* `native-json` to match this builder honestly.)
|
|
16
|
+
* For a schema-bearing request this builder emits `--output-format json`,
|
|
17
|
+
* which wraps the run in a RESULT ENVELOPE
|
|
18
|
+
* (`{"type":"result","result":"<final answer>","session_id":"…", …}`); the
|
|
19
|
+
* shared request lowering has already appended the schema instruction to the
|
|
20
|
+
* prompt. The envelope is unwrapped by `./result-extractor.ts`, and the
|
|
21
|
+
* engine's shared `runStructured` retry-until-valid loop validates the
|
|
22
|
+
* extracted text against the schema (hinted output is trusted but verified).
|
|
23
|
+
* Without a schema the argv carries no output flag.
|
|
25
24
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* self-sufficient — matching the copilot/gemini native-json builders. The
|
|
30
|
-
* result envelope is unwrapped by `./result-extractor.ts`, and the engine's
|
|
31
|
-
* shared `runStructured` retry-until-valid loop still validates the extracted
|
|
32
|
-
* text against the node schema (constrained/hinted output is trusted but
|
|
33
|
-
* verified). Without a schema the argv is byte-identical to the pre-fix shape.
|
|
25
|
+
* Claude Code 2.1.283 also has `--json-schema <schema>` (JSON Schema for
|
|
26
|
+
* structured output validation, with `--print`). akm does not pass it; the
|
|
27
|
+
* instruction and the validation loop above are what enforce a schema.
|
|
34
28
|
*
|
|
35
29
|
* The builder's `platform` stays `'claude'` (the canonical harness id).
|
|
36
30
|
*/
|
|
37
|
-
import {
|
|
31
|
+
import { isModelWorkTools } from "../../../execution/source.js";
|
|
32
|
+
import { modelFromArgs, normalizeTools, resolveDispatchModel, } from "../../agent/builder-shared.js";
|
|
38
33
|
import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return `${req.prompt}\n\nRespond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(req.schema)}`;
|
|
51
|
-
}
|
|
34
|
+
/** The model-work tool policy on Claude Code: read, edit in the working directory, `akm search`, `akm show`. */
|
|
35
|
+
export const MODEL_WORK_CLAUDE_FLAGS = Object.freeze([
|
|
36
|
+
"--restricted",
|
|
37
|
+
"--strict-mcp-config",
|
|
38
|
+
"--tools",
|
|
39
|
+
"Read,Edit,Bash",
|
|
40
|
+
"--allowedTools",
|
|
41
|
+
"Read,Edit,Bash(akm search *),Bash(akm show *)",
|
|
42
|
+
"--permission-mode",
|
|
43
|
+
"dontAsk",
|
|
44
|
+
]);
|
|
52
45
|
/**
|
|
53
46
|
* Claude Code builder.
|
|
54
47
|
* Command shape:
|
|
55
|
-
* claude [--agent <name>] [--system-prompt "..."] [--model <m>] [--
|
|
56
|
-
* [--output-format json] --print -- "<prompt
|
|
48
|
+
* claude [--agent <name>] [--system-prompt "..."] [--model <m>] [--effort <level>]
|
|
49
|
+
* [--allowedTools <t>] [--output-format json] --print -- "<prompt>"
|
|
57
50
|
*
|
|
58
51
|
* --print switches Claude Code to non-interactive captured output mode.
|
|
52
|
+
*
|
|
53
|
+
* `--effort` carries the request's `reasoningEffort` as the harness's own
|
|
54
|
+
* level (`low`, `medium`, `high`, `xhigh` or `max` in 2.1.283), passed through
|
|
55
|
+
* as given: a value Claude Code rejects fails the dispatch. It is the only
|
|
56
|
+
* inference field Claude Code takes on its command line, so the others are
|
|
57
|
+
* reported as untranslated.
|
|
58
|
+
*
|
|
59
|
+
* The model-work tool policy lowers to {@link MODEL_WORK_CLAUDE_FLAGS} in place
|
|
60
|
+
* of the engine's `args` and `--allowedTools`, verified against Claude Code
|
|
61
|
+
* 2.1.283 and a local stub:
|
|
62
|
+
* - `--restricted` ignores the user, project and local settings files (whose
|
|
63
|
+
* allow rules would otherwise pre-approve any command or path) and confines
|
|
64
|
+
* the file tools to the working directory;
|
|
65
|
+
* - `--strict-mcp-config` starts no MCP server, and `--tools` offers only
|
|
66
|
+
* Read, Edit and Bash;
|
|
67
|
+
* - `--allowedTools` pre-approves Read, Edit, `akm search` and `akm show`,
|
|
68
|
+
* and `--permission-mode dontAsk` denies everything else instead of
|
|
69
|
+
* prompting, including a compound, redirected or substituted command.
|
|
70
|
+
* The engine's `args` are left out because `--add-dir`, `--settings` or a
|
|
71
|
+
* second `--allowedTools` would widen that. Only the model they name is kept.
|
|
59
72
|
*/
|
|
60
73
|
export const claudeBuilder = {
|
|
61
74
|
platform: "claude",
|
|
@@ -65,10 +78,12 @@ export const claudeBuilder = {
|
|
|
65
78
|
personaChannel: "native",
|
|
66
79
|
nativeAgentSelector: true,
|
|
67
80
|
tools: "all",
|
|
68
|
-
|
|
81
|
+
modelWorkTools: true,
|
|
82
|
+
inference: ["reasoningEffort"],
|
|
69
83
|
}),
|
|
70
84
|
build(profile, req) {
|
|
71
|
-
const
|
|
85
|
+
const modelWork = isModelWorkTools(req.tools);
|
|
86
|
+
const args = modelWork ? [...MODEL_WORK_CLAUDE_FLAGS] : [...profile.args];
|
|
72
87
|
if (req.agent) {
|
|
73
88
|
args.push("--agent", req.agent);
|
|
74
89
|
}
|
|
@@ -79,7 +94,15 @@ export const claudeBuilder = {
|
|
|
79
94
|
const resolved = resolveDispatchModel(req, profile, "claude");
|
|
80
95
|
args.push("--model", resolved);
|
|
81
96
|
}
|
|
82
|
-
if (
|
|
97
|
+
else if (modelWork) {
|
|
98
|
+
const model = modelFromArgs(profile.args);
|
|
99
|
+
if (model)
|
|
100
|
+
args.push("--model", model);
|
|
101
|
+
}
|
|
102
|
+
const effort = req.inference?.reasoningEffort;
|
|
103
|
+
if (typeof effort === "string" && effort.length > 0)
|
|
104
|
+
args.push("--effort", effort);
|
|
105
|
+
if (req.tools && !modelWork) {
|
|
83
106
|
args.push("--allowedTools", normalizeTools(req.tools));
|
|
84
107
|
}
|
|
85
108
|
if (req.schema) {
|
|
@@ -90,7 +113,7 @@ export const claudeBuilder = {
|
|
|
90
113
|
// --print = non-interactive, outputs to stdout — required for captured mode
|
|
91
114
|
args.push("--print");
|
|
92
115
|
args.push("--");
|
|
93
|
-
args.push(
|
|
116
|
+
args.push(req.prompt);
|
|
94
117
|
return { argv: [profile.bin, ...args] };
|
|
95
118
|
},
|
|
96
119
|
};
|
|
@@ -24,20 +24,6 @@ export class ClaudeHarness extends BaseHarness {
|
|
|
24
24
|
agentBuilder = claudeBuilder;
|
|
25
25
|
resultExtractor = claudeResultExtractor;
|
|
26
26
|
// ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
|
|
27
|
-
// Claude Code is the in-harness pattern: the orchestrating session itself
|
|
28
|
-
// drives units via the `akm workflow` gate spine (`claude -p` headless
|
|
29
|
-
// dispatch also exists via `agentBuilder`, but the pattern classification
|
|
30
|
-
// follows the matrix row).
|
|
31
|
-
pattern = "in-harness";
|
|
32
|
-
// Structured output tier for the AGENT-DISPATCH (`claude -p`) path akm's
|
|
33
|
-
// local runner uses (Codex round-3 finding A). The headless CLI has NO
|
|
34
|
-
// output-schema flag — its documented structured path is `--output-format
|
|
35
|
-
// json`, a RESULT ENVELOPE akm parses (`./result-extractor.ts`) and then
|
|
36
|
-
// validates against the node schema ⇒ the "native-json" tier. (Claude Code's
|
|
37
|
-
// in-harness `Workflow`/`agent()` tool-input-schema path IS native-schema,
|
|
38
|
-
// but that is a different surface than the dispatch builder — the descriptor
|
|
39
|
-
// is aligned to what the builder honestly does.)
|
|
40
|
-
structuredOutput = "native-json";
|
|
41
27
|
// Session-id env marker: presence of a concrete session id (not the bare
|
|
42
28
|
// "running under Claude Code" flag) attributes a run to this harness.
|
|
43
29
|
identityEnv = ["CLAUDE_SESSION_ID"];
|
|
@@ -42,9 +42,10 @@
|
|
|
42
42
|
* not expressible through `AgentDispatchRequest` (which has no session
|
|
43
43
|
* field yet); {@link codexResumeArgs} exposes the argv prefix for the
|
|
44
44
|
* integration task that wires session-id reuse from `workflow_run_units`.
|
|
45
|
-
* -
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* - The request's inference is not translated: the shared lowering reports
|
|
46
|
+
* each field as untranslated (`inference` in `harnesses/ids.ts` lists none
|
|
47
|
+
* for codex). codex would take `reasoningEffort` as
|
|
48
|
+
* `-c model_reasoning_effort=<v>`, which is left to the integration task.
|
|
48
49
|
*
|
|
49
50
|
* Registered: `codexBuilder` is `CodexHarness.agentBuilder` (`./index.ts`),
|
|
50
51
|
* one of the ten harnesses `HARNESS_REGISTRY` constructs
|
|
@@ -110,7 +111,6 @@ export const codexBuilder = {
|
|
|
110
111
|
adapter: "codex",
|
|
111
112
|
personaChannel: "prompt",
|
|
112
113
|
tools: "none",
|
|
113
|
-
outputSchema: true,
|
|
114
114
|
}),
|
|
115
115
|
build(profile, req) {
|
|
116
116
|
// Built-in codex profiles ship `args: []`; headless dispatch is the `exec`
|
|
@@ -29,10 +29,6 @@ export class CodexHarness extends BaseHarness {
|
|
|
29
29
|
agentBuilder = codexBuilder;
|
|
30
30
|
resultExtractor = codexResultExtractor;
|
|
31
31
|
// ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
|
|
32
|
-
// akm spawns `codex exec` locally per unit ⇒ local-runner.
|
|
33
|
-
pattern = "local-runner";
|
|
34
|
-
// `--output-schema <file>` enforces a caller-supplied JSON schema natively.
|
|
35
|
-
structuredOutput = "native-schema";
|
|
36
32
|
// No flag-shaped resume: codex resume is the `exec resume <id>` SUBCOMMAND
|
|
37
33
|
// chain (see `codexResumeArgs` in ./agent-builder.ts).
|
|
38
34
|
// Presence flag: CODEX_SANDBOX is stamped only on processes codex itself
|
|
@@ -24,9 +24,8 @@
|
|
|
24
24
|
* blank line.
|
|
25
25
|
* - **schema** — the matrix places Copilot in the "via prompt+validate" tier
|
|
26
26
|
* (no native `--output-schema` equivalent, unlike Codex), so the JSON
|
|
27
|
-
* Schema
|
|
28
|
-
*
|
|
29
|
-
* payload, and `--output-format json` is emitted so stdout is the
|
|
27
|
+
* Schema reaches it as the instruction the shared request lowering appends
|
|
28
|
+
* to the prompt, and `--output-format json` is emitted so stdout is the
|
|
30
29
|
* documented JSON envelope the copilot result extractor normalizes. The
|
|
31
30
|
* engine's shared retry-until-valid loop performs the actual validation.
|
|
32
31
|
* - **tools** — a string/array tool policy maps to repeated
|
|
@@ -66,26 +65,19 @@ function toolPolicyEntries(tools) {
|
|
|
66
65
|
}
|
|
67
66
|
return undefined;
|
|
68
67
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Assemble the `-p` payload: optional system prompt, the task prompt, and —
|
|
71
|
-
* when a schema is requested — the same schema directive the workflow
|
|
72
|
-
* engine's prompt assembly uses, so both dispatch paths speak one dialect.
|
|
73
|
-
*/
|
|
68
|
+
/** Assemble the `-p` payload: optional system prompt, then the task prompt. */
|
|
74
69
|
function buildPromptPayload(req) {
|
|
75
70
|
const sections = [];
|
|
76
71
|
if (req.systemPrompt)
|
|
77
72
|
sections.push(req.systemPrompt);
|
|
78
73
|
sections.push(req.prompt);
|
|
79
|
-
if (req.schema) {
|
|
80
|
-
sections.push(`Respond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(req.schema)}`);
|
|
81
|
-
}
|
|
82
74
|
return sections.join("\n\n");
|
|
83
75
|
}
|
|
84
76
|
/**
|
|
85
77
|
* GitHub Copilot CLI builder.
|
|
86
78
|
* Command shape:
|
|
87
79
|
* copilot [--model <m>] (--allow-all-tools | --allow-tool <t> ...)
|
|
88
|
-
* [--output-format json] -p "<systemPrompt?\n\nprompt
|
|
80
|
+
* [--output-format json] -p "<systemPrompt?\n\nprompt>"
|
|
89
81
|
*/
|
|
90
82
|
export const copilotBuilder = {
|
|
91
83
|
platform: COPILOT_PLATFORM,
|
|
@@ -94,7 +86,6 @@ export const copilotBuilder = {
|
|
|
94
86
|
adapter: COPILOT_PLATFORM,
|
|
95
87
|
personaChannel: "prompt",
|
|
96
88
|
tools: "flat",
|
|
97
|
-
outputSchema: true,
|
|
98
89
|
}),
|
|
99
90
|
build(profile, req) {
|
|
100
91
|
const args = [...profile.args];
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* It also defines {@link CopilotHarness}, the {@link AkmHarness} descriptor
|
|
12
12
|
* that `HARNESS_REGISTRY` registers. This is the LOCAL Copilot CLI
|
|
13
|
-
* (`copilot -p …`); the cloud "Copilot coding agent" is
|
|
14
|
-
*
|
|
13
|
+
* (`copilot -p …`); the cloud "Copilot coding agent" is a separate, future
|
|
14
|
+
* descriptor.
|
|
15
15
|
*/
|
|
16
16
|
import { caps } from "../shared.js";
|
|
17
17
|
import { BaseHarness } from "../types.js";
|
|
@@ -30,11 +30,6 @@ export class CopilotHarness extends BaseHarness {
|
|
|
30
30
|
agentBuilder = copilotBuilder;
|
|
31
31
|
resultExtractor = copilotResultExtractor;
|
|
32
32
|
// ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
|
|
33
|
-
// akm spawns the `copilot` CLI locally per unit ⇒ local-runner.
|
|
34
|
-
pattern = "local-runner";
|
|
35
|
-
// `--output-format json` emits a documented JSON envelope akm parses, then
|
|
36
|
-
// validates against the node schema ⇒ native-json tier.
|
|
37
|
-
structuredOutput = "native-json";
|
|
38
33
|
// Session-id env marker only. The matrix's other candidates (GH_TOKEN,
|
|
39
34
|
// bare COPILOT_* presence vars) are credential/presence flags that would
|
|
40
35
|
// stamp identity onto manual runs, so they are deliberately NOT registered
|
|
@@ -26,9 +26,8 @@
|
|
|
26
26
|
* prompt, separated by a blank line.
|
|
27
27
|
* - **schema** — the matrix places Gemini in the "via prompt+validate" tier
|
|
28
28
|
* (no native `--output-schema` equivalent, unlike Codex — so no temp-file
|
|
29
|
-
* plumbing here), so the JSON Schema
|
|
30
|
-
*
|
|
31
|
-
* `buildUnitPrompt`) is appended to the `-p` payload, and
|
|
29
|
+
* plumbing here), so the JSON Schema reaches it as the instruction the
|
|
30
|
+
* shared request lowering appends to the prompt, and
|
|
32
31
|
* `--output-format json` is emitted so stdout is the documented JSON
|
|
33
32
|
* envelope the gemini result extractor normalizes. The engine's shared
|
|
34
33
|
* retry-until-valid loop performs the actual validation.
|
|
@@ -69,26 +68,19 @@ function toolPolicyEntries(tools) {
|
|
|
69
68
|
}
|
|
70
69
|
return undefined;
|
|
71
70
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Assemble the `-p` payload: optional system prompt, the task prompt, and —
|
|
74
|
-
* when a schema is requested — the same schema directive the workflow
|
|
75
|
-
* engine's prompt assembly uses, so both dispatch paths speak one dialect.
|
|
76
|
-
*/
|
|
71
|
+
/** Assemble the `-p` payload: optional system prompt, then the task prompt. */
|
|
77
72
|
function buildPromptPayload(req) {
|
|
78
73
|
const sections = [];
|
|
79
74
|
if (req.systemPrompt)
|
|
80
75
|
sections.push(req.systemPrompt);
|
|
81
76
|
sections.push(req.prompt);
|
|
82
|
-
if (req.schema) {
|
|
83
|
-
sections.push(`Respond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(req.schema)}`);
|
|
84
|
-
}
|
|
85
77
|
return sections.join("\n\n");
|
|
86
78
|
}
|
|
87
79
|
/**
|
|
88
80
|
* Gemini CLI builder.
|
|
89
81
|
* Command shape:
|
|
90
82
|
* gemini [--model <m>] [--allowed-tools <t> ...]
|
|
91
|
-
* [--output-format json] -p "<systemPrompt?\n\nprompt
|
|
83
|
+
* [--output-format json] -p "<systemPrompt?\n\nprompt>"
|
|
92
84
|
*/
|
|
93
85
|
export const geminiBuilder = {
|
|
94
86
|
platform: GEMINI_PLATFORM,
|
|
@@ -97,7 +89,6 @@ export const geminiBuilder = {
|
|
|
97
89
|
adapter: GEMINI_PLATFORM,
|
|
98
90
|
personaChannel: "prompt",
|
|
99
91
|
tools: "flat",
|
|
100
|
-
outputSchema: true,
|
|
101
92
|
}),
|
|
102
93
|
build(profile, req) {
|
|
103
94
|
const args = [...profile.args];
|
|
@@ -29,11 +29,6 @@ export class GeminiHarness extends BaseHarness {
|
|
|
29
29
|
agentBuilder = geminiBuilder;
|
|
30
30
|
resultExtractor = geminiResultExtractor;
|
|
31
31
|
// ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
|
|
32
|
-
// akm spawns the `gemini` CLI locally per unit ⇒ local-runner.
|
|
33
|
-
pattern = "local-runner";
|
|
34
|
-
// `--output-format json` emits a documented JSON envelope akm parses, then
|
|
35
|
-
// validates against the node schema ⇒ native-json tier.
|
|
36
|
-
structuredOutput = "native-json";
|
|
37
32
|
// The matrix's identity marker: Gemini CLI stamps GEMINI_CLI=1 only on
|
|
38
33
|
// processes it spawns, so it genuinely means "running under gemini" (it is
|
|
39
34
|
// not a user-profile config var) — but its VALUE ("1") is a bare flag, not
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/** What opencode carries on a model; see `harnesses/opencode/model-config.ts`. */
|
|
5
|
+
const OPENCODE_INFERENCE = ["temperature", "maxTokens", "contextLength", "enableThinking", "reasoningEffort"];
|
|
4
6
|
export const HARNESS_ID_TABLE = [
|
|
5
|
-
{ id: "opencode", agentDispatch: true },
|
|
6
|
-
{ id: "claude", agentDispatch: true },
|
|
7
|
-
{ id: "opencode-sdk", agentDispatch: true },
|
|
8
|
-
{ id: "codex", agentDispatch: true },
|
|
9
|
-
{ id: "copilot", agentDispatch: true },
|
|
10
|
-
{ id: "pi", agentDispatch: true },
|
|
11
|
-
{ id: "gemini", agentDispatch: true },
|
|
12
|
-
{ id: "aider", agentDispatch: true },
|
|
13
|
-
{ id: "amazonq", agentDispatch: true },
|
|
14
|
-
{ id: "openhands", agentDispatch: true },
|
|
7
|
+
{ id: "opencode", agentDispatch: true, enforcesModelWorkTools: true, inference: OPENCODE_INFERENCE },
|
|
8
|
+
{ id: "claude", agentDispatch: true, enforcesModelWorkTools: true, inference: ["reasoningEffort"] },
|
|
9
|
+
{ id: "opencode-sdk", agentDispatch: true, enforcesModelWorkTools: true, inference: OPENCODE_INFERENCE },
|
|
10
|
+
{ id: "codex", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
|
|
11
|
+
{ id: "copilot", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
|
|
12
|
+
{ id: "pi", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
|
|
13
|
+
{ id: "gemini", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
|
|
14
|
+
{ id: "aider", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
|
|
15
|
+
{ id: "amazonq", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
|
|
16
|
+
{ id: "openhands", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
|
|
15
17
|
];
|
|
16
18
|
/**
|
|
17
19
|
* Canonical, ordered list of valid harness / platform ids — the
|
|
@@ -22,3 +24,9 @@ export const HARNESS_ID_TABLE = [
|
|
|
22
24
|
export const VALID_HARNESS_IDS = Object.freeze(HARNESS_ID_TABLE.map((h) => h.id));
|
|
23
25
|
/** Harness ids whose `capabilities.agentDispatch` is `true`. */
|
|
24
26
|
export const HARNESS_AGENT_DISPATCH_IDS = new Set(HARNESS_ID_TABLE.filter((h) => h.agentDispatch).map((h) => h.id));
|
|
27
|
+
/** Harness ids that confine the model-work tool policy, so unattended model work may run on them. */
|
|
28
|
+
export const HARNESS_MODEL_WORK_IDS = new Set(HARNESS_ID_TABLE.filter((h) => h.enforcesModelWorkTools).map((h) => h.id));
|
|
29
|
+
/** The inference keys an agent engine on `platform` may set; none for a platform that is not registered. */
|
|
30
|
+
export function harnessInferenceKeys(platform) {
|
|
31
|
+
return HARNESS_ID_TABLE.find((h) => h.id === platform)?.inference ?? [];
|
|
32
|
+
}
|
|
@@ -14,26 +14,68 @@
|
|
|
14
14
|
* pre-migration `opencodeBuilder`. The builder's `platform` stays `'opencode'`
|
|
15
15
|
* (the canonical harness id).
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import { isModelWorkTools } from "../../../execution/source.js";
|
|
18
|
+
import { modelFromArgs, resolveDispatchModel } from "../../agent/builder-shared.js";
|
|
18
19
|
import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
|
|
20
|
+
import { opencodeCarriedKeys, opencodeInferenceConfig, opencodeModelConfig, splitOpencodeModel } from "./model-config.js";
|
|
21
|
+
import { MODEL_WORK_OPENCODE_AGENT, modelWorkOpencodeConfig } from "./model-work-agent.js";
|
|
19
22
|
/**
|
|
20
23
|
* OpenCode builder.
|
|
21
|
-
* Command shape: opencode run [--
|
|
24
|
+
* Command shape: opencode run [--agent <name>] [--model <m>] -- "<prompt>"
|
|
25
|
+
*
|
|
26
|
+
* `opencode run` has no system-prompt option (1.18.25 prints its usage and
|
|
27
|
+
* exits 1 on `--system-prompt`), so the shared lowerer composes a persona
|
|
28
|
+
* into the prompt.
|
|
22
29
|
*
|
|
23
30
|
* Tool policy is omitted — opencode manages tool access through its own agent
|
|
24
|
-
* config files, not via CLI flags.
|
|
31
|
+
* config files, not via CLI flags. The one exception is the model-work tool
|
|
32
|
+
* policy: the builder injects its confined agent through
|
|
33
|
+
* `OPENCODE_CONFIG_CONTENT` and selects it with `--agent`. That command is
|
|
34
|
+
* akm's own (`opencode run --agent akm-model-work`): the engine's `args` are
|
|
35
|
+
* left out, because one such as `--attach` or `--dir` would move the run out
|
|
36
|
+
* of the injected config or the scratch working directory. Only the model they
|
|
37
|
+
* name is kept.
|
|
38
|
+
*
|
|
39
|
+
* Inference reaches the run the same way (`./model-config.ts`): the model it
|
|
40
|
+
* names gets an entry in the injected config, which merges over the user's own
|
|
41
|
+
* config for that model, and model work's agent carries the options, whether
|
|
42
|
+
* or not a model is named. A dispatch whose request carries no translatable
|
|
43
|
+
* inference injects nothing, so its argv and env are as they were.
|
|
25
44
|
*/
|
|
26
45
|
export const opencodeBuilder = {
|
|
27
46
|
platform: "opencode",
|
|
28
|
-
personaChannel: "
|
|
47
|
+
personaChannel: "prompt",
|
|
29
48
|
lower: createAgentRequestLowerer({
|
|
30
49
|
adapter: "opencode",
|
|
31
|
-
personaChannel: "
|
|
50
|
+
personaChannel: "prompt",
|
|
32
51
|
nativeAgentSelector: true,
|
|
33
52
|
tools: "none",
|
|
34
|
-
|
|
53
|
+
modelWorkTools: true,
|
|
54
|
+
inference: (profile, request) => {
|
|
55
|
+
const model = request.model?.resolved ?? modelFromArgs(profile.args);
|
|
56
|
+
return opencodeCarriedKeys(model !== undefined && splitOpencodeModel(model) !== undefined, request.inference, isModelWorkTools(request.tools));
|
|
57
|
+
},
|
|
35
58
|
}),
|
|
36
59
|
build(profile, req) {
|
|
60
|
+
const model = req.model ?? modelFromArgs(profile.args);
|
|
61
|
+
const modelWork = isModelWorkTools(req.tools);
|
|
62
|
+
// What goes on the model needs a `provider/model` to go on; model work's agent options do not.
|
|
63
|
+
const { entry, agentOptions } = opencodeInferenceConfig(req.inference, modelWork);
|
|
64
|
+
const modelConfig = opencodeModelConfig(model, entry);
|
|
65
|
+
if (modelWork) {
|
|
66
|
+
return {
|
|
67
|
+
argv: [
|
|
68
|
+
profile.bin,
|
|
69
|
+
"run",
|
|
70
|
+
"--agent",
|
|
71
|
+
MODEL_WORK_OPENCODE_AGENT,
|
|
72
|
+
...(model ? ["--model", model] : []),
|
|
73
|
+
"--",
|
|
74
|
+
req.prompt,
|
|
75
|
+
],
|
|
76
|
+
env: { OPENCODE_CONFIG_CONTENT: JSON.stringify({ ...modelWorkOpencodeConfig(agentOptions), ...modelConfig }) },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
37
79
|
const args = req.model ? [] : [...profile.args];
|
|
38
80
|
if (req.model) {
|
|
39
81
|
for (let index = 0; index < profile.args.length; index += 1) {
|
|
@@ -48,9 +90,6 @@ export const opencodeBuilder = {
|
|
|
48
90
|
}
|
|
49
91
|
}
|
|
50
92
|
}
|
|
51
|
-
if (req.systemPrompt) {
|
|
52
|
-
args.push("--system-prompt", req.systemPrompt);
|
|
53
|
-
}
|
|
54
93
|
if (req.agent) {
|
|
55
94
|
args.push("--agent", req.agent);
|
|
56
95
|
}
|
|
@@ -60,6 +99,9 @@ export const opencodeBuilder = {
|
|
|
60
99
|
}
|
|
61
100
|
args.push("--");
|
|
62
101
|
args.push(req.prompt);
|
|
63
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
argv: [profile.bin, ...args],
|
|
104
|
+
...(modelConfig ? { env: { OPENCODE_CONFIG_CONTENT: JSON.stringify(modelConfig) } } : {}),
|
|
105
|
+
};
|
|
64
106
|
},
|
|
65
107
|
};
|
|
@@ -21,14 +21,6 @@ export class OpencodeHarness extends BaseHarness {
|
|
|
21
21
|
setupDetectionDir = ".config/opencode";
|
|
22
22
|
agentBuilder = opencodeBuilder;
|
|
23
23
|
// ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
|
|
24
|
-
// This entry is the CLI spawn path (`opencode run …`): akm launches the
|
|
25
|
-
// harness locally per unit ⇒ local-runner. (The SDK path is the separate
|
|
26
|
-
// `opencode-sdk` harness.)
|
|
27
|
-
pattern = "local-runner";
|
|
28
|
-
// The CLI path emits plain text — no JSON stream akm consumes — so the
|
|
29
|
-
// engine uses the prompt-injected schema + embedded-JSON extraction tier
|
|
30
|
-
// (the matrix's "via prompt+validate"). The SDK entry is native-json.
|
|
31
|
-
structuredOutput = "none";
|
|
32
24
|
// Session-id env marker for run attribution.
|
|
33
25
|
identityEnv = ["OPENCODE_SESSION_ID"];
|
|
34
26
|
sessionLogProvider = () => new OpenCodeProvider();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
const isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
5
|
+
/**
|
|
6
|
+
* What opencode carries of `inference`, and the keys that covers. A key it
|
|
7
|
+
* cannot carry is left out: one of the wrong type, or `maxTokens` or
|
|
8
|
+
* `contextLength` without the other. An explicit `null` clears a field, so it
|
|
9
|
+
* is carried as nothing.
|
|
10
|
+
*/
|
|
11
|
+
export function carriedInference(inference) {
|
|
12
|
+
const carried = {};
|
|
13
|
+
const { temperature, reasoningEffort, enableThinking, maxTokens, contextLength } = inference ?? {};
|
|
14
|
+
if (typeof temperature === "number" && Number.isFinite(temperature))
|
|
15
|
+
carried.temperature = temperature;
|
|
16
|
+
if (typeof reasoningEffort === "string" && reasoningEffort.length > 0)
|
|
17
|
+
carried.reasoningEffort = reasoningEffort;
|
|
18
|
+
if (typeof enableThinking === "boolean")
|
|
19
|
+
carried.enableThinking = enableThinking;
|
|
20
|
+
const limit = isPositiveInteger(maxTokens) && isPositiveInteger(contextLength);
|
|
21
|
+
if (limit)
|
|
22
|
+
carried.limit = { context: contextLength, output: maxTokens };
|
|
23
|
+
const keys = [
|
|
24
|
+
["temperature", carried.temperature !== undefined, temperature],
|
|
25
|
+
["reasoningEffort", carried.reasoningEffort !== undefined, reasoningEffort],
|
|
26
|
+
["enableThinking", carried.enableThinking !== undefined, enableThinking],
|
|
27
|
+
["maxTokens", limit, maxTokens],
|
|
28
|
+
["contextLength", limit, contextLength],
|
|
29
|
+
]
|
|
30
|
+
.filter(([, isCarried, value]) => isCarried || value === null)
|
|
31
|
+
.map(([key]) => key);
|
|
32
|
+
return { carried, keys };
|
|
33
|
+
}
|
|
34
|
+
/** opencode's own split of `provider/model`: at the first slash, so a model id may contain slashes. */
|
|
35
|
+
export function splitOpencodeModel(model) {
|
|
36
|
+
const slash = model.indexOf("/");
|
|
37
|
+
if (slash <= 0 || slash === model.length - 1)
|
|
38
|
+
return undefined;
|
|
39
|
+
return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The inference keys opencode carries for a dispatch. With a model to attach
|
|
43
|
+
* them to (`attachable`) it carries all of them. Without one it carries only
|
|
44
|
+
* what the model-work agent can, which is every option but the limit; any
|
|
45
|
+
* other dispatch carries nothing.
|
|
46
|
+
*/
|
|
47
|
+
export function opencodeCarriedKeys(attachable, inference, modelWork) {
|
|
48
|
+
const { keys } = carriedInference(inference);
|
|
49
|
+
if (attachable)
|
|
50
|
+
return keys;
|
|
51
|
+
return modelWork ? keys.filter((key) => key !== "maxTokens" && key !== "contextLength") : [];
|
|
52
|
+
}
|
|
53
|
+
/** Split `inference` between the model and, for model work, the confined agent (see the module comment). */
|
|
54
|
+
export function opencodeInferenceConfig(inference, modelWork) {
|
|
55
|
+
const { carried } = carriedInference(inference);
|
|
56
|
+
const options = {};
|
|
57
|
+
if (carried.temperature !== undefined)
|
|
58
|
+
options.temperature = carried.temperature;
|
|
59
|
+
if (carried.reasoningEffort !== undefined)
|
|
60
|
+
options.reasoningEffort = carried.reasoningEffort;
|
|
61
|
+
if (carried.enableThinking !== undefined) {
|
|
62
|
+
options.chat_template_kwargs = { enable_thinking: carried.enableThinking };
|
|
63
|
+
options.enable_thinking = carried.enableThinking;
|
|
64
|
+
}
|
|
65
|
+
const hasOptions = Object.keys(options).length > 0;
|
|
66
|
+
return {
|
|
67
|
+
entry: {
|
|
68
|
+
...(hasOptions && !modelWork ? { options } : {}),
|
|
69
|
+
...(carried.limit ? { limit: { ...carried.limit } } : {}),
|
|
70
|
+
},
|
|
71
|
+
...(hasOptions && modelWork ? { agentOptions: options } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** The config that gives `model` (`provider/model`) `entry`: undefined when the model cannot be split or `entry` is empty. */
|
|
75
|
+
export function opencodeModelConfig(model, entry) {
|
|
76
|
+
const target = model === undefined ? undefined : splitOpencodeModel(model);
|
|
77
|
+
if (!target || Object.keys(entry).length === 0)
|
|
78
|
+
return undefined;
|
|
79
|
+
return { provider: { [target.providerID]: { models: { [target.modelID]: entry } } } };
|
|
80
|
+
}
|