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.
Files changed (68) hide show
  1. package/CHANGELOG.md +243 -280
  2. package/dist/assets/prompts/reflect-feedback-framing.md +1 -1
  3. package/dist/assets/prompts/reflect-llm-framed-contract.md +2 -9
  4. package/dist/assets/prompts/reflect-llm-schema-contract.md +1 -3
  5. package/dist/assets/prompts/reflect-output-repair.md +1 -1
  6. package/dist/cli.js +1 -1
  7. package/dist/commands/improve/consolidate/pair-pass.js +1 -0
  8. package/dist/commands/improve/consolidate.js +7 -2
  9. package/dist/commands/improve/execution.js +2 -3
  10. package/dist/commands/improve/extract-cli.js +3 -2
  11. package/dist/commands/improve/extract.js +2 -1
  12. package/dist/commands/improve/improve-cli.js +33 -1
  13. package/dist/commands/improve/loop-stages.js +3 -0
  14. package/dist/commands/improve/reflect-noise.js +125 -0
  15. package/dist/commands/improve/reflect.js +150 -333
  16. package/dist/commands/improve/retrieval-gate.js +7 -2
  17. package/dist/commands/improve/session-asset.js +6 -0
  18. package/dist/commands/improve/stage.js +31 -39
  19. package/dist/commands/proposal/drain.js +4 -7
  20. package/dist/commands/proposal/propose.js +2 -11
  21. package/dist/commands/proposal/validators/proposal-quality-validators.js +11 -5
  22. package/dist/commands/proposal/validators/proposal-validators.js +4 -5
  23. package/dist/commands/read/search-cli.js +0 -38
  24. package/dist/core/asset/asset-serialize.js +1 -1
  25. package/dist/core/config/schema/engines.js +15 -33
  26. package/dist/core/config/schema/improve-processes.js +16 -0
  27. package/dist/core/content-safety.js +0 -24
  28. package/dist/core/redaction.js +4 -0
  29. package/dist/core/spawn-env.js +25 -0
  30. package/dist/core/structured.js +1 -1
  31. package/dist/execution/source.js +8 -12
  32. package/dist/integrations/agent/config.js +1 -3
  33. package/dist/integrations/agent/engine-resolution.js +0 -3
  34. package/dist/integrations/agent/execution.js +14 -13
  35. package/dist/integrations/agent/index.js +1 -1
  36. package/dist/integrations/agent/model-map.js +15 -16
  37. package/dist/integrations/agent/profiles.js +2 -2
  38. package/dist/integrations/agent/prompts.js +51 -127
  39. package/dist/integrations/agent/request-lowering.js +9 -7
  40. package/dist/integrations/agent/runner-dispatch.js +25 -31
  41. package/dist/integrations/harnesses/aider/agent-builder.js +1 -2
  42. package/dist/integrations/harnesses/amazonq/agent-builder.js +1 -2
  43. package/dist/integrations/harnesses/claude/agent-builder.js +4 -16
  44. package/dist/integrations/harnesses/codex/agent-builder.js +1 -2
  45. package/dist/integrations/harnesses/codex/index.js +6 -11
  46. package/dist/integrations/harnesses/codex/session-log.js +211 -0
  47. package/dist/integrations/harnesses/ids.js +10 -16
  48. package/dist/integrations/harnesses/opencode/agent-builder.js +14 -24
  49. package/dist/integrations/harnesses/opencode/model-config.js +15 -62
  50. package/dist/integrations/harnesses/opencode/model-work-agent.js +71 -36
  51. package/dist/integrations/harnesses/opencode-sdk/harness.js +2 -6
  52. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +32 -90
  53. package/dist/integrations/harnesses/openhands/agent-builder.js +1 -2
  54. package/dist/integrations/harnesses/pi/agent-builder.js +1 -2
  55. package/dist/integrations/harnesses/types.js +3 -3
  56. package/dist/llm/feature-gate.js +2 -5
  57. package/dist/llm/index-passes.js +2 -2
  58. package/dist/llm/structured-call.js +5 -5
  59. package/dist/output/shapes/passthrough.js +1 -0
  60. package/dist/scripts/akm-migrate-node.js +381 -239
  61. package/dist/scripts/akm-migrate.js +381 -239
  62. package/dist/workflows/exec/unit-dispatch.js +4 -13
  63. package/docs/reference/cli.md +29 -16
  64. package/docs/reference/configuration.md +79 -85
  65. package/docs/reference/data-and-telemetry.md +2 -3
  66. package/docs/reference/workflow-schema.md +6 -9
  67. package/package.json +1 -1
  68. 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>] [--effort <level>]
49
- * [--allowedTools <t>] [--output-format json] --print -- "<prompt>"
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 = isModelWorkTools(req.tools);
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
- const effort = req.inference?.reasoningEffort;
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 (`inference` in `harnesses/ids.ts` lists none
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, 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: [] },
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 { opencodeCarriedKeys, opencodeInferenceConfig, opencodeModelConfig, splitOpencodeModel } from "./model-config.js";
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
- * 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.
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
- 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
- },
50
+ inference: MODEL_WORK_AGENT_INFERENCE,
58
51
  }),
59
52
  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) {
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: { OPENCODE_CONFIG_CONTENT: JSON.stringify({ ...modelWorkOpencodeConfig(agentOptions), ...modelConfig }) },
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
- * 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.
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 carriedInference(inference) {
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
- carried.temperature = temperature;
16
+ options.temperature = temperature;
16
17
  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;
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
- ...(carried.limit ? { limit: { ...carried.limit } } : {}),
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
- }