akm-cli 0.9.23 → 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.
Files changed (74) hide show
  1. package/CHANGELOG.md +339 -0
  2. package/dist/commands/health/checks.js +10 -11
  3. package/dist/commands/improve/consolidate/pair-pass.js +3 -2
  4. package/dist/commands/improve/consolidate.js +3 -2
  5. package/dist/commands/improve/execution.js +4 -10
  6. package/dist/commands/improve/extract-prompt.js +4 -4
  7. package/dist/commands/improve/extract.js +10 -13
  8. package/dist/commands/improve/improve-cli.js +32 -33
  9. package/dist/commands/improve/improve-strategies.js +49 -43
  10. package/dist/commands/improve/improve-usage-report.js +8 -17
  11. package/dist/commands/improve/preparation.js +3 -1
  12. package/dist/commands/improve/reflect.js +132 -177
  13. package/dist/commands/improve/stage.js +69 -18
  14. package/dist/commands/proposal/drain.js +19 -7
  15. package/dist/commands/proposal/proposal-cli.js +1 -5
  16. package/dist/commands/proposal/propose-cli.js +2 -2
  17. package/dist/commands/proposal/propose.js +80 -83
  18. package/dist/commands/remember.js +3 -3
  19. package/dist/commands/sources/schema-repair.js +1 -1
  20. package/dist/core/config/config-schema.js +37 -60
  21. package/dist/core/config/engine-semantics.js +15 -11
  22. package/dist/core/config/schema/engines.js +33 -15
  23. package/dist/core/config/schema/improve-processes.js +2 -2
  24. package/dist/core/improve-result.js +3 -3
  25. package/dist/core/structured.js +10 -0
  26. package/dist/execution/source.js +14 -0
  27. package/dist/indexer/passes/memory-inference.js +2 -1
  28. package/dist/integrations/agent/builder-shared.js +15 -0
  29. package/dist/integrations/agent/config.js +2 -0
  30. package/dist/integrations/agent/engine-resolution.js +16 -31
  31. package/dist/integrations/agent/execution.js +46 -21
  32. package/dist/integrations/agent/index.js +1 -1
  33. package/dist/integrations/agent/model-map.js +16 -15
  34. package/dist/integrations/agent/prompts.js +53 -76
  35. package/dist/integrations/agent/request-lowering.js +20 -9
  36. package/dist/integrations/agent/runner-dispatch.js +103 -4
  37. package/dist/integrations/agent/runner.js +8 -2
  38. package/dist/integrations/harnesses/aider/agent-builder.js +11 -23
  39. package/dist/integrations/harnesses/aider/index.js +0 -5
  40. package/dist/integrations/harnesses/amazonq/agent-builder.js +10 -21
  41. package/dist/integrations/harnesses/amazonq/index.js +0 -5
  42. package/dist/integrations/harnesses/claude/agent-builder.js +61 -38
  43. package/dist/integrations/harnesses/claude/index.js +0 -14
  44. package/dist/integrations/harnesses/codex/agent-builder.js +4 -4
  45. package/dist/integrations/harnesses/codex/index.js +0 -4
  46. package/dist/integrations/harnesses/copilot/agent-builder.js +4 -13
  47. package/dist/integrations/harnesses/copilot/index.js +2 -7
  48. package/dist/integrations/harnesses/gemini/agent-builder.js +4 -13
  49. package/dist/integrations/harnesses/gemini/index.js +0 -5
  50. package/dist/integrations/harnesses/ids.js +18 -10
  51. package/dist/integrations/harnesses/opencode/agent-builder.js +52 -10
  52. package/dist/integrations/harnesses/opencode/index.js +0 -8
  53. package/dist/integrations/harnesses/opencode/model-config.js +80 -0
  54. package/dist/integrations/harnesses/opencode/model-work-agent.js +80 -0
  55. package/dist/integrations/harnesses/opencode-sdk/harness.js +6 -7
  56. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +168 -24
  57. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -21
  58. package/dist/integrations/harnesses/openhands/index.js +0 -5
  59. package/dist/integrations/harnesses/pi/agent-builder.js +8 -21
  60. package/dist/integrations/harnesses/pi/index.js +0 -5
  61. package/dist/llm/client.js +5 -0
  62. package/dist/llm/feature-gate.js +10 -4
  63. package/dist/llm/index-passes.js +3 -6
  64. package/dist/llm/memory-infer.js +6 -5
  65. package/dist/llm/structured-call.js +33 -11
  66. package/dist/scripts/akm-migrate-node.js +298 -204
  67. package/dist/scripts/akm-migrate.js +298 -204
  68. package/dist/workflows/exec/step-work.js +6 -5
  69. package/dist/workflows/freeze/step-values.js +1 -1
  70. package/docs/reference/cli.md +34 -14
  71. package/docs/reference/configuration.md +171 -12
  72. package/docs/reference/workflow-schema.md +13 -9
  73. package/package.json +1 -1
  74. package/schemas/akm-config.json +36 -0
