akm-cli 0.9.25-alpha.1 → 0.9.25-alpha.3
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 +243 -280
- package/dist/assets/prompts/reflect-feedback-framing.md +1 -1
- package/dist/assets/prompts/reflect-llm-framed-contract.md +2 -9
- package/dist/assets/prompts/reflect-llm-schema-contract.md +1 -3
- package/dist/assets/prompts/reflect-output-repair.md +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/improve/consolidate/pair-pass.js +1 -0
- package/dist/commands/improve/consolidate.js +7 -2
- package/dist/commands/improve/execution.js +2 -3
- package/dist/commands/improve/extract-cli.js +3 -2
- package/dist/commands/improve/extract.js +2 -1
- package/dist/commands/improve/improve-cli.js +33 -1
- package/dist/commands/improve/loop-stages.js +3 -0
- package/dist/commands/improve/reflect-noise.js +125 -0
- package/dist/commands/improve/reflect.js +150 -333
- package/dist/commands/improve/retrieval-gate.js +7 -2
- package/dist/commands/improve/session-asset.js +6 -0
- package/dist/commands/improve/stage.js +31 -39
- package/dist/commands/proposal/drain.js +4 -7
- package/dist/commands/proposal/propose.js +2 -11
- package/dist/commands/proposal/validators/proposal-quality-validators.js +11 -5
- package/dist/commands/proposal/validators/proposal-validators.js +4 -5
- package/dist/commands/read/search-cli.js +0 -38
- package/dist/core/asset/asset-serialize.js +1 -1
- package/dist/core/config/schema/engines.js +15 -33
- package/dist/core/config/schema/improve-processes.js +16 -0
- package/dist/core/content-safety.js +0 -24
- package/dist/core/redaction.js +4 -0
- package/dist/core/spawn-env.js +25 -0
- package/dist/core/structured.js +1 -1
- package/dist/execution/source.js +8 -12
- package/dist/integrations/agent/config.js +1 -3
- package/dist/integrations/agent/engine-resolution.js +0 -3
- package/dist/integrations/agent/execution.js +14 -13
- package/dist/integrations/agent/index.js +1 -1
- package/dist/integrations/agent/model-map.js +15 -16
- package/dist/integrations/agent/profiles.js +2 -2
- package/dist/integrations/agent/prompts.js +51 -127
- package/dist/integrations/agent/request-lowering.js +9 -7
- package/dist/integrations/agent/runner-dispatch.js +25 -31
- package/dist/integrations/harnesses/aider/agent-builder.js +1 -2
- package/dist/integrations/harnesses/amazonq/agent-builder.js +1 -2
- package/dist/integrations/harnesses/claude/agent-builder.js +4 -16
- package/dist/integrations/harnesses/codex/agent-builder.js +1 -2
- package/dist/integrations/harnesses/codex/index.js +6 -11
- package/dist/integrations/harnesses/codex/session-log.js +211 -0
- package/dist/integrations/harnesses/ids.js +10 -16
- package/dist/integrations/harnesses/opencode/agent-builder.js +14 -24
- package/dist/integrations/harnesses/opencode/model-config.js +15 -62
- package/dist/integrations/harnesses/opencode/model-work-agent.js +71 -36
- package/dist/integrations/harnesses/opencode-sdk/harness.js +2 -6
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +32 -90
- package/dist/integrations/harnesses/openhands/agent-builder.js +1 -2
- package/dist/integrations/harnesses/pi/agent-builder.js +1 -2
- package/dist/integrations/harnesses/types.js +3 -3
- package/dist/llm/feature-gate.js +2 -5
- package/dist/llm/index-passes.js +2 -2
- package/dist/llm/structured-call.js +5 -5
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/scripts/akm-migrate-node.js +381 -239
- package/dist/scripts/akm-migrate.js +381 -239
- package/dist/workflows/exec/unit-dispatch.js +4 -13
- package/docs/reference/cli.md +29 -16
- package/docs/reference/configuration.md +79 -85
- package/docs/reference/data-and-telemetry.md +2 -3
- package/docs/reference/workflow-schema.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +108 -36
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
*
|
|
29
29
|
* The builder's `platform` stays `'claude'` (the canonical harness id).
|
|
30
30
|
*/
|
|
31
|
-
import { isModelWorkTools } from "../../../execution/source.js";
|
|
32
31
|
import { modelFromArgs, normalizeTools, resolveDispatchModel, } from "../../agent/builder-shared.js";
|
|
33
32
|
import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
|
|
34
33
|
/** The model-work tool policy on Claude Code: read, edit in the working directory, `akm search`, `akm show`. */
|
|
@@ -45,17 +44,11 @@ export const MODEL_WORK_CLAUDE_FLAGS = Object.freeze([
|
|
|
45
44
|
/**
|
|
46
45
|
* Claude Code builder.
|
|
47
46
|
* Command shape:
|
|
48
|
-
* claude [--agent <name>] [--system-prompt "..."] [--model <m>] [--
|
|
49
|
-
* [--
|
|
47
|
+
* claude [--agent <name>] [--system-prompt "..."] [--model <m>] [--allowedTools <t>]
|
|
48
|
+
* [--output-format json] --print -- "<prompt>"
|
|
50
49
|
*
|
|
51
50
|
* --print switches Claude Code to non-interactive captured output mode.
|
|
52
51
|
*
|
|
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
52
|
* The model-work tool policy lowers to {@link MODEL_WORK_CLAUDE_FLAGS} in place
|
|
60
53
|
* of the engine's `args` and `--allowedTools`, verified against Claude Code
|
|
61
54
|
* 2.1.283 and a local stub:
|
|
@@ -78,11 +71,9 @@ export const claudeBuilder = {
|
|
|
78
71
|
personaChannel: "native",
|
|
79
72
|
nativeAgentSelector: true,
|
|
80
73
|
tools: "all",
|
|
81
|
-
modelWorkTools: true,
|
|
82
|
-
inference: ["reasoningEffort"],
|
|
83
74
|
}),
|
|
84
75
|
build(profile, req) {
|
|
85
|
-
const modelWork =
|
|
76
|
+
const modelWork = req.modelWork === true;
|
|
86
77
|
const args = modelWork ? [...MODEL_WORK_CLAUDE_FLAGS] : [...profile.args];
|
|
87
78
|
if (req.agent) {
|
|
88
79
|
args.push("--agent", req.agent);
|
|
@@ -99,10 +90,7 @@ export const claudeBuilder = {
|
|
|
99
90
|
if (model)
|
|
100
91
|
args.push("--model", model);
|
|
101
92
|
}
|
|
102
|
-
|
|
103
|
-
if (typeof effort === "string" && effort.length > 0)
|
|
104
|
-
args.push("--effort", effort);
|
|
105
|
-
if (req.tools && !modelWork) {
|
|
93
|
+
if (req.tools) {
|
|
106
94
|
args.push("--allowedTools", normalizeTools(req.tools));
|
|
107
95
|
}
|
|
108
96
|
if (req.schema) {
|
|
@@ -43,8 +43,7 @@
|
|
|
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
45
|
* - The request's inference is not translated: the shared lowering reports
|
|
46
|
-
* each field as untranslated
|
|
47
|
-
* for codex). codex would take `reasoningEffort` as
|
|
46
|
+
* each field as untranslated. codex would take `reasoningEffort` as
|
|
48
47
|
* `-c model_reasoning_effort=<v>`, which is left to the integration task.
|
|
49
48
|
*
|
|
50
49
|
* Registered: `codexBuilder` is `CodexHarness.agentBuilder` (`./index.ts`),
|
|
@@ -1,23 +1,14 @@
|
|
|
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
|
-
/**
|
|
5
|
-
* OpenAI Codex CLI harness (P2 integration, plan §"The adapter contract").
|
|
6
|
-
*
|
|
7
|
-
* Per-harness barrel gathering the Codex integration surfaces:
|
|
8
|
-
* - agent command builder → ./agent-builder.ts (codexBuilder)
|
|
9
|
-
* - result extractor → ./result-extractor.ts (codexResultExtractor)
|
|
10
|
-
*
|
|
11
|
-
* It also defines {@link CodexHarness}, the {@link AkmHarness} descriptor that
|
|
12
|
-
* `HARNESS_REGISTRY` registers. Dispatch-only: no native session-log reader or
|
|
13
|
-
* config importer yet.
|
|
14
|
-
*/
|
|
15
4
|
import { caps } from "../shared.js";
|
|
16
5
|
import { BaseHarness } from "../types.js";
|
|
17
6
|
import { codexBuilder } from "./agent-builder.js";
|
|
18
7
|
import { codexResultExtractor } from "./result-extractor.js";
|
|
8
|
+
import { CodexProvider } from "./session-log.js";
|
|
19
9
|
export { codexBuilder, codexResumeArgs, writeCodexOutputSchemaFile } from "./agent-builder.js";
|
|
20
10
|
export { codexResultExtractor } from "./result-extractor.js";
|
|
11
|
+
export { CodexProvider } from "./session-log.js";
|
|
21
12
|
/**
|
|
22
13
|
* OpenAI Codex CLI.
|
|
23
14
|
*
|
|
@@ -26,6 +17,8 @@ export { codexResultExtractor } from "./result-extractor.js";
|
|
|
26
17
|
export class CodexHarness extends BaseHarness {
|
|
27
18
|
id = "codex";
|
|
28
19
|
displayName = "OpenAI Codex CLI";
|
|
20
|
+
// No `setupDetectionDir`: `~/.codex` holds credentials and session rollouts,
|
|
21
|
+
// not assets, so `akm setup` must not offer it as a stash source.
|
|
29
22
|
agentBuilder = codexBuilder;
|
|
30
23
|
resultExtractor = codexResultExtractor;
|
|
31
24
|
// ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
|
|
@@ -40,7 +33,9 @@ export class CodexHarness extends BaseHarness {
|
|
|
40
33
|
// user config-dir var commonly exported in shell profiles, so it would stamp
|
|
41
34
|
// identity onto manual runs (see `AkmHarness.presenceEnv`).
|
|
42
35
|
presenceEnv = ["CODEX_SANDBOX"];
|
|
36
|
+
sessionLogProvider = () => new CodexProvider();
|
|
43
37
|
capabilities = caps({
|
|
38
|
+
sessionLogs: true,
|
|
44
39
|
agentDispatch: true,
|
|
45
40
|
detection: true,
|
|
46
41
|
});
|
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { extractInlineRefMentions } from "../../session-logs/inline-refs.js";
|
|
8
|
+
import { AbstractSessionLogProvider } from "../../session-logs/provider-base.js";
|
|
9
|
+
/**
|
|
10
|
+
* Root directory holding Codex's rollout files, one per session, as
|
|
11
|
+
* `YYYY/MM/DD/rollout-<timestamp>-<thread-id>.jsonl`.
|
|
12
|
+
*
|
|
13
|
+
* Resolved per call (not memoized at module load) so `CODEX_HOME` — Codex's own
|
|
14
|
+
* override of `~/.codex` — can be set after import; tests point it at a fixture
|
|
15
|
+
* directory instead of the real history.
|
|
16
|
+
*/
|
|
17
|
+
function codexSessionsDir() {
|
|
18
|
+
return path.join(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"), "sessions");
|
|
19
|
+
}
|
|
20
|
+
/** Bytes read to get a rollout's first record, `session_meta`: its embedded base instructions make it 20-50 KB. */
|
|
21
|
+
const META_PEEK_BYTES = 128 * 1024;
|
|
22
|
+
/**
|
|
23
|
+
* User-role blocks Codex injects itself, which are not the person's words: the
|
|
24
|
+
* AGENTS.md instructions and the context tags it is known to wrap (environment,
|
|
25
|
+
* plugin recommendations, an invoked skill's text, ...). Any other block is kept:
|
|
26
|
+
* a stray bit of context costs less than a dropped reply.
|
|
27
|
+
*/
|
|
28
|
+
const INJECTED_CONTEXT_RE = /^\s*(?:# AGENTS\.md instructions[\s\S]*<\/INSTRUCTIONS>|<(environment_context|user_instructions|recommended_plugins|skill|turn_aborted)>[\s\S]*<\/\1>)\s*$/i;
|
|
29
|
+
/** The `session_meta` record, always a rollout's first; `undefined` for any other record. */
|
|
30
|
+
function parseSessionMeta(entry) {
|
|
31
|
+
const e = entry;
|
|
32
|
+
if (e?.type !== "session_meta" || !e.payload)
|
|
33
|
+
return undefined;
|
|
34
|
+
const { timestamp, cwd, source } = e.payload;
|
|
35
|
+
const startedAt = typeof timestamp === "string" ? Date.parse(timestamp) : Number.NaN;
|
|
36
|
+
return {
|
|
37
|
+
...(Number.isNaN(startedAt) ? {} : { startedAt }),
|
|
38
|
+
...(typeof cwd === "string" ? { cwd } : {}),
|
|
39
|
+
isAgent: typeof source === "object" && source !== null && ("subagent" in source || "internal" in source),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** The text of a message's or tool output's content: a plain string, or blocks whose images and audio carry none. */
|
|
43
|
+
function blockText(blocks, keep = () => true) {
|
|
44
|
+
if (typeof blocks === "string")
|
|
45
|
+
return blocks;
|
|
46
|
+
if (!Array.isArray(blocks))
|
|
47
|
+
return "";
|
|
48
|
+
const parts = [];
|
|
49
|
+
for (const block of blocks) {
|
|
50
|
+
const text = block?.text;
|
|
51
|
+
if (typeof text === "string" && keep(text))
|
|
52
|
+
parts.push(text);
|
|
53
|
+
}
|
|
54
|
+
return parts.join("\n");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A tool call's input as text. A shell call surfaces its command line (`cmd`, or
|
|
58
|
+
* `command` as a string or an argv array) so the inline-ref scanner can match
|
|
59
|
+
* `akm remember "..."` without JSON-quote escaping mangling the regex; any
|
|
60
|
+
* other input stays as it was written.
|
|
61
|
+
*/
|
|
62
|
+
function toolInputText(input) {
|
|
63
|
+
let args = input;
|
|
64
|
+
if (typeof input === "string") {
|
|
65
|
+
try {
|
|
66
|
+
args = JSON.parse(input);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return input;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const fields = args && typeof args === "object" ? args : {};
|
|
73
|
+
const cmd = fields.cmd ?? fields.command;
|
|
74
|
+
if (typeof cmd === "string")
|
|
75
|
+
return cmd;
|
|
76
|
+
if (Array.isArray(cmd))
|
|
77
|
+
return cmd.join(" ");
|
|
78
|
+
return typeof input === "string" ? input : (JSON.stringify(input) ?? "");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Parse one rollout record into a normalized {@link SessionEvent}. The
|
|
82
|
+
* conversation lives in `response_item` records; the rest (`event_msg` UI events
|
|
83
|
+
* that repeat it, `turn_context`, `world_state`, token counts, compaction
|
|
84
|
+
* markers) and items with nothing to read (`reasoning` is encrypted) give
|
|
85
|
+
* `undefined`. Codex's `developer` messages are its own instructions, so they
|
|
86
|
+
* are skipped too.
|
|
87
|
+
*
|
|
88
|
+
* Tool calls become `[tool:<name>] <input>` and their results `[tool_result]
|
|
89
|
+
* <output>`, the shapes the Claude reader writes. A result is a `tool` event:
|
|
90
|
+
* Codex has no enclosing user message to give it.
|
|
91
|
+
*/
|
|
92
|
+
function parseCodexRecord(entry, sessionId, filePath, fallbackTsMs) {
|
|
93
|
+
const e = entry;
|
|
94
|
+
const item = e?.type === "response_item" ? e.payload : undefined;
|
|
95
|
+
if (!item)
|
|
96
|
+
return undefined;
|
|
97
|
+
const toolName = typeof item.name === "string" ? item.name : "tool";
|
|
98
|
+
let role = "assistant";
|
|
99
|
+
let text;
|
|
100
|
+
switch (item.type) {
|
|
101
|
+
case "message":
|
|
102
|
+
if (item.role !== "user" && item.role !== "assistant")
|
|
103
|
+
return undefined;
|
|
104
|
+
role = item.role;
|
|
105
|
+
text = blockText(item.content, item.role === "user" ? (t) => !INJECTED_CONTEXT_RE.test(t) : undefined);
|
|
106
|
+
break;
|
|
107
|
+
case "function_call":
|
|
108
|
+
text = `[tool:${toolName}] ${toolInputText(item.arguments)}`;
|
|
109
|
+
break;
|
|
110
|
+
case "custom_tool_call":
|
|
111
|
+
text = `[tool:${toolName}] ${typeof item.input === "string" ? item.input : ""}`;
|
|
112
|
+
break;
|
|
113
|
+
case "local_shell_call":
|
|
114
|
+
text = `[tool:shell] ${toolInputText(item.action)}`;
|
|
115
|
+
break;
|
|
116
|
+
case "function_call_output":
|
|
117
|
+
case "custom_tool_call_output": {
|
|
118
|
+
role = "tool";
|
|
119
|
+
const output = blockText(item.output);
|
|
120
|
+
text = output ? `[tool_result] ${output}` : "";
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
default:
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
if (!text.trim())
|
|
127
|
+
return undefined;
|
|
128
|
+
const ts = typeof e?.timestamp === "string" ? Date.parse(e.timestamp) || fallbackTsMs : fallbackTsMs;
|
|
129
|
+
return { harness: "codex", text, ts, sessionId, role, filePath };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Codex native session-log reader.
|
|
133
|
+
*
|
|
134
|
+
* Events, refs, extraction keys, and the harness registry all use `codex`. The
|
|
135
|
+
* session id is the thread id in the rollout's file name (the id `codex resume`
|
|
136
|
+
* takes) and `projectHint` is the session's working directory.
|
|
137
|
+
*/
|
|
138
|
+
export class CodexProvider extends AbstractSessionLogProvider {
|
|
139
|
+
name = "codex";
|
|
140
|
+
availabilityRoot() {
|
|
141
|
+
return codexSessionsDir();
|
|
142
|
+
}
|
|
143
|
+
listSessions(input = {}) {
|
|
144
|
+
return this.listSessionsFromFiles({
|
|
145
|
+
sinceMs: input.sinceMs ?? 0,
|
|
146
|
+
enumerate: () => this.walkFiles(input.location ?? codexSessionsDir(), (name) => /^rollout-.+\.jsonl$/.test(name)),
|
|
147
|
+
summarize: (rolloutPath, stat) => {
|
|
148
|
+
const meta = this.#peekMeta(rolloutPath);
|
|
149
|
+
if (meta?.isAgent)
|
|
150
|
+
return undefined;
|
|
151
|
+
return this.sessionRef({
|
|
152
|
+
// The thread id is the 36-character UUID that ends the file name.
|
|
153
|
+
sessionId: path.basename(rolloutPath, ".jsonl").slice(-36),
|
|
154
|
+
filePath: rolloutPath,
|
|
155
|
+
startedAt: meta?.startedAt ?? stat.ctimeMs,
|
|
156
|
+
endedAt: stat.mtimeMs,
|
|
157
|
+
projectHint: meta?.cwd,
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
readSession(ref) {
|
|
163
|
+
const stat = fs.statSync(ref.filePath);
|
|
164
|
+
const events = [];
|
|
165
|
+
const inlineRefs = [];
|
|
166
|
+
let meta;
|
|
167
|
+
for (const line of fs.readFileSync(ref.filePath, "utf8").split("\n").filter(Boolean)) {
|
|
168
|
+
let entry;
|
|
169
|
+
try {
|
|
170
|
+
entry = JSON.parse(line);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
continue; // a partial line (the session is still being written) or garbage
|
|
174
|
+
}
|
|
175
|
+
meta ??= parseSessionMeta(entry);
|
|
176
|
+
const parsed = parseCodexRecord(entry, ref.sessionId, ref.filePath, stat.mtimeMs);
|
|
177
|
+
if (!parsed)
|
|
178
|
+
continue;
|
|
179
|
+
events.push(parsed);
|
|
180
|
+
inlineRefs.push(...extractInlineRefMentions(parsed.text, parsed.ts));
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
ref: this.sessionRef({
|
|
184
|
+
sessionId: ref.sessionId,
|
|
185
|
+
filePath: ref.filePath,
|
|
186
|
+
startedAt: meta?.startedAt ?? events[0]?.ts ?? stat.ctimeMs,
|
|
187
|
+
endedAt: events[events.length - 1]?.ts ?? stat.mtimeMs,
|
|
188
|
+
projectHint: meta?.cwd,
|
|
189
|
+
}),
|
|
190
|
+
events,
|
|
191
|
+
inlineRefs,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** The `session_meta` record on a rollout's first line, read without loading the file. */
|
|
195
|
+
#peekMeta(filePath) {
|
|
196
|
+
try {
|
|
197
|
+
const fd = fs.openSync(filePath, "r");
|
|
198
|
+
try {
|
|
199
|
+
const head = Buffer.alloc(META_PEEK_BYTES);
|
|
200
|
+
const firstLine = head.toString("utf8", 0, fs.readSync(fd, head, 0, head.length, 0)).split("\n", 1)[0] ?? "";
|
|
201
|
+
return parseSessionMeta(JSON.parse(firstLine));
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
fs.closeSync(fd);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return undefined; // unreadable / vanished file, or a first line that is not one whole record
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -1,19 +1,17 @@
|
|
|
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"];
|
|
6
4
|
export const HARNESS_ID_TABLE = [
|
|
7
|
-
{ id: "opencode", agentDispatch: true, enforcesModelWorkTools: true
|
|
8
|
-
{ id: "claude", agentDispatch: true, enforcesModelWorkTools: true
|
|
9
|
-
{ id: "opencode-sdk", agentDispatch: true, enforcesModelWorkTools: true
|
|
10
|
-
{ id: "codex", agentDispatch: true, enforcesModelWorkTools: false
|
|
11
|
-
{ id: "copilot", agentDispatch: true, enforcesModelWorkTools: false
|
|
12
|
-
{ id: "pi", agentDispatch: true, enforcesModelWorkTools: false
|
|
13
|
-
{ id: "gemini", agentDispatch: true, enforcesModelWorkTools: false
|
|
14
|
-
{ id: "aider", agentDispatch: true, enforcesModelWorkTools: false
|
|
15
|
-
{ id: "amazonq", agentDispatch: true, enforcesModelWorkTools: false
|
|
16
|
-
{ id: "openhands", agentDispatch: true, enforcesModelWorkTools: false
|
|
5
|
+
{ id: "opencode", agentDispatch: true, enforcesModelWorkTools: true },
|
|
6
|
+
{ id: "claude", agentDispatch: true, enforcesModelWorkTools: true },
|
|
7
|
+
{ id: "opencode-sdk", agentDispatch: true, enforcesModelWorkTools: true },
|
|
8
|
+
{ id: "codex", agentDispatch: true, enforcesModelWorkTools: false },
|
|
9
|
+
{ id: "copilot", agentDispatch: true, enforcesModelWorkTools: false },
|
|
10
|
+
{ id: "pi", agentDispatch: true, enforcesModelWorkTools: false },
|
|
11
|
+
{ id: "gemini", agentDispatch: true, enforcesModelWorkTools: false },
|
|
12
|
+
{ id: "aider", agentDispatch: true, enforcesModelWorkTools: false },
|
|
13
|
+
{ id: "amazonq", agentDispatch: true, enforcesModelWorkTools: false },
|
|
14
|
+
{ id: "openhands", agentDispatch: true, enforcesModelWorkTools: false },
|
|
17
15
|
];
|
|
18
16
|
/**
|
|
19
17
|
* Canonical, ordered list of valid harness / platform ids — the
|
|
@@ -26,7 +24,3 @@ export const VALID_HARNESS_IDS = Object.freeze(HARNESS_ID_TABLE.map((h) => h.id)
|
|
|
26
24
|
export const HARNESS_AGENT_DISPATCH_IDS = new Set(HARNESS_ID_TABLE.filter((h) => h.agentDispatch).map((h) => h.id));
|
|
27
25
|
/** Harness ids that confine the model-work tool policy, so unattended model work may run on them. */
|
|
28
26
|
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,11 +14,10 @@
|
|
|
14
14
|
* pre-migration `opencodeBuilder`. The builder's `platform` stays `'opencode'`
|
|
15
15
|
* (the canonical harness id).
|
|
16
16
|
*/
|
|
17
|
-
import { isModelWorkTools } from "../../../execution/source.js";
|
|
18
17
|
import { modelFromArgs, resolveDispatchModel } from "../../agent/builder-shared.js";
|
|
19
18
|
import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
|
|
20
|
-
import {
|
|
21
|
-
import { MODEL_WORK_OPENCODE_AGENT, modelWorkOpencodeConfig } from "./model-work-agent.js";
|
|
19
|
+
import { MODEL_WORK_AGENT_INFERENCE, opencodeInferenceConfig } from "./model-config.js";
|
|
20
|
+
import { MODEL_WORK_OPENCODE_AGENT, modelWorkOpencodeConfig, modelWorkPluginEnv } from "./model-work-agent.js";
|
|
22
21
|
/**
|
|
23
22
|
* OpenCode builder.
|
|
24
23
|
* Command shape: opencode run [--agent <name>] [--model <m>] -- "<prompt>"
|
|
@@ -36,11 +35,9 @@ import { MODEL_WORK_OPENCODE_AGENT, modelWorkOpencodeConfig } from "./model-work
|
|
|
36
35
|
* of the injected config or the scratch working directory. Only the model they
|
|
37
36
|
* name is kept.
|
|
38
37
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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.
|
|
38
|
+
* Model work's agent carries the request's inference options
|
|
39
|
+
* (`./model-config.ts`). Any other dispatch injects nothing and carries none, so
|
|
40
|
+
* the model's own opencode config applies: set inference there.
|
|
44
41
|
*/
|
|
45
42
|
export const opencodeBuilder = {
|
|
46
43
|
platform: "opencode",
|
|
@@ -50,19 +47,12 @@ export const opencodeBuilder = {
|
|
|
50
47
|
personaChannel: "prompt",
|
|
51
48
|
nativeAgentSelector: true,
|
|
52
49
|
tools: "none",
|
|
53
|
-
|
|
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
|
-
},
|
|
50
|
+
inference: MODEL_WORK_AGENT_INFERENCE,
|
|
58
51
|
}),
|
|
59
52
|
build(profile, req) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const { entry, agentOptions } = opencodeInferenceConfig(req.inference, modelWork);
|
|
64
|
-
const modelConfig = opencodeModelConfig(model, entry);
|
|
65
|
-
if (modelWork) {
|
|
53
|
+
if (req.modelWork) {
|
|
54
|
+
const model = req.model ?? modelFromArgs(profile.args);
|
|
55
|
+
const { agentOptions } = opencodeInferenceConfig(req.inference, true);
|
|
66
56
|
return {
|
|
67
57
|
argv: [
|
|
68
58
|
profile.bin,
|
|
@@ -73,7 +63,10 @@ export const opencodeBuilder = {
|
|
|
73
63
|
"--",
|
|
74
64
|
req.prompt,
|
|
75
65
|
],
|
|
76
|
-
env: {
|
|
66
|
+
env: {
|
|
67
|
+
...modelWorkPluginEnv(),
|
|
68
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify(modelWorkOpencodeConfig(agentOptions)),
|
|
69
|
+
},
|
|
77
70
|
};
|
|
78
71
|
}
|
|
79
72
|
const args = req.model ? [] : [...profile.args];
|
|
@@ -99,9 +92,6 @@ export const opencodeBuilder = {
|
|
|
99
92
|
}
|
|
100
93
|
args.push("--");
|
|
101
94
|
args.push(req.prompt);
|
|
102
|
-
return {
|
|
103
|
-
argv: [profile.bin, ...args],
|
|
104
|
-
...(modelConfig ? { env: { OPENCODE_CONFIG_CONTENT: JSON.stringify(modelConfig) } } : {}),
|
|
105
|
-
};
|
|
95
|
+
return { argv: [profile.bin, ...args] };
|
|
106
96
|
},
|
|
107
97
|
};
|
|
@@ -1,80 +1,33 @@
|
|
|
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
|
+
/** The inference keys model work's agent carries as options. */
|
|
5
|
+
export const MODEL_WORK_AGENT_INFERENCE = ["temperature", "reasoningEffort", "enableThinking"];
|
|
4
6
|
const isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* is carried as nothing.
|
|
8
|
+
* Split `inference` into opencode config: `entry` for a model (its options, and
|
|
9
|
+
* its limit), and for model work `agentOptions` for the confined agent in place
|
|
10
|
+
* of the entry's options. A value of the wrong type is left out.
|
|
10
11
|
*/
|
|
11
|
-
export function
|
|
12
|
-
const carried = {};
|
|
12
|
+
export function opencodeInferenceConfig(inference, modelWork) {
|
|
13
13
|
const { temperature, reasoningEffort, enableThinking, maxTokens, contextLength } = inference ?? {};
|
|
14
|
+
const options = {};
|
|
14
15
|
if (typeof temperature === "number" && Number.isFinite(temperature))
|
|
15
|
-
|
|
16
|
+
options.temperature = temperature;
|
|
16
17
|
if (typeof reasoningEffort === "string" && reasoningEffort.length > 0)
|
|
17
|
-
|
|
18
|
-
if (typeof enableThinking === "boolean")
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
18
|
+
options.reasoningEffort = reasoningEffort;
|
|
19
|
+
if (typeof enableThinking === "boolean") {
|
|
20
|
+
options.chat_template_kwargs = { enable_thinking: enableThinking };
|
|
21
|
+
options.enable_thinking = enableThinking;
|
|
64
22
|
}
|
|
65
23
|
const hasOptions = Object.keys(options).length > 0;
|
|
66
24
|
return {
|
|
67
25
|
entry: {
|
|
68
26
|
...(hasOptions && !modelWork ? { options } : {}),
|
|
69
|
-
...(
|
|
27
|
+
...(isPositiveInteger(maxTokens) && isPositiveInteger(contextLength)
|
|
28
|
+
? { limit: { context: contextLength, output: maxTokens } }
|
|
29
|
+
: {}),
|
|
70
30
|
},
|
|
71
31
|
...(hasOptions && modelWork ? { agentOptions: options } : {}),
|
|
72
32
|
};
|
|
73
33
|
}
|
|
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
|
-
}
|