akm-cli 0.9.25-alpha.1 → 0.9.25-alpha.2
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 +159 -280
- package/dist/cli.js +1 -1
- package/dist/commands/improve/consolidate/pair-pass.js +1 -0
- package/dist/commands/improve/consolidate.js +7 -2
- package/dist/commands/improve/execution.js +2 -3
- package/dist/commands/improve/extract.js +1 -0
- package/dist/commands/improve/improve-cli.js +33 -1
- package/dist/commands/improve/loop-stages.js +3 -0
- package/dist/commands/improve/reflect-noise.js +125 -0
- package/dist/commands/improve/reflect.js +13 -16
- package/dist/commands/improve/retrieval-gate.js +7 -2
- package/dist/commands/improve/stage.js +31 -39
- package/dist/commands/proposal/drain.js +4 -7
- package/dist/commands/proposal/propose.js +2 -11
- package/dist/commands/proposal/validators/proposal-quality-validators.js +4 -2
- package/dist/commands/read/search-cli.js +0 -38
- package/dist/core/config/schema/engines.js +15 -33
- package/dist/core/config/schema/improve-processes.js +16 -0
- package/dist/core/redaction.js +4 -0
- package/dist/core/spawn-env.js +25 -0
- package/dist/core/structured.js +1 -1
- package/dist/execution/source.js +8 -12
- package/dist/integrations/agent/config.js +1 -3
- package/dist/integrations/agent/engine-resolution.js +0 -3
- package/dist/integrations/agent/execution.js +14 -13
- package/dist/integrations/agent/index.js +1 -1
- package/dist/integrations/agent/model-map.js +15 -16
- package/dist/integrations/agent/profiles.js +2 -2
- package/dist/integrations/agent/prompts.js +3 -39
- package/dist/integrations/agent/request-lowering.js +9 -7
- package/dist/integrations/agent/runner-dispatch.js +25 -31
- package/dist/integrations/harnesses/aider/agent-builder.js +1 -2
- package/dist/integrations/harnesses/amazonq/agent-builder.js +1 -2
- package/dist/integrations/harnesses/claude/agent-builder.js +4 -16
- package/dist/integrations/harnesses/codex/agent-builder.js +1 -2
- package/dist/integrations/harnesses/ids.js +10 -16
- package/dist/integrations/harnesses/opencode/agent-builder.js +14 -24
- package/dist/integrations/harnesses/opencode/model-config.js +15 -62
- package/dist/integrations/harnesses/opencode/model-work-agent.js +71 -36
- package/dist/integrations/harnesses/opencode-sdk/harness.js +2 -6
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +32 -90
- package/dist/integrations/harnesses/openhands/agent-builder.js +1 -2
- package/dist/integrations/harnesses/pi/agent-builder.js +1 -2
- package/dist/llm/feature-gate.js +2 -5
- package/dist/llm/index-passes.js +2 -2
- package/dist/llm/structured-call.js +5 -5
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/scripts/akm-migrate-node.js +170 -194
- package/dist/scripts/akm-migrate.js +170 -194
- package/dist/workflows/exec/unit-dispatch.js +4 -13
- package/docs/reference/cli.md +12 -7
- package/docs/reference/configuration.md +78 -82
- package/docs/reference/data-and-telemetry.md +2 -3
- package/docs/reference/workflow-schema.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +108 -36
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/** What opencode carries on a model; see `harnesses/opencode/model-config.ts`. */
|
|
5
|
-
const OPENCODE_INFERENCE = ["temperature", "maxTokens", "contextLength", "enableThinking", "reasoningEffort"];
|
|
6
4
|
export const HARNESS_ID_TABLE = [
|
|
7
|
-
{ id: "opencode", agentDispatch: true, enforcesModelWorkTools: true
|
|
8
|
-
{ id: "claude", agentDispatch: true, enforcesModelWorkTools: true
|
|
9
|
-
{ id: "opencode-sdk", agentDispatch: true, enforcesModelWorkTools: true
|
|
10
|
-
{ id: "codex", agentDispatch: true, enforcesModelWorkTools: false
|
|
11
|
-
{ id: "copilot", agentDispatch: true, enforcesModelWorkTools: false
|
|
12
|
-
{ id: "pi", agentDispatch: true, enforcesModelWorkTools: false
|
|
13
|
-
{ id: "gemini", agentDispatch: true, enforcesModelWorkTools: false
|
|
14
|
-
{ id: "aider", agentDispatch: true, enforcesModelWorkTools: false
|
|
15
|
-
{ id: "amazonq", agentDispatch: true, enforcesModelWorkTools: false
|
|
16
|
-
{ id: "openhands", agentDispatch: true, enforcesModelWorkTools: false
|
|
5
|
+
{ id: "opencode", agentDispatch: true, enforcesModelWorkTools: true },
|
|
6
|
+
{ id: "claude", agentDispatch: true, enforcesModelWorkTools: true },
|
|
7
|
+
{ id: "opencode-sdk", agentDispatch: true, enforcesModelWorkTools: true },
|
|
8
|
+
{ id: "codex", agentDispatch: true, enforcesModelWorkTools: false },
|
|
9
|
+
{ id: "copilot", agentDispatch: true, enforcesModelWorkTools: false },
|
|
10
|
+
{ id: "pi", agentDispatch: true, enforcesModelWorkTools: false },
|
|
11
|
+
{ id: "gemini", agentDispatch: true, enforcesModelWorkTools: false },
|
|
12
|
+
{ id: "aider", agentDispatch: true, enforcesModelWorkTools: false },
|
|
13
|
+
{ id: "amazonq", agentDispatch: true, enforcesModelWorkTools: false },
|
|
14
|
+
{ id: "openhands", agentDispatch: true, enforcesModelWorkTools: false },
|
|
17
15
|
];
|
|
18
16
|
/**
|
|
19
17
|
* Canonical, ordered list of valid harness / platform ids — the
|
|
@@ -26,7 +24,3 @@ export const VALID_HARNESS_IDS = Object.freeze(HARNESS_ID_TABLE.map((h) => h.id)
|
|
|
26
24
|
export const HARNESS_AGENT_DISPATCH_IDS = new Set(HARNESS_ID_TABLE.filter((h) => h.agentDispatch).map((h) => h.id));
|
|
27
25
|
/** Harness ids that confine the model-work tool policy, so unattended model work may run on them. */
|
|
28
26
|
export const HARNESS_MODEL_WORK_IDS = new Set(HARNESS_ID_TABLE.filter((h) => h.enforcesModelWorkTools).map((h) => h.id));
|
|
29
|
-
/** The inference keys an agent engine on `platform` may set; none for a platform that is not registered. */
|
|
30
|
-
export function harnessInferenceKeys(platform) {
|
|
31
|
-
return HARNESS_ID_TABLE.find((h) => h.id === platform)?.inference ?? [];
|
|
32
|
-
}
|
|
@@ -14,11 +14,10 @@
|
|
|
14
14
|
* pre-migration `opencodeBuilder`. The builder's `platform` stays `'opencode'`
|
|
15
15
|
* (the canonical harness id).
|
|
16
16
|
*/
|
|
17
|
-
import { isModelWorkTools } from "../../../execution/source.js";
|
|
18
17
|
import { modelFromArgs, resolveDispatchModel } from "../../agent/builder-shared.js";
|
|
19
18
|
import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
|
|
20
|
-
import {
|
|
21
|
-
import { MODEL_WORK_OPENCODE_AGENT, modelWorkOpencodeConfig } from "./model-work-agent.js";
|
|
19
|
+
import { MODEL_WORK_AGENT_INFERENCE, opencodeInferenceConfig } from "./model-config.js";
|
|
20
|
+
import { MODEL_WORK_OPENCODE_AGENT, modelWorkOpencodeConfig, modelWorkPluginEnv } from "./model-work-agent.js";
|
|
22
21
|
/**
|
|
23
22
|
* OpenCode builder.
|
|
24
23
|
* Command shape: opencode run [--agent <name>] [--model <m>] -- "<prompt>"
|
|
@@ -36,11 +35,9 @@ import { MODEL_WORK_OPENCODE_AGENT, modelWorkOpencodeConfig } from "./model-work
|
|
|
36
35
|
* of the injected config or the scratch working directory. Only the model they
|
|
37
36
|
* name is kept.
|
|
38
37
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* or not a model is named. A dispatch whose request carries no translatable
|
|
43
|
-
* inference injects nothing, so its argv and env are as they were.
|
|
38
|
+
* Model work's agent carries the request's inference options
|
|
39
|
+
* (`./model-config.ts`). Any other dispatch injects nothing and carries none, so
|
|
40
|
+
* the model's own opencode config applies: set inference there.
|
|
44
41
|
*/
|
|
45
42
|
export const opencodeBuilder = {
|
|
46
43
|
platform: "opencode",
|
|
@@ -50,19 +47,12 @@ export const opencodeBuilder = {
|
|
|
50
47
|
personaChannel: "prompt",
|
|
51
48
|
nativeAgentSelector: true,
|
|
52
49
|
tools: "none",
|
|
53
|
-
|
|
54
|
-
inference: (profile, request) => {
|
|
55
|
-
const model = request.model?.resolved ?? modelFromArgs(profile.args);
|
|
56
|
-
return opencodeCarriedKeys(model !== undefined && splitOpencodeModel(model) !== undefined, request.inference, isModelWorkTools(request.tools));
|
|
57
|
-
},
|
|
50
|
+
inference: MODEL_WORK_AGENT_INFERENCE,
|
|
58
51
|
}),
|
|
59
52
|
build(profile, req) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const { entry, agentOptions } = opencodeInferenceConfig(req.inference, modelWork);
|
|
64
|
-
const modelConfig = opencodeModelConfig(model, entry);
|
|
65
|
-
if (modelWork) {
|
|
53
|
+
if (req.modelWork) {
|
|
54
|
+
const model = req.model ?? modelFromArgs(profile.args);
|
|
55
|
+
const { agentOptions } = opencodeInferenceConfig(req.inference, true);
|
|
66
56
|
return {
|
|
67
57
|
argv: [
|
|
68
58
|
profile.bin,
|
|
@@ -73,7 +63,10 @@ export const opencodeBuilder = {
|
|
|
73
63
|
"--",
|
|
74
64
|
req.prompt,
|
|
75
65
|
],
|
|
76
|
-
env: {
|
|
66
|
+
env: {
|
|
67
|
+
...modelWorkPluginEnv(),
|
|
68
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify(modelWorkOpencodeConfig(agentOptions)),
|
|
69
|
+
},
|
|
77
70
|
};
|
|
78
71
|
}
|
|
79
72
|
const args = req.model ? [] : [...profile.args];
|
|
@@ -99,9 +92,6 @@ export const opencodeBuilder = {
|
|
|
99
92
|
}
|
|
100
93
|
args.push("--");
|
|
101
94
|
args.push(req.prompt);
|
|
102
|
-
return {
|
|
103
|
-
argv: [profile.bin, ...args],
|
|
104
|
-
...(modelConfig ? { env: { OPENCODE_CONFIG_CONTENT: JSON.stringify(modelConfig) } } : {}),
|
|
105
|
-
};
|
|
95
|
+
return { argv: [profile.bin, ...args] };
|
|
106
96
|
},
|
|
107
97
|
};
|
|
@@ -1,80 +1,33 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/** The inference keys model work's agent carries as options. */
|
|
5
|
+
export const MODEL_WORK_AGENT_INFERENCE = ["temperature", "reasoningEffort", "enableThinking"];
|
|
4
6
|
const isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* is carried as nothing.
|
|
8
|
+
* Split `inference` into opencode config: `entry` for a model (its options, and
|
|
9
|
+
* its limit), and for model work `agentOptions` for the confined agent in place
|
|
10
|
+
* of the entry's options. A value of the wrong type is left out.
|
|
10
11
|
*/
|
|
11
|
-
export function
|
|
12
|
-
const carried = {};
|
|
12
|
+
export function opencodeInferenceConfig(inference, modelWork) {
|
|
13
13
|
const { temperature, reasoningEffort, enableThinking, maxTokens, contextLength } = inference ?? {};
|
|
14
|
+
const options = {};
|
|
14
15
|
if (typeof temperature === "number" && Number.isFinite(temperature))
|
|
15
|
-
|
|
16
|
+
options.temperature = temperature;
|
|
16
17
|
if (typeof reasoningEffort === "string" && reasoningEffort.length > 0)
|
|
17
|
-
|
|
18
|
-
if (typeof enableThinking === "boolean")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (limit)
|
|
22
|
-
carried.limit = { context: contextLength, output: maxTokens };
|
|
23
|
-
const keys = [
|
|
24
|
-
["temperature", carried.temperature !== undefined, temperature],
|
|
25
|
-
["reasoningEffort", carried.reasoningEffort !== undefined, reasoningEffort],
|
|
26
|
-
["enableThinking", carried.enableThinking !== undefined, enableThinking],
|
|
27
|
-
["maxTokens", limit, maxTokens],
|
|
28
|
-
["contextLength", limit, contextLength],
|
|
29
|
-
]
|
|
30
|
-
.filter(([, isCarried, value]) => isCarried || value === null)
|
|
31
|
-
.map(([key]) => key);
|
|
32
|
-
return { carried, keys };
|
|
33
|
-
}
|
|
34
|
-
/** opencode's own split of `provider/model`: at the first slash, so a model id may contain slashes. */
|
|
35
|
-
export function splitOpencodeModel(model) {
|
|
36
|
-
const slash = model.indexOf("/");
|
|
37
|
-
if (slash <= 0 || slash === model.length - 1)
|
|
38
|
-
return undefined;
|
|
39
|
-
return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* The inference keys opencode carries for a dispatch. With a model to attach
|
|
43
|
-
* them to (`attachable`) it carries all of them. Without one it carries only
|
|
44
|
-
* what the model-work agent can, which is every option but the limit; any
|
|
45
|
-
* other dispatch carries nothing.
|
|
46
|
-
*/
|
|
47
|
-
export function opencodeCarriedKeys(attachable, inference, modelWork) {
|
|
48
|
-
const { keys } = carriedInference(inference);
|
|
49
|
-
if (attachable)
|
|
50
|
-
return keys;
|
|
51
|
-
return modelWork ? keys.filter((key) => key !== "maxTokens" && key !== "contextLength") : [];
|
|
52
|
-
}
|
|
53
|
-
/** Split `inference` between the model and, for model work, the confined agent (see the module comment). */
|
|
54
|
-
export function opencodeInferenceConfig(inference, modelWork) {
|
|
55
|
-
const { carried } = carriedInference(inference);
|
|
56
|
-
const options = {};
|
|
57
|
-
if (carried.temperature !== undefined)
|
|
58
|
-
options.temperature = carried.temperature;
|
|
59
|
-
if (carried.reasoningEffort !== undefined)
|
|
60
|
-
options.reasoningEffort = carried.reasoningEffort;
|
|
61
|
-
if (carried.enableThinking !== undefined) {
|
|
62
|
-
options.chat_template_kwargs = { enable_thinking: carried.enableThinking };
|
|
63
|
-
options.enable_thinking = carried.enableThinking;
|
|
18
|
+
options.reasoningEffort = reasoningEffort;
|
|
19
|
+
if (typeof enableThinking === "boolean") {
|
|
20
|
+
options.chat_template_kwargs = { enable_thinking: enableThinking };
|
|
21
|
+
options.enable_thinking = enableThinking;
|
|
64
22
|
}
|
|
65
23
|
const hasOptions = Object.keys(options).length > 0;
|
|
66
24
|
return {
|
|
67
25
|
entry: {
|
|
68
26
|
...(hasOptions && !modelWork ? { options } : {}),
|
|
69
|
-
...(
|
|
27
|
+
...(isPositiveInteger(maxTokens) && isPositiveInteger(contextLength)
|
|
28
|
+
? { limit: { context: contextLength, output: maxTokens } }
|
|
29
|
+
: {}),
|
|
70
30
|
},
|
|
71
31
|
...(hasOptions && modelWork ? { agentOptions: options } : {}),
|
|
72
32
|
};
|
|
73
33
|
}
|
|
74
|
-
/** The config that gives `model` (`provider/model`) `entry`: undefined when the model cannot be split or `entry` is empty. */
|
|
75
|
-
export function opencodeModelConfig(model, entry) {
|
|
76
|
-
const target = model === undefined ? undefined : splitOpencodeModel(model);
|
|
77
|
-
if (!target || Object.keys(entry).length === 0)
|
|
78
|
-
return undefined;
|
|
79
|
-
return { provider: { [target.providerID]: { models: { [target.modelID]: entry } } } };
|
|
80
|
-
}
|
|
@@ -3,18 +3,20 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
5
|
* The opencode agent that runs unattended model work under the model-work
|
|
6
|
-
* tool policy (`
|
|
6
|
+
* tool policy (`MODEL_WORK_POLICY_ID`). The CLI builder injects it through
|
|
7
7
|
* `OPENCODE_CONFIG_CONTENT` and selects it with `--agent`; the SDK runner adds
|
|
8
8
|
* it to its server config and names it in the prompt body.
|
|
9
9
|
*
|
|
10
10
|
* What opencode 1.18.25 confines, checked against a local stub:
|
|
11
|
-
* - read and
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* - read, grep and glob work in the session directory and akm's primary
|
|
12
|
+
* stash, and nowhere else. edit works in the session directory only: the
|
|
13
|
+
* stash is denied, by its path without the leading slash, because opencode
|
|
14
|
+
* matches edit patterns root-relative (to the git root, when the session
|
|
15
|
+
* directory is in a repository). Write is part of the edit permission.
|
|
16
|
+
* - `akm_search` and `akm_show` are the akm-opencode plugin's read tools; its
|
|
17
|
+
* other three are denied. bash is denied: opencode matches a bash rule
|
|
18
|
+
* against the command's words only, so `akm show x > ~/stash/asset.md`
|
|
19
|
+
* would pass an `akm show *` rule and write anywhere.
|
|
18
20
|
* - every other tool is denied, `doom_loop` included (its default, `ask`,
|
|
19
21
|
* would hang a headless server). Each permission opencode knows is named,
|
|
20
22
|
* so a same-named agent in the user's config cannot re-allow one through
|
|
@@ -26,36 +28,62 @@
|
|
|
26
28
|
* - its own short `prompt` replaces the provider's coding prompt, which
|
|
27
29
|
* tells the model to search extensively; opencode appends a request's
|
|
28
30
|
* system text after it and never substitutes it;
|
|
29
|
-
* - `steps
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
* - it sets no `steps`. At its step limit opencode sends a "maximum steps"
|
|
32
|
+
* text as a trailing assistant message, which a qwen chat template (LM
|
|
33
|
+
* Studio, llama-server) renders as the start of the model's reply: LM
|
|
34
|
+
* Studio then returns nothing and llama-server returns that text as the
|
|
35
|
+
* answer. The dispatch timeout bounds a run instead;
|
|
32
36
|
* - automatic compaction is off, so a long run cannot summarize the task
|
|
33
37
|
* away.
|
|
34
38
|
*/
|
|
39
|
+
import path from "node:path";
|
|
40
|
+
import { resolveStashDir } from "../../../core/common.js";
|
|
41
|
+
import { getStateDir } from "../../../core/paths.js";
|
|
35
42
|
export const MODEL_WORK_OPENCODE_AGENT = "akm-model-work";
|
|
36
|
-
/** The agentic iterations a model-work run may take: a judge answers in one, a generator in a few. */
|
|
37
|
-
export const MODEL_WORK_STEPS = 8;
|
|
38
|
-
/** Steps past {@link MODEL_WORK_STEPS} after which the SDK runner aborts the session. */
|
|
39
|
-
export const MODEL_WORK_STEP_GRACE = 2;
|
|
40
43
|
const MODEL_WORK_PROMPT = "You do one bounded task for akm. Use tools only to check what the task needs, never repeat a tool call, and reply with exactly what the task asks for.";
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
44
|
+
function modelWorkPermission(stash) {
|
|
45
|
+
return {
|
|
46
|
+
"*": "deny",
|
|
47
|
+
read: "allow",
|
|
48
|
+
grep: "allow",
|
|
49
|
+
glob: "allow",
|
|
50
|
+
edit: { "*": "allow", ...(stash ? { [`${stash.slice(1)}/*`]: "deny" } : {}) },
|
|
51
|
+
external_directory: {
|
|
52
|
+
...(stash ? { [`${stash}/*`]: "allow" } : {}),
|
|
53
|
+
"~/.local/share/opencode/tool-output/*": "deny",
|
|
54
|
+
},
|
|
55
|
+
akm_search: "allow",
|
|
56
|
+
akm_show: "allow",
|
|
57
|
+
akm_feedback: "deny",
|
|
58
|
+
akm_remember: "deny",
|
|
59
|
+
akm_curate: "deny",
|
|
60
|
+
bash: "deny",
|
|
61
|
+
doom_loop: "deny",
|
|
62
|
+
list: "deny",
|
|
63
|
+
lsp: "deny",
|
|
64
|
+
question: "deny",
|
|
65
|
+
skill: "deny",
|
|
66
|
+
task: "deny",
|
|
67
|
+
todowrite: "deny",
|
|
68
|
+
webfetch: "deny",
|
|
69
|
+
websearch: "deny",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* What an opencode model-work dispatch gives the akm-opencode plugin (it comes from the user's opencode config and gives
|
|
74
|
+
* the model `akm_search` and `akm_show`): its own switches off, and its state in akm's state directory, the same for every
|
|
75
|
+
* dispatch because an SDK server outlives its dispatch. win32 has no /bin/true, so the plugin keeps its CLI there.
|
|
76
|
+
*/
|
|
77
|
+
export function modelWorkPluginEnv() {
|
|
78
|
+
return {
|
|
79
|
+
AKM_AUTO_CURATE: "0",
|
|
80
|
+
AKM_AUTO_LEARNING: "0",
|
|
81
|
+
AKM_AUTO_SKILL_PROPOSALS: "0",
|
|
82
|
+
AKM_WRITE_GATE: "off",
|
|
83
|
+
XDG_STATE_HOME: path.join(getStateDir(), "opencode-model-work"),
|
|
84
|
+
...(process.platform === "win32" ? {} : { AKM_OPENCODE_CLI: "/bin/true" }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
59
87
|
/**
|
|
60
88
|
* The opencode config fragment that defines and confines the model-work agent.
|
|
61
89
|
* The agent carries the request's inference options (`model-config.ts`), which
|
|
@@ -63,17 +91,24 @@ const MODEL_WORK_PERMISSION = {
|
|
|
63
91
|
* the session, keep the model's defaults.
|
|
64
92
|
*/
|
|
65
93
|
export function modelWorkOpencodeConfig(options) {
|
|
94
|
+
let stash;
|
|
95
|
+
try {
|
|
96
|
+
stash = resolveStashDir();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// akm has no stash here, so the agent is given no path into one.
|
|
100
|
+
}
|
|
101
|
+
const permission = modelWorkPermission(stash);
|
|
66
102
|
return {
|
|
67
|
-
permission: { ...
|
|
103
|
+
permission: { ...permission },
|
|
68
104
|
compaction: { auto: false },
|
|
69
105
|
agent: {
|
|
70
106
|
[MODEL_WORK_OPENCODE_AGENT]: {
|
|
71
107
|
mode: "primary",
|
|
72
108
|
description: "akm unattended model work: read and edit inside its working directory only.",
|
|
73
109
|
prompt: MODEL_WORK_PROMPT,
|
|
74
|
-
steps: MODEL_WORK_STEPS,
|
|
75
110
|
...(options ? { options } : {}),
|
|
76
|
-
permission: { ...
|
|
111
|
+
permission: { ...permission },
|
|
77
112
|
},
|
|
78
113
|
},
|
|
79
114
|
};
|
|
@@ -21,9 +21,8 @@
|
|
|
21
21
|
* Importing the descriptor from this leaf keeps the registry a config-leaf and
|
|
22
22
|
* breaks the cycle.
|
|
23
23
|
*/
|
|
24
|
-
import { isModelWorkTools } from "../../../execution/source.js";
|
|
25
24
|
import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
|
|
26
|
-
import {
|
|
25
|
+
import { MODEL_WORK_AGENT_INFERENCE } from "../opencode/model-config.js";
|
|
27
26
|
import { caps } from "../shared.js";
|
|
28
27
|
import { BaseHarness } from "../types.js";
|
|
29
28
|
/**
|
|
@@ -43,10 +42,7 @@ export class OpencodeSdkHarness extends BaseHarness {
|
|
|
43
42
|
personaChannel: "native",
|
|
44
43
|
nativeAgentSelector: true,
|
|
45
44
|
tools: "sdk",
|
|
46
|
-
|
|
47
|
-
// The server config gives the routed model its inference entry, so the request must name a model, except
|
|
48
|
-
// that model work's agent carries its options whichever model opencode picks.
|
|
49
|
-
inference: (_profile, request) => opencodeCarriedKeys(Boolean(request.model?.resolved), request.inference, isModelWorkTools(request.tools)),
|
|
45
|
+
inference: MODEL_WORK_AGENT_INFERENCE,
|
|
50
46
|
}),
|
|
51
47
|
};
|
|
52
48
|
// No flag-shaped resume: session reuse is programmatic — the SDK session id is
|
|
@@ -93,11 +93,10 @@
|
|
|
93
93
|
import { spawn } from "node:child_process";
|
|
94
94
|
import { createHash } from "node:crypto";
|
|
95
95
|
import { isRecord } from "../../../core/common.js";
|
|
96
|
-
import { COMMON_SPAWN_ENV_PASSTHROUGH, spawnEnvNamesFor } from "../../../core/spawn-env.js";
|
|
97
|
-
import { isModelWorkTools } from "../../../execution/source.js";
|
|
96
|
+
import { COMMON_SPAWN_ENV_PASSTHROUGH, spawnEnvNamesFor, XDG_BASE_DIR_ENV_PASSTHROUGH } from "../../../core/spawn-env.js";
|
|
98
97
|
import { DEFAULT_AGENT_TIMEOUT_MS } from "../../agent/config.js";
|
|
99
|
-
import { opencodeInferenceConfig
|
|
100
|
-
import { MODEL_WORK_OPENCODE_AGENT,
|
|
98
|
+
import { opencodeInferenceConfig } from "../opencode/model-config.js";
|
|
99
|
+
import { MODEL_WORK_OPENCODE_AGENT, modelWorkOpencodeConfig, modelWorkPluginEnv } from "../opencode/model-work-agent.js";
|
|
101
100
|
// Server registry — one server per complete server-material signature. Caller
|
|
102
101
|
// deadlines race the shared promise independently; they never become startup
|
|
103
102
|
// configuration inherited by later callers.
|
|
@@ -251,12 +250,12 @@ function fallbackInference(llmConfig) {
|
|
|
251
250
|
* aliases resolve once before harness lowering. A server for model work also
|
|
252
251
|
* defines the confined model-work agent (`../opencode/model-work-agent`).
|
|
253
252
|
*
|
|
254
|
-
* The
|
|
255
|
-
* the
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
253
|
+
* The model the config routes through `akm-custom` is declared in full, with
|
|
254
|
+
* the dispatch's inference (`../opencode/model-config`): the fallback LLM
|
|
255
|
+
* engine's, under `requestInference`, the request's own. For model work the
|
|
256
|
+
* options go on the confined agent instead of the model, which needs no model
|
|
257
|
+
* named: it runs whichever model opencode picks. A model the user's own opencode
|
|
258
|
+
* config provides carries none: set inference there.
|
|
260
259
|
*/
|
|
261
260
|
export function buildSdkConfig(profile, llmConfig, modelWork = false, requestInference) {
|
|
262
261
|
const endpoint = llmConfig?.endpoint;
|
|
@@ -288,11 +287,6 @@ export function buildSdkConfig(profile, llmConfig, modelWork = false, requestInf
|
|
|
288
287
|
if (modelId)
|
|
289
288
|
sdkConfig.model = `akm-custom/${modelId}`;
|
|
290
289
|
}
|
|
291
|
-
else {
|
|
292
|
-
const modelConfig = opencodeModelConfig(model, entry);
|
|
293
|
-
if (modelConfig)
|
|
294
|
-
Object.assign(sdkConfig, modelConfig);
|
|
295
|
-
}
|
|
296
290
|
return modelWork ? { ...sdkConfig, ...modelWorkOpencodeConfig(agentOptions) } : sdkConfig;
|
|
297
291
|
}
|
|
298
292
|
/** Digest the executable and exact environment received by the child. */
|
|
@@ -302,11 +296,23 @@ function serverRegistryKey(profile, env) {
|
|
|
302
296
|
.update(JSON.stringify(canonicalize(material)))
|
|
303
297
|
.digest("hex");
|
|
304
298
|
}
|
|
305
|
-
/**
|
|
299
|
+
/**
|
|
300
|
+
* @internal Exact environment allowlist used to start the OpenCode SDK server:
|
|
301
|
+
* the common baseline, the XDG base-directory variables opencode resolves its
|
|
302
|
+
* config, data, cache and state from, and the profile's own names. The XDG names
|
|
303
|
+
* are the server's, not the profile's: profile `envPassthrough` is frozen into
|
|
304
|
+
* workflow plans, and an SDK profile's list has always been empty.
|
|
305
|
+
*/
|
|
306
306
|
export function opencodeSdkServerEnvironmentNames(profile) {
|
|
307
|
-
return [
|
|
307
|
+
return [
|
|
308
|
+
...new Set([
|
|
309
|
+
...spawnEnvNamesFor(COMMON_SPAWN_ENV_PASSTHROUGH),
|
|
310
|
+
...XDG_BASE_DIR_ENV_PASSTHROUGH,
|
|
311
|
+
...(profile.envPassthrough ?? []),
|
|
312
|
+
]),
|
|
313
|
+
];
|
|
308
314
|
}
|
|
309
|
-
function buildServerEnv(profile, config, bindings, envSource) {
|
|
315
|
+
function buildServerEnv(profile, config, bindings, envSource, modelWork) {
|
|
310
316
|
const env = {};
|
|
311
317
|
for (const key of opencodeSdkServerEnvironmentNames(profile)) {
|
|
312
318
|
const value = envSource[key];
|
|
@@ -315,6 +321,8 @@ function buildServerEnv(profile, config, bindings, envSource) {
|
|
|
315
321
|
}
|
|
316
322
|
for (const [key, value] of Object.entries(bindings ?? {}))
|
|
317
323
|
env[key] = value;
|
|
324
|
+
if (modelWork)
|
|
325
|
+
Object.assign(env, modelWorkPluginEnv());
|
|
318
326
|
env.OPENCODE_CONFIG_CONTENT = JSON.stringify(config);
|
|
319
327
|
return env;
|
|
320
328
|
}
|
|
@@ -586,7 +594,7 @@ function getOrStartServer(profile, llmConfig, env, envSource = process.env, mode
|
|
|
586
594
|
if (_testServer)
|
|
587
595
|
return { promise: Promise.resolve(_testServer), release() { } };
|
|
588
596
|
const sdkConfig = buildSdkConfig(profile, llmConfig, modelWork, inference);
|
|
589
|
-
const serverEnv = buildServerEnv(profile, sdkConfig, env, envSource);
|
|
597
|
+
const serverEnv = buildServerEnv(profile, sdkConfig, env, envSource, modelWork);
|
|
590
598
|
const key = serverRegistryKey(profile, serverEnv);
|
|
591
599
|
let entry = _servers.get(key);
|
|
592
600
|
if (!entry) {
|
|
@@ -736,51 +744,6 @@ async function deleteSessionBestEffort(client, sessionId, query, setTimeoutFn, c
|
|
|
736
744
|
function abortSessionBestEffort(client, sessionId, query) {
|
|
737
745
|
void client.session.abort?.({ path: { id: sessionId }, ...(query ? { query } : {}) }).catch(() => { });
|
|
738
746
|
}
|
|
739
|
-
/** How often a model-work session's messages are polled for its step count. */
|
|
740
|
-
const MODEL_WORK_STEP_POLL_MS = 1_000;
|
|
741
|
-
/**
|
|
742
|
-
* Count a model-work session's steps, its `step-start` parts, once a second
|
|
743
|
-
* and abort the session once they pass `MODEL_WORK_STEPS` +
|
|
744
|
-
* `MODEL_WORK_STEP_GRACE`: opencode 1.18.25 only asks the model to stop at the
|
|
745
|
-
* agent's `steps`. Polled rather than read from the server's event stream,
|
|
746
|
-
* because the SDK's stream cannot be closed mid-read without an unhandled
|
|
747
|
-
* AbortError (its abort handler drops the promise `reader.cancel()` returns),
|
|
748
|
-
* which the CLI turns into a crash. Best effort: the dispatch timeout still
|
|
749
|
-
* bounds the session.
|
|
750
|
-
*/
|
|
751
|
-
function watchModelWorkSteps(client, sessionId, query, timers) {
|
|
752
|
-
let steps = 0;
|
|
753
|
-
let timer;
|
|
754
|
-
let stopped = false;
|
|
755
|
-
const poll = async () => {
|
|
756
|
-
const listed = await client.session.messages?.({ path: { id: sessionId }, ...(query ? { query } : {}) });
|
|
757
|
-
if (stopped)
|
|
758
|
-
return;
|
|
759
|
-
steps = (listed?.data ?? [])
|
|
760
|
-
.flatMap((message) => message.parts ?? [])
|
|
761
|
-
.filter((p) => p.type === "step-start").length;
|
|
762
|
-
if (steps > MODEL_WORK_STEPS + MODEL_WORK_STEP_GRACE)
|
|
763
|
-
abortSessionBestEffort(client, sessionId, query);
|
|
764
|
-
else
|
|
765
|
-
schedule();
|
|
766
|
-
};
|
|
767
|
-
const schedule = () => {
|
|
768
|
-
if (stopped || !client.session.messages)
|
|
769
|
-
return;
|
|
770
|
-
timer = timers.setTimeoutFn(() => void poll().catch(() => schedule()), MODEL_WORK_STEP_POLL_MS);
|
|
771
|
-
if (typeof timer !== "number")
|
|
772
|
-
timer.unref?.();
|
|
773
|
-
};
|
|
774
|
-
schedule();
|
|
775
|
-
return {
|
|
776
|
-
steps: () => steps,
|
|
777
|
-
stop: () => {
|
|
778
|
-
stopped = true;
|
|
779
|
-
if (timer !== undefined)
|
|
780
|
-
timers.clearTimeoutFn(timer);
|
|
781
|
-
},
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
747
|
function abortedBeforeSdkStart(profile) {
|
|
785
748
|
return {
|
|
786
749
|
ok: false,
|
|
@@ -801,7 +764,7 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
801
764
|
const clearTimeoutImpl = opts.clearTimeoutFn ?? clearTimeout;
|
|
802
765
|
if (opts.signal?.aborted)
|
|
803
766
|
return abortedBeforeSdkStart(profile);
|
|
804
|
-
const modelWork =
|
|
767
|
+
const modelWork = opts.dispatch?.modelWork === true;
|
|
805
768
|
let client;
|
|
806
769
|
if (_testServer) {
|
|
807
770
|
client = _testServer.client;
|
|
@@ -935,7 +898,7 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
935
898
|
const dispatch = opts.dispatch;
|
|
936
899
|
const agent = modelWork ? MODEL_WORK_OPENCODE_AGENT : dispatch?.agent;
|
|
937
900
|
const system = dispatch?.systemPrompt;
|
|
938
|
-
const tools =
|
|
901
|
+
const tools = toolsToSdkAllowlist(dispatch?.tools);
|
|
939
902
|
const body = { parts: [{ type: "text", text: prompt }] };
|
|
940
903
|
if (agent)
|
|
941
904
|
body.agent = agent;
|
|
@@ -944,9 +907,6 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
944
907
|
if (tools)
|
|
945
908
|
body.tools = tools;
|
|
946
909
|
let result;
|
|
947
|
-
const steps = modelWork
|
|
948
|
-
? watchModelWorkSteps(client, sessionId, query, { setTimeoutFn: setTimeoutImpl, clearTimeoutFn: clearTimeoutImpl })
|
|
949
|
-
: undefined;
|
|
950
910
|
try {
|
|
951
911
|
const prompted = await raceSdkOperation(client.session.prompt({ path: { id: sessionId }, body, ...(query ? { query } : {}) }), {
|
|
952
912
|
timeoutMs: remainingTimeoutMs(),
|
|
@@ -957,8 +917,8 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
957
917
|
void deleteSessionBestEffort(client, sessionId, query, setTimeoutImpl, clearTimeoutImpl);
|
|
958
918
|
},
|
|
959
919
|
});
|
|
960
|
-
// A
|
|
961
|
-
if (
|
|
920
|
+
// A session the dispatch stops early is aborted on the server too.
|
|
921
|
+
if (prompted === SDK_OPERATION_ABORTED || prompted === SDK_OPERATION_TIMED_OUT) {
|
|
962
922
|
abortSessionBestEffort(client, sessionId, query);
|
|
963
923
|
}
|
|
964
924
|
if (prompted === SDK_OPERATION_ABORTED) {
|
|
@@ -994,22 +954,7 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
994
954
|
// default sdk runner.
|
|
995
955
|
const usage = extractUsage(prompted.data?.info);
|
|
996
956
|
const sdkError = prompted.error ?? prompted.data?.info?.error;
|
|
997
|
-
|
|
998
|
-
if (overSteps) {
|
|
999
|
-
const message = `opencode-sdk agent "${profile.name}" ran past the ${MODEL_WORK_STEPS}-step limit for model work; akm aborted the session.`;
|
|
1000
|
-
result = {
|
|
1001
|
-
ok: false,
|
|
1002
|
-
stdout,
|
|
1003
|
-
stderr: message,
|
|
1004
|
-
durationMs: Date.now() - start,
|
|
1005
|
-
exitCode: 1,
|
|
1006
|
-
reason: "parse_error",
|
|
1007
|
-
error: message,
|
|
1008
|
-
sessionId,
|
|
1009
|
-
...(usage ? { usage } : {}),
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
else if (sdkError) {
|
|
957
|
+
if (sdkError) {
|
|
1013
958
|
const failure = sdkErrorFailure(sdkError);
|
|
1014
959
|
result = {
|
|
1015
960
|
ok: false,
|
|
@@ -1048,9 +993,6 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
1048
993
|
sessionId,
|
|
1049
994
|
};
|
|
1050
995
|
}
|
|
1051
|
-
finally {
|
|
1052
|
-
steps?.stop();
|
|
1053
|
-
}
|
|
1054
996
|
// Clean up session to prevent disk accumulation in ~/.local/share/opencode/.
|
|
1055
997
|
// Failures are non-fatal to the agent result but must not be invisible.
|
|
1056
998
|
const cleanupWarning = await deleteSessionBestEffort(client, sessionId, query, setTimeoutImpl, clearTimeoutImpl);
|
|
@@ -59,8 +59,7 @@
|
|
|
59
59
|
* akm's `workflow_run_units` remains the durable source of truth either way
|
|
60
60
|
* (plan §"Session, MCP, and identity across harnesses").
|
|
61
61
|
* - **inference** — not translated: the shared lowering reports each field of
|
|
62
|
-
* the request's inference as untranslated
|
|
63
|
-
* lists none for this harness).
|
|
62
|
+
* the request's inference as untranslated.
|
|
64
63
|
*
|
|
65
64
|
* Registered: `openhandsBuilder` is `OpenhandsHarness.agentBuilder`
|
|
66
65
|
* (`./index.ts`), one of the ten harnesses `HARNESS_REGISTRY` constructs
|
|
@@ -40,8 +40,7 @@
|
|
|
40
40
|
* produce a silently broken command. A restrictive policy is therefore
|
|
41
41
|
* dropped rather than approximated — never silently widened.
|
|
42
42
|
* - **inference** — not translated: the shared lowering reports each field of
|
|
43
|
-
* the request's inference as untranslated
|
|
44
|
-
* lists none for this harness).
|
|
43
|
+
* the request's inference as untranslated.
|
|
45
44
|
*
|
|
46
45
|
* Registered: `piBuilder` is `PiHarness.agentBuilder` (`./index.ts`), one of
|
|
47
46
|
* the ten harnesses `HARNESS_REGISTRY` constructs (`harnesses/index.ts`);
|
package/dist/llm/feature-gate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
|
+
import { DEFAULT_LLM_TIMEOUT_MS } from "../integrations/agent/config.js";
|
|
4
5
|
/**
|
|
5
6
|
* For each feature key, return the effective enabled state by reading the
|
|
6
7
|
* 0.9.0 config shape.
|
|
@@ -37,10 +38,6 @@ export function isLlmFeatureEnabled(config, feature, improveEnabled) {
|
|
|
37
38
|
return false;
|
|
38
39
|
return resolver(config);
|
|
39
40
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Default hard timeout for every bounded in-tree LLM call.
|
|
42
|
-
*/
|
|
43
|
-
const DEFAULT_TIMEOUT_MS = 600_000;
|
|
44
41
|
/**
|
|
45
42
|
* Run `fn()` only if `isLlmFeatureEnabled(config, feature)` is `true`. On
|
|
46
43
|
* disablement, throw, or timeout, return `fallback` (or — if it is a
|
|
@@ -53,7 +50,7 @@ export async function tryLlmFeature(feature, config, fn, fallback, opts) {
|
|
|
53
50
|
opts?.onFallback?.({ feature, reason: "disabled" });
|
|
54
51
|
return resolveFallback();
|
|
55
52
|
}
|
|
56
|
-
const timeoutMs = opts && Object.hasOwn(opts, "timeoutMs") ? (opts.timeoutMs ?? null) :
|
|
53
|
+
const timeoutMs = opts && Object.hasOwn(opts, "timeoutMs") ? (opts.timeoutMs ?? null) : DEFAULT_LLM_TIMEOUT_MS;
|
|
57
54
|
try {
|
|
58
55
|
if (timeoutMs === null || timeoutMs <= 0) {
|
|
59
56
|
return await fn(new AbortController().signal);
|