@@ -39,7 +39,8 @@
39
39
  * enforced at save time via `superRefine` on the top-level schema.
40
40
  */
41
41
  import { z } from "zod";
42
- import { BUILTIN_IMPROVE_STRATEGY_NAMES, IMPROVE_PROCESS_ENGINE_CAPABILITIES } from "./engine-semantics.js";
42
+ import { HARNESS_MODEL_WORK_IDS } from "../../integrations/harnesses/ids.js";
43
+ import { BUILTIN_IMPROVE_STRATEGY_NAMES, IMPROVE_ENGINE_PROCESSES } from "./engine-semantics.js";
43
44
  import { EmbeddingConnectionConfigSchema } from "./schema/embedding.js";
44
45
  import { EnginesSchema } from "./schema/engines.js";
45
46
  import { ExecutionPolicyConfigSchema } from "./schema/execution.js";
@@ -222,13 +223,32 @@ export const AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) =>
222
223
  message: "engine does not name a configured engine",
223
224
  });
224
225
  }
225
- const defaultLlm = config.defaults?.llmEngine;
226
- if (defaultLlm && config.engines?.[defaultLlm]?.kind !== "llm") {
227
- ctx.addIssue({
228
- code: z.ZodIssueCode.custom,
229
- path: ["defaults", "llmEngine"],
230
- message: "llmEngine must name an LLM engine",
231
- });
226
+ // One rule for every key unattended model work reads its engine from: the
227
+ // engine must confine the model-work tool policy (an LLM, or an agent whose
228
+ // harness does), because that work runs over generated content with no one
229
+ // watching.
230
+ const confining = [...HARNESS_MODEL_WORK_IDS];
231
+ const confiningPlatforms = `${confining.slice(0, -1).join(", ")} or ${confining.at(-1)}`;
232
+ const modelWorkEngine = (name, path) => {
233
+ if (!name)
234
+ return;
235
+ const engine = config.engines?.[name];
236
+ if (!engine) {
237
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path, message: "engine does not name a configured engine" });
238
+ }
239
+ else if (engine.kind !== "llm" && !HARNESS_MODEL_WORK_IDS.has(engine.platform)) {
240
+ ctx.addIssue({
241
+ code: z.ZodIssueCode.custom,
242
+ path,
243
+ message: `engine "${name}" (platform ${engine.platform}) cannot confine the model-work tool policy, which unattended model work requires. Use an LLM engine, or an agent engine on ${confiningPlatforms}.`,
244
+ });
245
+ }
246
+ };
247
+ modelWorkEngine(config.defaults?.llmEngine, ["defaults", "llmEngine"]);
248
+ for (const [passName, pass] of Object.entries(config.index ?? {})) {
249
+ const engine = pass?.engine;
250
+ if (typeof engine === "string")
251
+ modelWorkEngine(engine, ["index", passName, "engine"]);
232
252
  }
233
253
  const workflowJudge = config.workflow?.judgeEngine;
234
254
  if (workflowJudge && !config.engines?.[workflowJudge]) {
@@ -256,68 +276,25 @@ export const AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) =>
256
276
  });
257
277
  }
