akm-cli 0.9.24 → 0.9.25-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +294 -0
- package/dist/commands/health/checks.js +10 -11
- package/dist/commands/improve/consolidate/pair-pass.js +3 -2
- package/dist/commands/improve/consolidate.js +3 -2
- package/dist/commands/improve/execution.js +4 -10
- package/dist/commands/improve/extract-prompt.js +4 -4
- package/dist/commands/improve/extract.js +10 -13
- package/dist/commands/improve/improve-cli.js +32 -33
- package/dist/commands/improve/improve-strategies.js +49 -43
- package/dist/commands/improve/improve-usage-report.js +8 -17
- package/dist/commands/improve/preparation.js +3 -1
- package/dist/commands/improve/reflect.js +108 -172
- package/dist/commands/improve/stage.js +69 -18
- package/dist/commands/proposal/drain.js +14 -2
- package/dist/commands/proposal/proposal-cli.js +1 -5
- package/dist/commands/proposal/propose-cli.js +2 -2
- package/dist/commands/proposal/propose.js +80 -83
- package/dist/commands/remember.js +3 -3
- package/dist/commands/sources/schema-repair.js +1 -1
- package/dist/core/config/config-schema.js +37 -60
- package/dist/core/config/engine-semantics.js +15 -11
- package/dist/core/config/schema/engines.js +33 -15
- package/dist/core/config/schema/improve-processes.js +2 -2
- package/dist/core/improve-result.js +3 -3
- package/dist/core/structured.js +10 -0
- package/dist/execution/source.js +14 -0
- package/dist/indexer/passes/memory-inference.js +2 -1
- package/dist/integrations/agent/builder-shared.js +15 -0
- package/dist/integrations/agent/config.js +2 -0
- package/dist/integrations/agent/engine-resolution.js +16 -31
- package/dist/integrations/agent/execution.js +46 -21
- package/dist/integrations/agent/index.js +1 -1
- package/dist/integrations/agent/model-map.js +16 -15
- package/dist/integrations/agent/prompts.js +53 -76
- package/dist/integrations/agent/request-lowering.js +20 -9
- package/dist/integrations/agent/runner-dispatch.js +103 -4
- package/dist/integrations/agent/runner.js +8 -2
- package/dist/integrations/harnesses/aider/agent-builder.js +11 -23
- package/dist/integrations/harnesses/aider/index.js +0 -5
- package/dist/integrations/harnesses/amazonq/agent-builder.js +10 -21
- package/dist/integrations/harnesses/amazonq/index.js +0 -5
- package/dist/integrations/harnesses/claude/agent-builder.js +61 -38
- package/dist/integrations/harnesses/claude/index.js +0 -14
- package/dist/integrations/harnesses/codex/agent-builder.js +4 -4
- package/dist/integrations/harnesses/codex/index.js +0 -4
- package/dist/integrations/harnesses/copilot/agent-builder.js +4 -13
- package/dist/integrations/harnesses/copilot/index.js +2 -7
- package/dist/integrations/harnesses/gemini/agent-builder.js +4 -13
- package/dist/integrations/harnesses/gemini/index.js +0 -5
- package/dist/integrations/harnesses/ids.js +18 -10
- package/dist/integrations/harnesses/opencode/agent-builder.js +52 -10
- package/dist/integrations/harnesses/opencode/index.js +0 -8
- package/dist/integrations/harnesses/opencode/model-config.js +80 -0
- package/dist/integrations/harnesses/opencode/model-work-agent.js +80 -0
- package/dist/integrations/harnesses/opencode-sdk/harness.js +6 -7
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +168 -24
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -21
- package/dist/integrations/harnesses/openhands/index.js +0 -5
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -21
- package/dist/integrations/harnesses/pi/index.js +0 -5
- package/dist/llm/client.js +5 -0
- package/dist/llm/feature-gate.js +10 -4
- package/dist/llm/index-passes.js +3 -6
- package/dist/llm/memory-infer.js +6 -5
- package/dist/llm/structured-call.js +33 -11
- package/dist/scripts/akm-migrate-node.js +298 -204
- package/dist/scripts/akm-migrate.js +298 -204
- package/dist/workflows/exec/step-work.js +6 -5
- package/dist/workflows/freeze/step-values.js +1 -1
- package/docs/reference/cli.md +29 -11
- package/docs/reference/configuration.md +168 -11
- package/docs/reference/workflow-schema.md +13 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +36 -0
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { getImproveProcessConfig } from "../../core/config/config.js";
|
|
5
5
|
import { ConfigError } from "../../core/errors.js";
|
|
6
|
+
import { validateJsonSchemaSubset } from "../../core/json-schema.js";
|
|
6
7
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
8
|
+
import { runStructured } from "../../core/structured.js";
|
|
7
9
|
import { warn } from "../../core/warn.js";
|
|
8
|
-
import {
|
|
9
|
-
import { callStructured } from "../../llm/structured-call.js";
|
|
10
|
+
import { runnerLlmConnection } from "../../integrations/agent/runner.js";
|
|
11
|
+
import { callStructured, dispatchFailureReason, dispatchFailureResult, } from "../../llm/structured-call.js";
|
|
10
12
|
import { currentLlmStage, withLlmStage } from "../../llm/usage-telemetry.js";
|
|
11
13
|
import { isProceduralRejection } from "../proposal/proposal-types.js";
|
|
12
14
|
import { createProposal, listProposalsReadOnly, proposalContentHash, recordGateDecision, } from "../proposal/repository.js";
|
|
13
|
-
import {
|
|
15
|
+
import { resolveImproveExecution } from "./execution.js";
|
|
14
16
|
/** Normalize an unknown thrown value to a message. */
|
|
15
17
|
export function errMessage(e) {
|
|
16
18
|
return e instanceof Error ? e.message : String(e);
|
|
@@ -27,13 +29,13 @@ export function noticeSet(forward) {
|
|
|
27
29
|
return { add, list, fields: () => (byKey.size > 0 ? { notices: list() } : {}) };
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
|
-
* A stage's
|
|
32
|
+
* A stage's runner: the one the improve plan froze for it (an own
|
|
31
33
|
* `llmRunner` key, `null` meaning "none"), else the process engine cascade.
|
|
32
34
|
*/
|
|
33
35
|
export function stageRunner(frozen, config, profile, processName, onNotices) {
|
|
34
36
|
if (Object.hasOwn(frozen, "llmRunner"))
|
|
35
37
|
return frozen.llmRunner ?? undefined;
|
|
36
|
-
const resolved =
|
|
38
|
+
const resolved = resolveImproveExecution({
|
|
37
39
|
config,
|
|
38
40
|
profile,
|
|
39
41
|
process: getImproveProcessConfig(processName, profile),
|
|
@@ -43,11 +45,59 @@ export function stageRunner(frozen, config, profile, processName, onNotices) {
|
|
|
43
45
|
onNotices?.(resolved.notices);
|
|
44
46
|
return resolved?.runner;
|
|
45
47
|
}
|
|
48
|
+
const TRANSPORT_FAILED = Symbol("stage-transport-failed");
|
|
46
49
|
/**
|
|
47
|
-
* One model call. Provider trouble (transport error, timeout, a
|
|
48
|
-
* feature) comes back as `{ ok: false }`; only a configuration
|
|
50
|
+
* One model call. Provider trouble (transport error, timeout, abort, a
|
|
51
|
+
* disabled feature) comes back as `{ ok: false }`; only a configuration
|
|
52
|
+
* failure throws. A reply to a call with `request.responseSchema` that fails
|
|
53
|
+
* the schema gets one corrective retry. The caller's own parse still decides
|
|
54
|
+
* what it accepts, so the last reply comes back even when it fails the schema.
|
|
49
55
|
*/
|
|
50
56
|
export async function callStage(call) {
|
|
57
|
+
const schema = call.request?.responseSchema;
|
|
58
|
+
if (!schema)
|
|
59
|
+
return callStageOnce(call);
|
|
60
|
+
let reply = undefined;
|
|
61
|
+
let failure = undefined;
|
|
62
|
+
try {
|
|
63
|
+
await runStructured({
|
|
64
|
+
dispatch: async (feedback) => {
|
|
65
|
+
const outcome = await callStageOnce(feedback ? { ...call, prompt: `${call.prompt}\n\n${feedback}` } : call);
|
|
66
|
+
if (!outcome.ok) {
|
|
67
|
+
failure = outcome;
|
|
68
|
+
throw TRANSPORT_FAILED;
|
|
69
|
+
}
|
|
70
|
+
reply = outcome;
|
|
71
|
+
return outcome.raw;
|
|
72
|
+
},
|
|
73
|
+
validate: (candidate) => {
|
|
74
|
+
const errors = validateJsonSchemaSubset(candidate, schema);
|
|
75
|
+
return errors.length === 0 ? { ok: true, value: candidate } : { ok: false, errors };
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (err !== TRANSPORT_FAILED)
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
// A retry that fails in transport keeps the first reply, which the caller may still accept.
|
|
84
|
+
return reply ?? failure ?? { ok: false, reason: "error" };
|
|
85
|
+
}
|
|
86
|
+
/** Timeout and abort come from the dispatch's own reason, whatever the runner's kind. */
|
|
87
|
+
function failureReason(err) {
|
|
88
|
+
const reason = dispatchFailureReason(err);
|
|
89
|
+
return reason === "timeout" || reason === "aborted" ? reason : "error";
|
|
90
|
+
}
|
|
91
|
+
/** A failed call: its reason and message, and the dispatch's own result when it reached a transport. */
|
|
92
|
+
function failedCall(err) {
|
|
93
|
+
const result = dispatchFailureResult(err);
|
|
94
|
+
return { ok: false, reason: failureReason(err), error: errMessage(err), ...(result ? { result } : {}) };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* One dispatch with no validation, for a caller that parses and repairs the
|
|
98
|
+
* reply itself (reflect's repair turn, extract's own structured loop).
|
|
99
|
+
*/
|
|
100
|
+
export async function callStageOnce(call) {
|
|
51
101
|
const messages = [
|
|
52
102
|
...(call.system ? [{ role: "system", content: call.system }] : []),
|
|
53
103
|
...(call.history ?? []),
|
|
@@ -63,10 +113,11 @@ export async function callStage(call) {
|
|
|
63
113
|
runner: call.runner,
|
|
64
114
|
messages,
|
|
65
115
|
...(call.request ? { request: call.request } : {}),
|
|
116
|
+
...(call.current ? { current: call.current } : {}),
|
|
66
117
|
...(call.onNotices ? { onNotices: call.onNotices } : {}),
|
|
67
118
|
parse: (r) => r ?? "",
|
|
68
119
|
onError: (_cls, err) => {
|
|
69
|
-
failure =
|
|
120
|
+
failure = failedCall(err);
|
|
70
121
|
return undefined;
|
|
71
122
|
},
|
|
72
123
|
fallback: undefined,
|
|
@@ -85,8 +136,7 @@ export async function callStage(call) {
|
|
|
85
136
|
catch (err) {
|
|
86
137
|
if (err instanceof ConfigError)
|
|
87
138
|
throw err;
|
|
88
|
-
|
|
89
|
-
return { ok: false, reason: timedOut ? "timeout" : "error", error: errMessage(err) };
|
|
139
|
+
return failedCall(err);
|
|
90
140
|
}
|
|
91
141
|
}
|
|
92
142
|
/** Attribute a stage's LLM calls to its process and planned engine (the usage report). */
|
|
@@ -162,9 +212,10 @@ export function stageJudgedProposal(stash, proposal, judged, proposalsCtx) {
|
|
|
162
212
|
* The judge a quality gate names for itself (#1011): the gate's `engine`,
|
|
163
213
|
* `model`, `timeoutMs` and `llm` over the process's own settings, as
|
|
164
214
|
* `processes.triage.judgment` resolves over triage. `undefined` when the gate
|
|
165
|
-
* is off or sets none of them, so the caller keeps its own judge.
|
|
166
|
-
*
|
|
167
|
-
*
|
|
215
|
+
* is off or sets none of them, so the caller keeps its own judge. The engine
|
|
216
|
+
* may be of any kind; config validation has required one that confines the
|
|
217
|
+
* model-work tool policy. Throws when they resolve to no engine at all,
|
|
218
|
+
* before anything is generated: the gate never falls back to another judge.
|
|
168
219
|
*/
|
|
169
220
|
export function resolveQualityGateJudge(config, profile, processName, onNotices) {
|
|
170
221
|
const process = profile?.processes?.[processName];
|
|
@@ -173,7 +224,7 @@ export function resolveQualityGateJudge(config, profile, processName, onNotices)
|
|
|
173
224
|
return undefined;
|
|
174
225
|
if (!["engine", "model", "timeoutMs", "llm"].some((key) => Object.hasOwn(gate, key)))
|
|
175
226
|
return undefined;
|
|
176
|
-
const resolved =
|
|
227
|
+
const resolved = resolveImproveExecution({
|
|
177
228
|
config,
|
|
178
229
|
processName: `${processName}-quality-judge`,
|
|
179
230
|
...(profile ? { profile } : {}),
|
|
@@ -181,7 +232,7 @@ export function resolveQualityGateJudge(config, profile, processName, onNotices)
|
|
|
181
232
|
current: gate,
|
|
182
233
|
});
|
|
183
234
|
if (!resolved) {
|
|
184
|
-
throw new ConfigError(`The ${processName} quality gate's judge
|
|
235
|
+
throw new ConfigError(`The ${processName} quality gate's judge has no engine. Set processes.${processName}.qualityGate.engine.`, "INVALID_CONFIG_FILE");
|
|
185
236
|
}
|
|
186
237
|
onNotices?.(resolved.notices);
|
|
187
238
|
return resolved.runner;
|
|
@@ -349,13 +400,13 @@ function judgeResponseSchema(keys) {
|
|
|
349
400
|
*/
|
|
350
401
|
async function runQualityJudge(feature, config, prompt, keys, chat, options) {
|
|
351
402
|
const resolved = !options.runnerSelectionFrozen && !options.llmRunner
|
|
352
|
-
?
|
|
403
|
+
? resolveImproveExecution({ config, processName: `${feature}-judge` })
|
|
353
404
|
: null;
|
|
354
405
|
if (resolved)
|
|
355
406
|
options.onNotices?.(resolved.notices);
|
|
356
407
|
const runner = options.llmRunner ?? resolved?.runner;
|
|
357
408
|
if (!runner)
|
|
358
|
-
return { pass: false, score: -1, reason: "no
|
|
409
|
+
return { pass: false, score: -1, reason: "no engine configured — cannot judge, failing closed" };
|
|
359
410
|
const outcome = await callStage({
|
|
360
411
|
feature,
|
|
361
412
|
runner,
|
|
@@ -363,7 +414,7 @@ async function runQualityJudge(feature, config, prompt, keys, chat, options) {
|
|
|
363
414
|
prompt,
|
|
364
415
|
request: {
|
|
365
416
|
// Off unless the judge's own engine enables thinking (a slower, separate judge engine, #1011).
|
|
366
|
-
enableThinking: runner
|
|
417
|
+
enableThinking: runnerLlmConnection(runner)?.enableThinking === true,
|
|
367
418
|
temperature: 0,
|
|
368
419
|
responseSchema: judgeResponseSchema(keys),
|
|
369
420
|
...(Object.hasOwn(options, "timeoutMs") ? { timeoutMs: options.timeoutMs } : {}),
|
|
@@ -25,6 +25,8 @@ import { ConfigError } from "../../core/errors.js";
|
|
|
25
25
|
import { appendEvent } from "../../core/events.js";
|
|
26
26
|
import { escapeJsonStringControls, stripCodeFences, stripThinkBlocks } from "../../core/parse.js";
|
|
27
27
|
import { info, warn } from "../../core/warn.js";
|
|
28
|
+
import { MODEL_WORK_TOOLS } from "../../execution/source.js";
|
|
29
|
+
import { DEFAULT_MODEL_WORK_TIMEOUT_MS } from "../../integrations/agent/config.js";
|
|
28
30
|
import { buildExecution, resolveExecution } from "../../integrations/agent/execution.js";
|
|
29
31
|
import { assertRunnerCredentials, runExecution, } from "../../integrations/agent/runner-dispatch.js";
|
|
30
32
|
import { errMessage, noticeSet } from "../improve/stage.js";
|
|
@@ -170,7 +172,16 @@ export function parseJudgmentVerdict(raw) {
|
|
|
170
172
|
async function dispatchJudgment(runner, prompt, seams) {
|
|
171
173
|
let notices = [];
|
|
172
174
|
try {
|
|
173
|
-
|
|
175
|
+
// Model work is bounded on every runner kind: one with no timeout of its own gets the default.
|
|
176
|
+
// The judgment runs under the model-work tool policy.
|
|
177
|
+
const prepared = resolveExecution({
|
|
178
|
+
content: prompt,
|
|
179
|
+
runner,
|
|
180
|
+
current: {
|
|
181
|
+
...(Object.hasOwn(runner, "timeoutMs") ? {} : { timeout: DEFAULT_MODEL_WORK_TIMEOUT_MS }),
|
|
182
|
+
tools: MODEL_WORK_TOOLS,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
174
185
|
const lowered = buildExecution(prepared.request, prepared.runner);
|
|
175
186
|
notices = lowered.notices;
|
|
176
187
|
const chat = seams.chat;
|
|
@@ -330,10 +341,11 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
|
|
|
330
341
|
}
|
|
331
342
|
}
|
|
332
343
|
if (opts.judgment && result.deferred.length > 0) {
|
|
333
|
-
// Symbolic credentials are checked before any gate, reject or promote.
|
|
344
|
+
// Symbolic credentials, and the runner's model-work tool policy, are checked before any gate, reject or promote.
|
|
334
345
|
const prepared = resolveExecution({
|
|
335
346
|
content: "Validate the selected proposal judgment runner before mutation.",
|
|
336
347
|
runner: opts.judgment,
|
|
348
|
+
current: { tools: MODEL_WORK_TOOLS },
|
|
337
349
|
});
|
|
338
350
|
assertRunnerCredentials(buildExecution(prepared.request, prepared.runner).runner);
|
|
339
351
|
}
|
|
@@ -18,7 +18,7 @@ import { parsePositiveIntFlag } from "../../cli/parse-args.js";
|
|
|
18
18
|
import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
|
|
19
19
|
import { resolveStashDir } from "../../core/common.js";
|
|
20
20
|
import { loadConfig } from "../../core/config/config.js";
|
|
21
|
-
import {
|
|
21
|
+
import { UsageError } from "../../core/errors.js";
|
|
22
22
|
import { installLlmUsagePersistenceIfAbsent } from "../../llm/usage-persist.js";
|
|
23
23
|
import { withLlmStage } from "../../llm/usage-telemetry.js";
|
|
24
24
|
import { resolveImproveExecution } from "../improve/execution.js";
|
|
@@ -470,10 +470,6 @@ const proposalDrainCommand = defineJsonCommand({
|
|
|
470
470
|
})
|
|
471
471
|
: null;
|
|
472
472
|
const judgment = judgmentResolution?.runner ?? null;
|
|
473
|
-
const effectiveJudgmentLlm = triageConfig?.judgment?.llm ?? triageConfig?.llm ?? selectedStrategy.config.llm;
|
|
474
|
-
if (judgment && judgment.kind !== "llm" && effectiveJudgmentLlm) {
|
|
475
|
-
throw new ConfigError(`Triage judgment engine "${judgment.engine ?? "unknown"}" is an agent engine and cannot receive llm overrides.`, "INVALID_CONFIG_FILE");
|
|
476
|
-
}
|
|
477
473
|
// #576: persist + attribute per-call LLM usage for the standalone drain
|
|
478
474
|
// path. `IfAbsent` keeps an enclosing `akm improve` sink in charge when
|
|
479
475
|
// drain runs as a sub-step; the disposer clears only a sink we installed.
|
|
@@ -26,7 +26,7 @@ const EXIT_GENERAL = EXIT_CODES.GENERAL;
|
|
|
26
26
|
export const proposeCommand = defineCommand({
|
|
27
27
|
meta: {
|
|
28
28
|
name: "new",
|
|
29
|
-
description: "Ask the configured
|
|
29
|
+
description: "Ask the configured engine to author a brand-new asset as JSON and queue it as a proposal",
|
|
30
30
|
},
|
|
31
31
|
// Raw defineCommand: declare the global output flags so their space-separated
|
|
32
32
|
// values are consumed rather than shifting the `type` / `name` positionals.
|
|
@@ -48,7 +48,7 @@ export const proposeCommand = defineCommand({
|
|
|
48
48
|
task: { type: "string", description: "Task description for the agent (what should the asset do?)" },
|
|
49
49
|
file: { type: "string", description: "Read the task or prompt text from a UTF-8 file" },
|
|
50
50
|
engine: { type: "string", description: "Engine to use (defaults to defaults.engine)" },
|
|
51
|
-
"timeout-ms": { type: "string", description: "Override the
|
|
51
|
+
"timeout-ms": { type: "string", description: "Override the engine timeout in milliseconds" },
|
|
52
52
|
},
|
|
53
53
|
async run({ args }) {
|
|
54
54
|
await runWithJsonErrors(async () => {
|
|
@@ -2,17 +2,17 @@
|
|
|
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
4
|
/**
|
|
5
|
-
* `akm
|
|
6
|
-
*
|
|
5
|
+
* `akm proposal new <type> <name> --task ...` — proposal-producing command
|
|
6
|
+
* (#226).
|
|
7
7
|
*
|
|
8
|
-
* Mirrors {@link akmReflect} but for fresh authoring. The
|
|
9
|
-
* task description plus per-asset-type schema hints and
|
|
10
|
-
*
|
|
8
|
+
* Mirrors {@link akmReflect} but for fresh authoring. The engine, of any kind,
|
|
9
|
+
* receives a task description plus per-asset-type schema hints and returns a
|
|
10
|
+
* brand-new asset payload as JSON on stdout. The output lands ONLY in the
|
|
11
|
+
* proposal queue.
|
|
11
12
|
*
|
|
12
13
|
* Failures use the same {@link AgentFailureReason} discriminants as
|
|
13
14
|
* `akm reflect`. `propose_invoked` is emitted at command entry.
|
|
14
15
|
*/
|
|
15
|
-
import fs from "node:fs";
|
|
16
16
|
import { placementTypes, stashDirFor } from "../../core/asset/asset-placement.js";
|
|
17
17
|
import { parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
18
18
|
import { resolveStashDir } from "../../core/common.js";
|
|
@@ -21,12 +21,14 @@ import { UsageError } from "../../core/errors.js";
|
|
|
21
21
|
import { appendEvent } from "../../core/events.js";
|
|
22
22
|
import { redactSensitiveText } from "../../core/redaction.js";
|
|
23
23
|
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
24
|
+
import { runStructured } from "../../core/structured.js";
|
|
24
25
|
import { warn } from "../../core/warn.js";
|
|
25
26
|
import { deriveEntryProvenance } from "../../indexer/installations.js";
|
|
26
27
|
import { fallbackAnnouncement } from "../../integrations/agent/engine-fallback.js";
|
|
27
28
|
import { buildExecution, resolveExecution } from "../../integrations/agent/execution.js";
|
|
28
|
-
import { buildProposePrompt,
|
|
29
|
+
import { buildProposePrompt, PROPOSAL_JSON_SCHEMA, validateProposalPayload, } from "../../integrations/agent/prompts.js";
|
|
29
30
|
import { assertRunnerCredentials, collectDispatchSensitiveValues, runExecution, } from "../../integrations/agent/runner-dispatch.js";
|
|
31
|
+
import { getHarness } from "../../integrations/harnesses/index.js";
|
|
30
32
|
import { baseFailureFields, enoentHintMessage, isEnoentFailure } from "../agent/agent-support.js";
|
|
31
33
|
import { createProposal, resolveProposalQueueTarget, } from "./repository.js";
|
|
32
34
|
function failureEnvelope(result, type, name, engine, notices, fallbackReason = "non_zero_exit") {
|
|
@@ -42,42 +44,71 @@ function failureEnvelope(result, type, name, engine, notices, fallbackReason = "
|
|
|
42
44
|
function noticeFields(notices) {
|
|
43
45
|
return notices.length > 0 ? { notices } : {};
|
|
44
46
|
}
|
|
45
|
-
|
|
47
|
+
const DISPATCH_FAILED = Symbol("proposal-dispatch-failed");
|
|
48
|
+
/** A reply's text, unwrapped from its harness's framing (claude's `--output-format json` envelope). */
|
|
49
|
+
function replyText(execution, result) {
|
|
50
|
+
const runner = execution.runner;
|
|
51
|
+
if (runner.kind !== "agent")
|
|
52
|
+
return result.stdout;
|
|
53
|
+
const extractor = getHarness(runner.profile.platform ?? runner.profile.name)?.resultExtractor;
|
|
54
|
+
return extractor ? extractor(result).text : result.stdout;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve, lower, and dispatch the already-rendered proposal prompt with the
|
|
58
|
+
* proposal's JSON Schema as its output schema, and capture the reply. A reply
|
|
59
|
+
* that is not a proposal gets one corrective retry.
|
|
60
|
+
*/
|
|
46
61
|
async function dispatchProposalPrompt(prompt, config, options, onDispatchReady) {
|
|
47
62
|
const current = {
|
|
48
63
|
...(options.engine !== undefined ? { engine: options.engine } : {}),
|
|
49
64
|
...(options.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
|
|
65
|
+
outputSchema: PROPOSAL_JSON_SCHEMA,
|
|
50
66
|
};
|
|
51
|
-
const
|
|
52
|
-
content
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
});
|
|
67
|
+
const lower = (content) => {
|
|
68
|
+
const prepared = resolveExecution({ content, config, current });
|
|
69
|
+
return { prepared, lowered: buildExecution(prepared.request, prepared.runner) };
|
|
70
|
+
};
|
|
71
|
+
const { prepared, lowered } = lower(prompt);
|
|
56
72
|
const engineName = prepared.request.engine.name;
|
|
57
73
|
const announcement = fallbackAnnouncement(prepared.fallbackEngineName, engineName);
|
|
58
74
|
if (announcement)
|
|
59
75
|
warn(announcement);
|
|
60
|
-
const lowered = buildExecution(prepared.request, prepared.runner);
|
|
61
|
-
const interactive = !options.runAgentOptions?.spawn;
|
|
62
|
-
const runOptions = {
|
|
63
|
-
stdio: interactive ? "interactive" : "captured",
|
|
64
|
-
parseOutput: "text",
|
|
65
|
-
...(options.runAgentOptions ?? {}),
|
|
66
|
-
};
|
|
67
76
|
// Validate every required symbolic credential before the entry event opens
|
|
68
77
|
// durable state. Provider/runtime failures still occur after the event,
|
|
69
|
-
// preserving the command-attempt observability contract.
|
|
70
|
-
|
|
78
|
+
// preserving the command-attempt observability contract. The dispatch reads
|
|
79
|
+
// the same caller environment.
|
|
80
|
+
const envSource = options.runAgentOptions?.envSource;
|
|
81
|
+
assertRunnerCredentials(lowered.runner, envSource);
|
|
71
82
|
onDispatchReady();
|
|
72
83
|
options.onDispatchReady?.();
|
|
73
|
-
|
|
84
|
+
// runStructured dispatches at least once, so a returned or DISPATCH_FAILED exchange has a result.
|
|
85
|
+
const results = [];
|
|
86
|
+
let reply;
|
|
87
|
+
try {
|
|
88
|
+
reply = await runStructured({
|
|
89
|
+
dispatch: async (feedback) => {
|
|
90
|
+
const execution = feedback ? lower(`${prompt}\n\n${feedback}`).lowered : lowered;
|
|
91
|
+
const result = await runExecution(execution, { runOptions: options.runAgentOptions ?? {} });
|
|
92
|
+
results.push(result);
|
|
93
|
+
if (!result.ok)
|
|
94
|
+
throw DISPATCH_FAILED;
|
|
95
|
+
return replyText(execution, result);
|
|
96
|
+
},
|
|
97
|
+
validate: validateProposalPayload,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (err !== DISPATCH_FAILED)
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
74
104
|
return {
|
|
75
|
-
result,
|
|
105
|
+
result: results.at(-1),
|
|
106
|
+
...(reply ? { reply } : {}),
|
|
107
|
+
durationMs: results.reduce((total, result) => total + result.durationMs, 0),
|
|
76
108
|
engineName,
|
|
77
109
|
...(lowered.runner.kind === "llm" ? {} : { engineBin: lowered.runner.profile.bin }),
|
|
78
110
|
notices: lowered.notices,
|
|
79
|
-
sensitiveValues: collectDispatchSensitiveValues(lowered.runner, {},
|
|
80
|
-
interactive,
|
|
111
|
+
sensitiveValues: collectDispatchSensitiveValues(lowered.runner, {}, envSource),
|
|
81
112
|
};
|
|
82
113
|
}
|
|
83
114
|
/**
|
|
@@ -128,10 +159,6 @@ export async function akmPropose(options) {
|
|
|
128
159
|
const config = options.agentConfig ?? (await import("../../core/config/config.js")).loadConfig();
|
|
129
160
|
const target = resolveProposalQueueTarget(stash, config);
|
|
130
161
|
// 2. Build terminal user content.
|
|
131
|
-
// Synthesize a temp draft path so opencode can write the asset content
|
|
132
|
-
// directly using its file tools rather than returning JSON via stdout.
|
|
133
|
-
const draftFilePath = import("node:os").then((os) => import("node:path").then((path) => path.join(os.tmpdir(), `akm-propose-${options.type}-${options.name.replace(/[^a-z0-9_-]/gi, "_")}-${Date.now()}.md`)));
|
|
134
|
-
const resolvedDraftPath = await draftFilePath;
|
|
135
162
|
// Standards "rulebook" for this target — wiki schema (wiki page) or stash
|
|
136
163
|
// convention/meta facts (non-wiki asset); empty when neither fires.
|
|
137
164
|
const standardsContext = resolveStandardsContext(`${options.type}:${options.name}`, stash);
|
|
@@ -140,14 +167,14 @@ export async function akmPropose(options) {
|
|
|
140
167
|
name: options.name,
|
|
141
168
|
task: options.task,
|
|
142
169
|
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
143
|
-
draftFilePath: resolvedDraftPath,
|
|
144
170
|
});
|
|
145
|
-
// 3. Preserve the fully-authored prompt as the terminal user content;
|
|
146
|
-
//
|
|
147
|
-
//
|
|
171
|
+
// 3. Preserve the fully-authored prompt as the terminal user content; the
|
|
172
|
+
// proposal's JSON Schema crosses the shared resolved/lowered boundary as the
|
|
173
|
+
// request's output schema, with no synthetic persona, conversation turn, or
|
|
174
|
+
// tool selection.
|
|
148
175
|
const dispatch = await dispatchProposalPrompt(prompt, config, options, () => emitProposeInvoked(target.source, options));
|
|
149
|
-
const { result, engineName, notices, sensitiveValues } = dispatch;
|
|
150
|
-
if (!
|
|
176
|
+
const { result, reply, engineName, notices, sensitiveValues } = dispatch;
|
|
177
|
+
if (!reply) {
|
|
151
178
|
// B3: ENOENT / not-found gives an actionable hint.
|
|
152
179
|
if (isEnoentFailure(result)) {
|
|
153
180
|
return {
|
|
@@ -157,54 +184,23 @@ export async function akmPropose(options) {
|
|
|
157
184
|
}
|
|
158
185
|
return failureEnvelope(result, options.type, options.name, engineName, notices);
|
|
159
186
|
}
|
|
160
|
-
// 5.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
187
|
+
// 5. The proposal the engine returned on stdout, validated.
|
|
188
|
+
if (!reply.ok) {
|
|
189
|
+
return {
|
|
190
|
+
schemaVersion: 2,
|
|
191
|
+
ok: false,
|
|
192
|
+
reason: "parse_error",
|
|
193
|
+
error: `Engine "${engineName}" reply was not valid proposal JSON after ${reply.attempts} attempts: ${reply.errors.join("; ")}`,
|
|
194
|
+
type: options.type,
|
|
195
|
+
name: options.name,
|
|
196
|
+
engine: engineName,
|
|
197
|
+
exitCode: result.exitCode,
|
|
198
|
+
stdout: result.stdout,
|
|
199
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
200
|
+
...noticeFields(notices),
|
|
170
201
|
};
|
|
171
202
|
}
|
|
172
|
-
|
|
173
|
-
// B1: When interactive mode was used and stdout is empty, the agent did not
|
|
174
|
-
// write the draft file and stdout was not captured — surface an actionable error.
|
|
175
|
-
if (dispatch.interactive && (result.stdout ?? "") === "") {
|
|
176
|
-
return {
|
|
177
|
-
schemaVersion: 2,
|
|
178
|
-
ok: false,
|
|
179
|
-
reason: "parse_error",
|
|
180
|
-
error: "Agent did not write draft file and stdout was not captured (interactive mode). Check that the agent CLI understood the file-write instruction, or configure a headless profile with stdio: 'captured'.",
|
|
181
|
-
type: options.type,
|
|
182
|
-
name: options.name,
|
|
183
|
-
engine: engineName,
|
|
184
|
-
exitCode: result.exitCode,
|
|
185
|
-
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
186
|
-
...noticeFields(notices),
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
try {
|
|
190
|
-
payload = parseAgentProposalPayload(result.stdout ?? "");
|
|
191
|
-
}
|
|
192
|
-
catch (err) {
|
|
193
|
-
return {
|
|
194
|
-
schemaVersion: 2,
|
|
195
|
-
ok: false,
|
|
196
|
-
reason: "parse_error",
|
|
197
|
-
error: err instanceof Error ? err.message : String(err),
|
|
198
|
-
type: options.type,
|
|
199
|
-
name: options.name,
|
|
200
|
-
engine: engineName,
|
|
201
|
-
exitCode: result.exitCode,
|
|
202
|
-
stdout: result.stdout,
|
|
203
|
-
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
204
|
-
...noticeFields(notices),
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
}
|
|
203
|
+
const payload = reply.value;
|
|
208
204
|
const unsafeContent = generatedContentRejection(payload.content, redactSensitiveText(payload.content, sensitiveValues));
|
|
209
205
|
if (unsafeContent) {
|
|
210
206
|
return {
|
|
@@ -270,6 +266,7 @@ export async function akmPropose(options) {
|
|
|
270
266
|
content: payload.content,
|
|
271
267
|
...(payload.frontmatter ? { frontmatter: payload.frontmatter } : {}),
|
|
272
268
|
},
|
|
269
|
+
...(payload.confidence !== undefined ? { confidence: payload.confidence } : {}),
|
|
273
270
|
};
|
|
274
271
|
const proposal = createProposal(stash, createInput, options.ctx);
|
|
275
272
|
return {
|
|
@@ -278,7 +275,7 @@ export async function akmPropose(options) {
|
|
|
278
275
|
proposal,
|
|
279
276
|
ref: proposal.ref,
|
|
280
277
|
engine: engineName,
|
|
281
|
-
durationMs:
|
|
278
|
+
durationMs: dispatch.durationMs,
|
|
282
279
|
...noticeFields(notices),
|
|
283
280
|
};
|
|
284
281
|
}
|
|
@@ -19,7 +19,7 @@ import { warn } from "../core/warn.js";
|
|
|
19
19
|
import { SCOPE_KEYS } from "../indexer/passes/metadata.js";
|
|
20
20
|
import { callStructured } from "../llm/structured-call.js";
|
|
21
21
|
import { withLlmStage } from "../llm/usage-telemetry.js";
|
|
22
|
-
import {
|
|
22
|
+
import { resolveImproveExecution } from "./improve/execution.js";
|
|
23
23
|
/**
|
|
24
24
|
* Parse a shorthand duration string to a number of milliseconds.
|
|
25
25
|
* Supports the CLI-wide canonical grammar: `30d` (days), `12h` (hours),
|
|
@@ -224,9 +224,9 @@ const LLM_ENRICH_TIMEOUT_MS = 10_000;
|
|
|
224
224
|
*/
|
|
225
225
|
export async function runLlmEnrich(body) {
|
|
226
226
|
const config = loadConfig();
|
|
227
|
-
const resolved =
|
|
227
|
+
const resolved = resolveImproveExecution({ config, processName: "remember-enrich" });
|
|
228
228
|
if (!resolved) {
|
|
229
|
-
warn("Warning: --enrich requires an
|
|
229
|
+
warn("Warning: --enrich requires an engine to be configured. Run `akm setup` to configure one.");
|
|
230
230
|
return { tags: [] };
|
|
231
231
|
}
|
|
232
232
|
const runner = resolved.runner;
|
|
@@ -105,7 +105,7 @@ export async function runSchemaRepairPass(failures, options) {
|
|
|
105
105
|
const { startMs, budgetMs, stashDir, findFilePath = defaultFindFilePath, isLessonCandidateFn = defaultIsLessonCandidate, chatFn, } = options;
|
|
106
106
|
const llmRunner = options.llmRunner ?? null;
|
|
107
107
|
if (!llmRunner)
|
|
108
|
-
throw new Error("runSchemaRepairPass requires a resolved
|
|
108
|
+
throw new Error("runSchemaRepairPass requires a resolved runner");
|
|
109
109
|
if (!stashDir) {
|
|
110
110
|
throw new Error("runSchemaRepairPass requires stashDir so repairs route through the proposal queue");
|
|
111
111
|
}
|