258
278
  for (const [strategyName, strategy] of Object.entries(config.improve?.strategies ?? {})) {
259
- const strategyEngine = strategy.engine;
260
- if (strategyEngine) {
261
- const engine = config.engines?.[strategyEngine];
262
- if (!engine || engine.kind !== "llm") {
263
- ctx.addIssue({
264
- code: z.ZodIssueCode.custom,
265
- path: ["improve", "strategies", strategyName, "engine"],
266
- message: engine ? "strategy engine must be an LLM engine" : "engine does not name a configured engine",
267
- });
268
- }
269
- }
279
+ const strategyPath = ["improve", "strategies", strategyName];
280
+ modelWorkEngine(strategy.engine, [...strategyPath, "engine"]);
270
281
  for (const [processName, process] of Object.entries(strategy.processes ?? {})) {
271
282
  const processConfig = process;
272
- const capability = IMPROVE_PROCESS_ENGINE_CAPABILITIES[processName];
273
- if (processConfig.engine && capability === null) {
283
+ const processPath = [...strategyPath, "processes", processName];
284
+ if (processConfig.engine && !IMPROVE_ENGINE_PROCESSES.includes(processName)) {
274
285
  ctx.addIssue({
275
286
  code: z.ZodIssueCode.custom,
276
- path: ["improve", "strategies", strategyName, "processes", processName, "engine"],
287
+ path: [...processPath, "engine"],
277
288
  message: `${processName} does not dispatch an engine`,
278
289
  });
279
290
  }
280
291
  else {
281
- const processEngine = processConfig.engine ?? strategyEngine;
282
- if (processEngine && capability === "llm") {
283
- const engine = config.engines?.[processEngine];
284
- if (!engine || engine.kind !== "llm") {
285
- ctx.addIssue({
286
- code: z.ZodIssueCode.custom,
287
- path: ["improve", "strategies", strategyName, "processes", processName, "engine"],
288
- message: engine ? `${processName} requires an LLM engine` : "engine does not name a configured engine",
289
- });
290
- }
291
- }
292
- else if (processConfig.engine && capability === "runner" && !config.engines?.[processConfig.engine]) {
293
- ctx.addIssue({
294
- code: z.ZodIssueCode.custom,
295
- path: ["improve", "strategies", strategyName, "processes", processName, "engine"],
296
- message: "engine does not name a configured engine",
297
- });
298
- }
292
+ modelWorkEngine(processConfig.engine, [...processPath, "engine"]);
299
293
  }
300
- const judgmentEngine = processConfig.judgment?.engine;
301
- if (processConfig.judgment?.enabled === true && judgmentEngine) {
302
- const engine = config.engines?.[judgmentEngine];
303
- if (!engine) {
304
- ctx.addIssue({
305
- code: z.ZodIssueCode.custom,
306
- path: ["improve", "strategies", strategyName, "processes", processName, "judgment", "engine"],
307
- message: "engine does not name a configured engine",
308
- });
309
- }
310
- }
311
- const gateEngine = processConfig.qualityGate?.engine;
312
- if (gateEngine && config.engines?.[gateEngine]?.kind !== "llm") {
313
- ctx.addIssue({
314
- code: z.ZodIssueCode.custom,
315
- path: ["improve", "strategies", strategyName, "processes", processName, "qualityGate", "engine"],
316
- message: config.engines?.[gateEngine]
317
- ? "a quality-gate judge must be an LLM engine"
318
- : "engine does not name a configured engine",
319
- });
294
+ if (processConfig.judgment?.enabled === true) {
295
+ modelWorkEngine(processConfig.judgment.engine, [...processPath, "judgment", "engine"]);
320
296
  }
297
+ modelWorkEngine(processConfig.qualityGate?.engine, [...processPath, "qualityGate", "engine"]);
321
298
  }
322
299
  }
323
300
  // #464.a: defaultWriteTarget must name a configured source. 0.9.0 (spec
@@ -11,14 +11,18 @@ export const BUILTIN_IMPROVE_STRATEGY_NAMES = [
11
11
  "reflect-distill",
12
12
  "proactive-maintenance",
13
13
  ];
14
- /** Engine capability required by each configured improve process. `null` means engine-free. */
15
- export const IMPROVE_PROCESS_ENGINE_CAPABILITIES = {
16
- reflect: "llm",
17
- distill: "llm",
18
- consolidate: "llm",
19
- memoryInference: "llm",
20
- extract: "llm",
21
- validation: "llm",
22
- triage: "runner",
23
- proactiveMaintenance: null,
24
- };
14
+ /**
15
+ * The improve processes that use an engine. Triage's engine is its judgment's;
16
+ * each of the others makes the process's own model calls.
17
+ */
18
+ export const IMPROVE_ENGINE_PROCESSES = [
19
+ "reflect",
20
+ "distill",
21
+ "consolidate",
22
+ "memoryInference",
23
+ "extract",
24
+ "validation",
25
+ "triage",
26
+ ];
27
+ /** Every improve process, in plan order: the engine processes and `proactiveMaintenance`, which uses none. */
28
+ export const IMPROVE_PROCESS_NAMES = [...IMPROVE_ENGINE_PROCESSES, "proactiveMaintenance"];
@@ -13,7 +13,7 @@ import { z } from "zod";
13
13
  // `config-types`, which type-derives from this barrel via
14
14
  // `typeof import("./config-schema")` — routing through config-types would mint
15
15
  // a config-schema ↔ config-types type cycle that collapses inference.
16
- import { HARNESS_AGENT_DISPATCH_IDS, VALID_HARNESS_IDS } from "../../../integrations/harnesses/ids.js";
16
+ import { HARNESS_AGENT_DISPATCH_IDS, harnessInferenceKeys, VALID_HARNESS_IDS, } from "../../../integrations/harnesses/ids.js";
17
17
  import { WORKFLOW_MAX_TIMEOUT_MS } from "../../../workflows/resource-limits.js";
18
18
  import { chatCompletionsEndpoint, ExtraParamsSchema, engineName, nonEmptyString, positiveInt, symbolicOrWarnApiKey, } from "./primitives.js";
19
19
  /**
@@ -105,6 +105,20 @@ const LlmEngineSchema = z
105
105
  });
106
106
  }
107
107
  });
108
+ /**
109
+ * The inference fields an agent engine may set: the ones its platform
110
+ * translates (`harnesses/ids.ts`). An asset's or a caller's inference reaches
111
+ * every engine and reports what the engine does not translate as a lowering
112
+ * notice; an engine the operator configures for a platform names only what
113
+ * that platform carries, so the rest is an error here.
114
+ */
115
+ const AGENT_INFERENCE_KEYS = [
116
+ "temperature",
117
+ "maxTokens",
118
+ "contextLength",
119
+ "enableThinking",
120
+ "reasoningEffort",
121
+ ];
108
122
  const AgentEngineSchema = z
109
123
  .object({
110
124
  kind: z.literal("agent"),
@@ -117,26 +131,30 @@ const AgentEngineSchema = z
117
131
  model: nonEmptyString.optional(),
118
132
  timeoutMs: timeoutMsField,
119
133
  llmEngine: engineName.optional(),
134
+ temperature: z.number().finite().optional(),
135
+ maxTokens: positiveInt.optional(),
136
+ contextLength: positiveInt.optional(),
137
+ enableThinking: z.boolean().optional(),
138
+ reasoningEffort: nonEmptyString.optional(),
120
139
  })
121
140
  .passthrough()
122
141
  .superRefine((value, ctx) => {
123
- for (const key of [
124
- "provider",
125
- "endpoint",
126
- "apiKey",
127
- "apiKeyFile",
128
- "temperature",
129
- "maxTokens",
130
- "concurrency",
131
- "extraParams",
132
- "contextLength",
133
- "enableThinking",
134
- "reasoningEffort",
135
- "modelAliases",
136
- ]) {
142
+ for (const key of ["provider", "endpoint", "apiKey", "apiKeyFile", "concurrency", "extraParams", "modelAliases"]) {
137
143
  if (key in value)
138
144
  ctx.addIssue({ code: z.ZodIssueCode.custom, path: [key], message: `${key} is not valid on an agent engine` });
139
145
  }
146
+ const translated = harnessInferenceKeys(value.platform);
147
+ for (const key of AGENT_INFERENCE_KEYS) {
148
+ if (key in value && !translated.includes(key)) {
149
+ ctx.addIssue({
150
+ code: z.ZodIssueCode.custom,
151
+ path: [key],
152
+ message: `${key} is not valid on a ${value.platform} engine: ${translated.length > 0
153
+ ? `the platform translates only ${translated.join(", ")}`
154
+ : "the platform translates no inference fields"}`,
155
+ });
156
+ }
157
+ }
140
158
  if (value.platform !== "opencode-sdk" && value.llmEngine !== undefined) {
141
159
  ctx.addIssue({
142
160
  code: z.ZodIssueCode.custom,
@@ -6,7 +6,7 @@
6
6
  * verbatim from the former `config-schema.ts` monolith — no behavior change.
7
7
  */
8
8
  import { z } from "zod";
9
- import { IMPROVE_PROCESS_ENGINE_CAPABILITIES } from "../engine-semantics.js";
9
+ import { IMPROVE_PROCESS_NAMES } from "../engine-semantics.js";
10
10
  import { engineName, LlmInvocationOverridesSchema, nonEmptyString, positiveInt } from "./primitives.js";
11
11
  // ── Improve profile / process ──────────────────────────────────────────────
12
12
  //
@@ -319,7 +319,7 @@ const ImproveProfileProcessesSchema = z
319
319
  });
320
320
  }
321
321
  for (const [name, process] of Object.entries(val)) {
322
- if (!(name in IMPROVE_PROCESS_ENGINE_CAPABILITIES) &&
322
+ if (!IMPROVE_PROCESS_NAMES.includes(name) &&
323
323
  !RETIRED_PROCESS_NAMES.has(name) &&
324
324
  process !== null &&
325
325
  typeof process === "object" &&
@@ -3,7 +3,7 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { cloneExecutionJsonObject } from "../execution/json.js";
5
5
  import { isRecord } from "./common.js";
6
- import { IMPROVE_PROCESS_ENGINE_CAPABILITIES } from "./config/engine-semantics.js";
6
+ import { IMPROVE_PROCESS_NAMES } from "./config/engine-semantics.js";
7
7
  const COMMON_FIELDS = [
8
8
  "schemaVersion",
9
9
  "ok",
@@ -192,11 +192,11 @@ function validateProactivePlan(value) {
192
192
  fail("plan.proactive.selected must equal plan.proactive.selectedRefs.length");
193
193
  }
194
194
  }
195
- /** #947 — plan.processes: one row per IMPROVE_PROCESS_ENGINE_CAPABILITIES name, plus an optional "triage.judgment" row. */
195
+ /** #947 — plan.processes: one row per IMPROVE_PROCESS_NAMES name, plus an optional "triage.judgment" row. */
196
196
  function validateProcessRoutingRows(value) {
197
197
  if (!Array.isArray(value))
198
198
  fail("plan.processes must be an array");
199
- const canonicalNames = Object.keys(IMPROVE_PROCESS_ENGINE_CAPABILITIES);
199
+ const canonicalNames = IMPROVE_PROCESS_NAMES;
200
200
  const engineKinds = new Set(["llm", "agent", "sdk"]);
201
201
  const seen = new Set();
202
202
  for (const row of value) {
@@ -24,6 +24,16 @@
24
24
  * jittered retry; `runAgent` has timeout/abort semantics).
25
25
  */
26
26
  import { parseEmbeddedJsonResponse } from "./parse.js";
27
+ /**
28
+ * Append the one structured-output instruction to a prompt. Agent lowering
29
+ * appends it to every schema-bearing request, alongside a harness's native
30
+ * channel where one exists (codex `--output-schema`); the workflow engine
31
+ * appends it for a direct-LLM unit. Resumed workflow runs depend on these
32
+ * exact bytes, so do not reword it.
33
+ */
34
+ export function withSchemaInstruction(prompt, schema) {
35
+ return `${prompt}\n\nRespond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(schema)}`;
36
+ }
27
37
  function defaultFeedback(failure) {
28
38
  if (failure.reason === "parse_error") {
29
39
  return "Your previous response contained no parseable JSON. Respond with ONLY a JSON value that matches the requested schema — no prose, no code fences.";
@@ -5,6 +5,20 @@ import { cloneExecutionJson, cloneExecutionJsonObject } from "./json.js";
5
5
  export const EXECUTION_SOURCE_SCHEMA_VERSION = 1;
6
6
  /** Current internal adapter identifiers are lowercase kebab-case registry keys. */
7
7
  export const EXECUTION_ADAPTER_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
8
+ /**
9
+ * The model-work tool policy: unattended model work (improve, the judges,
10
+ * index passes, remember) may read, edit only inside the dispatch's own
11
+ * scratch working directory, and run `akm search` and `akm show`. The stash
12
+ * stays read-only to it. A transport grants what it can confine and refuses
13
+ * the policy at build when it can confine nothing; an LLM has no tools.
14
+ */
15
+ export const MODEL_WORK_TOOLS = Object.freeze(["read", "edit", "akm search", "akm show"]);
16
+ /** Whether a selection names the model-work tool policy. */
17
+ export function isModelWorkTools(tools) {
18
+ return (Array.isArray(tools) &&
19
+ tools.length === MODEL_WORK_TOOLS.length &&
20
+ tools.every((tool, index) => tool === MODEL_WORK_TOOLS[index]));
21
+ }
8
22
  function requireRecord(value, path) {
9
23
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
10
24
  throw new TypeError(`${path} must be an object`);
@@ -48,6 +48,7 @@ import { ConfigError } from "../../core/errors.js";
48
48
  import { warn } from "../../core/warn.js";
49
49
  import { beginWriteProvenance, recordWrittenPath } from "../../core/write-provenance.js";
50
50
  import { writeAssetToSource } from "../../core/write-source.js";
51
+ import { runnerLlmConnection } from "../../integrations/agent/runner.js";
51
52
  import { assertRunnerCredentials } from "../../integrations/agent/runner-dispatch.js";
52
53
  import { isProcessEnabled } from "../../llm/feature-gate.js";
53
54
  import { resolveIndexPassExecution } from "../../llm/index-passes.js";
@@ -283,7 +284,7 @@ async function runMemoryInferencePassBody(ctx, provenance) {
283
284
  }),
284
285
  // Caller-set connection concurrency or 1: `resolveLlmEngineUse` does
285
286
  // not forward `engines.<name>.concurrency`, so config cannot raise this.
286
- llmRunner.connection.concurrency ?? 1);
287
+ runnerLlmConnection(llmRunner)?.concurrency ?? 1);
287
288
  if (configFailure)
288
289
  throw configFailure;
289
290
  for (let i = 0; i < perRecordResults.length; i++) {
@@ -5,6 +5,21 @@
5
5
  export function resolveDispatchModel(request, _profile, _platform) {
6
6
  return request.model;
7
7
  }
8
+ /**
9
+ * The model an engine's own `args` select, as `--model X` or `--model=X` (the
10
+ * last one wins), for a builder that writes its own argv in place of those args.
11
+ */
12
+ export function modelFromArgs(args) {
13
+ let model;
14
+ for (let index = 0; index < args.length; index += 1) {
15
+ const arg = args[index];
16
+ if (arg === "--model")
17
+ model = args[index + 1];
18
+ else if (arg?.startsWith("--model="))
19
+ model = arg.slice("--model=".length);
20
+ }
21
+ return model;
22
+ }
8
23
  /**
9
24
  * Normalize a toolPolicy value to a comma-separated string suitable for a
10
25
  * CLI flag. Structured policy objects are JSON-serialized.
@@ -5,3 +5,5 @@
5
5
  export const DEFAULT_AGENT_TIMEOUT_MS = null;
6
6
  /** Default hard timeout for direct LLM calls when no engine/use override exists. */
7
7
  export const DEFAULT_LLM_TIMEOUT_MS = 600_000;
8
+ /** Default bound on model work (structured calls, judgments) on every runner kind. */
9
+ export const DEFAULT_MODEL_WORK_TIMEOUT_MS = 600_000;
@@ -10,10 +10,10 @@ import { SECRET_STORE_REFERENCE_PATTERN } from "../../core/config/schema/primiti
10
10
  import { ConfigError } from "../../core/errors.js";
11
11
  import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
12
12
  import { collectSensitiveValues } from "../../core/redaction.js";
13
- import { warn } from "../../core/warn.js";
14
13
  import { resolveSecretFromStore } from "../../sources/snapshot-fetchers/secret-seam.js";
15
14
  import { getHarness } from "../harnesses/index.js";
16
- import { DEFAULT_AGENT_TIMEOUT_MS, DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
15
+ import { DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
16
+ import { engineModelAndInference } from "./model-map.js";
17
17
  import { getBuiltinAgentProfile, OPENCODE_SDK_SERVER_BIN } from "./profiles.js";
18
18
  const LLM_CONNECTION_FIELDS = [
19
19
  "provider",
@@ -217,29 +217,15 @@ function rawLlmConnection(engine) {
217
217
  }
218
218
  return connection;
219
219
  }
220
- export function resolveLlmEngineUse(config, layers, options = {}) {
220
+ /** Resolve one selected LLM engine and overlays without materializing credentials. */
221
+ export function resolveLlmEngineUse(config, layers) {
221
222
  const name = selectedEngineName(config, layers, true);
222
223
  if (!name) {
223
- if (options.optional)
224
- return undefined;
225
224
  throw new ConfigError("No LLM engine is selected. Set defaults.llmEngine or specify engine.", "LLM_NOT_CONFIGURED");
226
225
  }
227
226
  const engine = configuredEngine(name, config);
228
- if (engine.kind !== "llm") {
229
- const fallbackName = engine.llmEngine ?? config.defaults?.llmEngine;
230
- const fallbackEngine = fallbackName ? configuredEngine(fallbackName, config) : undefined;
231
- if (!fallbackEngine || fallbackEngine.kind !== "llm") {
232
- if (options.optional)
233
- return undefined;
234
- throw new ConfigError(fallbackName
235
- ? `Engine "${name}" is not an LLM engine, and its llmEngine fallback "${fallbackName}" is not one either.`
236
- : `Engine "${name}" is not an LLM engine, and has no llmEngine fallback configured.`, "INVALID_CONFIG_FILE");
237
- }
238
- warn(`[akm] Engine "${name}" is an agent engine, not an LLM engine; using its llmEngine "${fallbackName}" instead.`);
239
- return options.optional
240
- ? resolveLlmEngineUse(config, [{ engine: fallbackName }], { optional: true })
241
- : resolveLlmEngineUse(config, [{ engine: fallbackName }]);
242
- }
227
+ if (engine.kind !== "llm")
228
+ throw new ConfigError(`Engine "${name}" is not an LLM engine.`, "INVALID_CONFIG_FILE");
243
229
  let connection = rawLlmConnection(engine);
244
230
  for (const layer of layers) {
245
231
  if (layer.llm)
@@ -288,6 +274,7 @@ function lowerAgentEngine(name, engine, config) {
288
274
  const platform = harness.id;
289
275
  const sdk = platform === "opencode-sdk";
290
276
  const builtin = getBuiltinAgentProfile(platform);
277
+ const { inference } = engineModelAndInference(engine);
291
278
  const profile = {
292
279
  name,
293
280
  platform,
@@ -300,20 +287,18 @@ function lowerAgentEngine(name, engine, config) {
300
287
  parseOutput: "text",
301
288
  ...(engine.workspace ? { workspace: path.resolve(engine.workspace) } : {}),
302
289
  ...(engine.model ? { model: engine.model } : {}),
290
+ ...(inference ? { inference } : {}),
303
291
  };
292
+ // An engine that sets no timeoutMs leaves it unset, so a caller's own default
293
+ // (model work's 600 s) can apply; a dispatch with none runs unbounded.
304
294
  const ownTimeout = Object.hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : undefined;
305
295
  if (!sdk) {
306
- return {
307
- kind: "agent",
308
- engine: name,
309
- profile,
310
- timeoutMs: ownTimeout !== undefined ? ownTimeout : DEFAULT_AGENT_TIMEOUT_MS,
311
- };
296
+ return { kind: "agent", engine: name, profile, ...(ownTimeout !== undefined ? { timeoutMs: ownTimeout } : {}) };
312
297
  }
313
- const fallbackName = engine.llmEngine ?? config.defaults?.llmEngine;
314
- const fallback = fallbackName
315
- ? resolveLlmEngineUse(config, [{ engine: fallbackName }], { optional: true })
316
- : undefined;
298
+ // The fallback connection is the engine's own `llmEngine` and nothing else. `defaults.llmEngine`
299
+ // is the default engine for model work, not a connection every SDK engine borrows: with no
300
+ // `llmEngine`, opencode resolves provider, model and auth from its own configuration.
301
+ const fallback = engine.llmEngine ? resolveLlmEngineUse(config, [{ engine: engine.llmEngine }]) : undefined;
317
302
  return {
318
303
  kind: "sdk",
319
304
  engine: name,
@@ -327,7 +312,7 @@ function lowerAgentEngine(name, engine, config) {
327
312
  fallbackTimeoutMs: fallback.timeoutMs,
328
313
  }
329
314
  : {}),
330
- timeoutMs: ownTimeout !== undefined ? ownTimeout : (fallback?.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS),
315
+ ...(ownTimeout !== undefined ? { timeoutMs: ownTimeout } : fallback ? { timeoutMs: fallback.timeoutMs } : {}),
331
316
  };
332
317
  }
333
318
  /** Resolve a configured engine name to its runner: an LLM connection, a spawned agent, or the SDK. */
@@ -6,9 +6,9 @@ import { ConfigError } from "../../core/errors.js";
6
6
  import { DURATION_UNITS, parseDuration } from "../../core/time.js";
7
7
  import { EXECUTION_MAX_TIMEOUT_MS } from "../../execution/limits.js";
8
8
  import { createInlineResolvedCommand, createResolvedExecutionRequest, decodeResolvedExecutionRequest, } from "../../execution/resolved-request.js";
9
- import { cloneToolSelection, isPortableExecutionAgentSelector, } from "../../execution/source.js";
9
+ import { cloneToolSelection, isModelWorkTools, isPortableExecutionAgentSelector, } from "../../execution/source.js";
10
10
  import { getHarness } from "../harnesses/index.js";
11
- import { DEFAULT_AGENT_TIMEOUT_MS, DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
11
+ import { DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
12
12
  import { FALLBACK_ENGINE_NAME, fallbackEngineConfig, NO_ENGINE_MESSAGE_SUFFIX, NO_ENGINE_REMEDY, } from "./engine-fallback.js";
13
13
  import { configuredEngine, resolveEngine } from "./engine-resolution.js";
14
14
  import { engineModelAndInference, loadModelMap, resolveModelMapAlias } from "./model-map.js";
@@ -62,21 +62,14 @@ function engineDefaults(name, engine, config) {
62
62
  if (engine.platform !== "opencode-sdk") {
63
63
  return { kind: "agent", platform: engine.platform, modelMapKey: engine.platform, values };
64
64
  }
65
- // An SDK engine runs its LLM fallback's model/inference/timeout unless it sets its own.
66
- const fallbackName = engine.llmEngine ?? config.defaults?.llmEngine;
65
+ // An SDK engine runs its own `llmEngine`'s model/inference/timeout unless it sets its own. With no
66
+ // `llmEngine` it has no fallback, and opencode picks the model: `defaults.llmEngine` is not borrowed.
67
+ const fallbackName = engine.llmEngine;
67
68
  const fallback = fallbackName && config.engines && Object.hasOwn(config.engines, fallbackName)
68
69
  ? config.engines[fallbackName]
69
70
  : undefined;
70
71
  if (fallback?.kind !== "llm" || !fallbackName) {
71
- return {
72
- kind: "sdk",
73
- platform: "opencode-sdk",
74
- modelMapKey: "opencode-sdk",
75
- values: {
76
- ...values,
77
- timeout: has(values, "timeout") ? values.timeout : DEFAULT_AGENT_TIMEOUT_MS,
78
- },
79
- };
72
+ return { kind: "sdk", platform: "opencode-sdk", modelMapKey: "opencode-sdk", values };
80
73
  }
81
74
  const inherited = engineModelAndInference(fallback);
82
75
  return {
@@ -85,8 +78,11 @@ function engineDefaults(name, engine, config) {
85
78
  modelMapKey: own.model === undefined ? fallbackName : "opencode-sdk",
86
79
  values: {
87
80
  ...(inherited.model !== undefined ? { model: inherited.model } : {}),
88
- ...(inherited.inference !== undefined ? { inference: inherited.inference } : {}),
89
81
  ...values,
82
+ // The engine's own inference is added over the fallback's, field by field.
83
+ ...(inherited.inference !== undefined || own.inference !== undefined
84
+ ? { inference: { ...inherited.inference, ...own.inference } }
85
+ : {}),
90
86
  timeout: Object.hasOwn(engine, "timeoutMs")
91
87
  ? (engine.timeoutMs ?? null)
92
88
  : Object.hasOwn(fallback, "timeoutMs")
@@ -114,7 +110,10 @@ function runnerDefaults(runner) {
114
110
  const platform = runner.profile.platform ?? runner.profile.name;
115
111
  const fallback = runner.kind === "sdk" ? runner.fallbackConnection : undefined;
116
112
  const model = runner.profile.model ?? fallback?.model;
117
- const inference = fallback ? inferenceOf(fallback) : undefined;
113
+ const fallbackInference = fallback ? inferenceOf(fallback) : undefined;
114
+ const inference = fallbackInference !== undefined || runner.profile.inference !== undefined
115
+ ? { ...fallbackInference, ...runner.profile.inference }
116
+ : undefined;
118
117
  return {
119
118
  kind: runner.kind,
120
119
  platform,
@@ -187,10 +186,17 @@ function requestedToolNames(tools) {
187
186
  return undefined;
188
187
  return Object.keys(policy).filter((tool) => policy[tool] === true);
189
188
  }
190
- /** Assets may only narrow the host's `execution.allowedTools`; without a config nothing is allowed. */
189
+ /**
190
+ * Assets may only narrow the host's `execution.allowedTools`; without a config
191
+ * nothing is allowed. The model-work policy is akm's own and always allowed:
192
+ * it confines an engine more tightly than leaving tools unset does.
193
+ */
191
194
  function authorizeTools(tools, config) {
192
195
  if (!hasToolSelection(tools))
193
196
  return { status: "not-required" };
197
+ if (isModelWorkTools(tools)) {
198
+ return { status: "allowed", reason: "The model-work tool policy is akm's own.", policy: { id: "model-work" } };
199
+ }
194
200
  if (!config) {
195
201
  return {
196
202
  status: "denied",
@@ -239,14 +245,16 @@ function applyRequest(base, request) {
239
245
  ...timeout,
240
246
  };
241
247
  }
242
- const { model: _model, workspace: _workspace, ...profile } = base.profile;
248
+ const { model: _model, workspace: _workspace, inference: ownInference, ...profile } = base.profile;
243
249
  const workspace = request.runtime.workspace;
250
+ const inference = Object.hasOwn(request, "inference") ? request.inference : ownInference;
244
251
  const next = {
245
252
  ...base,
246
253
  profile: {
247
254
  ...profile,
248
255
  ...(model !== undefined ? { model } : {}),
249
256
  ...(typeof workspace === "string" ? { workspace } : {}),
257
+ ...(inference ? { inference } : {}),
250
258
  },
251
259
  ...timeout,
252
260
  };
@@ -260,14 +268,29 @@ function applyRequest(base, request) {
260
268
  }
261
269
  return next;
262
270
  }
263
- function mergeInference(current, next, source, provenance) {
271
+ /**
272
+ * Reasoning effort has one word in a request: `reasoningEffort`, which is what
273
+ * engines, opencode and the LLM request body call it. `effort` is the same
274
+ * setting as a `models.json` alias or an asset's `effort:` frontmatter spells
275
+ * it, and becomes `reasoningEffort` here, in the one place every layer's
276
+ * inference is merged, so the nearest layer wins whichever word it used. When
277
+ * one inference object has both, `reasoningEffort` wins.
278
+ */
279
+ function withReasoningEffort(inference) {
280
+ if (!Object.hasOwn(inference, "effort"))
281
+ return inference;
282
+ const { effort, ...rest } = inference;
283
+ return Object.hasOwn(rest, "reasoningEffort") ? rest : { ...rest, reasoningEffort: effort };
284
+ }
285
+ function mergeInference(current, layerInference, source, provenance) {
264
286
  provenance["/inference"] = source;
265
- if (next === null) {
287
+ if (layerInference === null) {
266
288
  for (const key of Object.keys(provenance))
267
289
  if (key.startsWith("/inference/"))
268
290
  delete provenance[key];
269
291
  return null;
270
292
  }
293
+ const next = withReasoningEffort(layerInference);
271
294
  for (const key of Object.keys(next)) {
272
295
  provenance[`/inference/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`] = source;
273
296
  }
@@ -415,7 +438,8 @@ function buildLlm(request, runner, options) {
415
438
  if (typeof request.agent === "string" && request.persona === null) {
416
439
  throw new ConfigError(`The direct LLM transport cannot consume native agent selector ${JSON.stringify(request.agent)}.`, "INVALID_CONFIG_FILE");
417
440
  }
418
- if (hasToolSelection(request.tools)) {
441
+ // An LLM has no tools, which already meets the model-work policy.
442
+ if (hasToolSelection(request.tools) && !isModelWorkTools(request.tools)) {
419
443
  throw new ConfigError("The direct LLM transport cannot enforce the resolved tool policy.", "INVALID_CONFIG_FILE");
420
444
  }
421
445
  const notices = [...request.notices];
@@ -427,8 +451,9 @@ function buildLlm(request, runner, options) {
427
451
  skip(`inference.${key}`);
428
452
  }
429
453
  const chatOptions = {};
454
+ // Sent unless the engine opts out; chatCompletion drops it once if the provider rejects it.
430
455
  if (request.outputSchema) {
431
- if (runner.connection.supportsJsonSchema === true)
456
+ if (runner.connection.supportsJsonSchema !== false)
432
457
  chatOptions.responseSchema = request.outputSchema;
433
458
  else
434
459
  skip("outputSchema");
@@ -4,4 +4,4 @@
4
4
  export { DEFAULT_AGENT_TIMEOUT_MS } from "./config.js";
5
5
  export { _setAgentDetectForTests, defaultWhich, detectAgentCliProfiles, pickDefaultAgentProfile } from "./detect.js";
6
6
  export { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile, listBuiltinAgentProfiles, } from "./profiles.js";
7
- export { buildProposePrompt, buildReflectPrompt, buildSchemaRepairPrompt, extractDraftConfidence, parseAgentProposalPayload, } from "./prompts.js";
7
+ export { buildProposePrompt, buildReflectPrompt, buildSchemaRepairPrompt, parseAgentProposalPayload, } from "./prompts.js";