akm-cli 0.9.31 → 0.10.26101002-alpha

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 (236) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/STABILITY.md +79 -53
  3. package/dist/api-node.mjs +20 -0
  4. package/dist/api.d.ts +23 -0
  5. package/dist/api.js +45 -0
  6. package/dist/assets/hints/cli-hints-full.md +13 -27
  7. package/dist/assets/hints/cli-hints-short.md +1 -1
  8. package/dist/assets/improve-strategies/default.json +3 -3
  9. package/dist/assets/prompts/consolidate-pair.md +9 -7
  10. package/dist/cli/shared.js +31 -48
  11. package/dist/cli/unknown-flags.js +6 -43
  12. package/dist/cli.js +8 -34
  13. package/dist/commands/agent/contribute-cli.js +1 -1
  14. package/dist/commands/completions.js +1 -2
  15. package/dist/commands/config-cli.js +5 -28
  16. package/dist/commands/env/env-cli.js +5 -5
  17. package/dist/commands/env/secret-cli.js +2 -2
  18. package/dist/commands/health/checks.js +41 -16
  19. package/dist/commands/health/plugin-staleness.js +52 -22
  20. package/dist/commands/improve/autonomy-gate.js +5 -3
  21. package/dist/commands/improve/consolidate/pair-pass.js +7 -1
  22. package/dist/commands/improve/consolidate.js +6 -35
  23. package/dist/commands/improve/distill/lesson-update.js +91 -0
  24. package/dist/commands/improve/distill-guards.js +1 -43
  25. package/dist/commands/improve/distill.js +140 -20
  26. package/dist/commands/improve/eligibility.js +14 -1
  27. package/dist/commands/improve/extract-cli.js +1 -1
  28. package/dist/commands/improve/improve-cli.js +87 -102
  29. package/dist/commands/improve/improve-strategies.js +8 -12
  30. package/dist/commands/improve/improve.js +2 -2
  31. package/dist/commands/improve/loop-stages.js +0 -1
  32. package/dist/commands/improve/planner.js +0 -1
  33. package/dist/commands/improve/preparation.js +40 -99
  34. package/dist/commands/improve/proactive-maintenance.js +2 -2
  35. package/dist/commands/improve/reflect-noise.js +3 -55
  36. package/dist/commands/improve/reflect.js +3 -8
  37. package/dist/commands/improve/retrieval-scope.js +2 -2
  38. package/dist/commands/improve/salience.js +2 -9
  39. package/dist/commands/improve/stage.js +14 -9
  40. package/dist/commands/lint/index.js +5 -20
  41. package/dist/commands/observability-cli.js +3 -9
  42. package/dist/commands/proposal/drain.js +39 -6
  43. package/dist/commands/proposal/proposal-cli.js +11 -31
  44. package/dist/commands/proposal/repository.js +3 -3
  45. package/dist/commands/read/knowledge.js +3 -3
  46. package/dist/commands/read/remember-cli.js +0 -13
  47. package/dist/commands/read/search-cli.js +29 -97
  48. package/dist/commands/read/search.js +0 -11
  49. package/dist/commands/read/show.js +0 -19
  50. package/dist/commands/sources/plugin-upgrade.js +164 -101
  51. package/dist/commands/sources/sources-cli.js +1 -15
  52. package/dist/commands/sources/stash-cli.js +4 -12
  53. package/dist/commands/tasks/tasks-cli.js +4 -115
  54. package/dist/commands/workflow/plan.js +11 -33
  55. package/dist/commands/workflow-cli.js +2 -7
  56. package/dist/core/adapter/adapter-ids.js +1 -2
  57. package/dist/core/adapter/adapters/agent-skills-adapter.js +0 -4
  58. package/dist/core/adapter/adapters/akm-adapter.js +9 -48
  59. package/dist/core/adapter/adapters/akm-lint.js +10 -26
  60. package/dist/core/adapter/adapters/akm-metadata.js +2 -72
  61. package/dist/core/adapter/adapters/akm-task-adapter.js +0 -5
  62. package/dist/core/adapter/adapters/akm-workflow-adapter.js +9 -28
  63. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -20
  64. package/dist/core/adapter/adapters/generic-files-adapter.js +0 -10
  65. package/dist/core/adapter/adapters/llm-wiki-adapter.js +0 -4
  66. package/dist/core/adapter/adapters/tool-dir-shared.js +0 -24
  67. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -4
  68. package/dist/core/adapter/recognize-match.js +3 -4
  69. package/dist/core/common.js +18 -2
  70. package/dist/core/config/config-schema.js +12 -15
  71. package/dist/core/config/config-types.js +6 -15
  72. package/dist/core/config/config-walker.js +6 -23
  73. package/dist/core/config/config.js +17 -80
  74. package/dist/core/config/engine-semantics.js +7 -3
  75. package/dist/core/config/schema/engines.js +7 -7
  76. package/dist/core/config/schema/improve-processes.js +8 -61
  77. package/dist/core/config/schema/improve.js +2 -21
  78. package/dist/core/config/schema/index-config.js +6 -39
  79. package/dist/core/env-secret-ref.js +2 -45
  80. package/dist/core/errors.js +1 -2
  81. package/dist/core/extra-params.js +2 -86
  82. package/dist/core/improve-result.js +11 -1
  83. package/dist/core/mutation-target.js +2 -2
  84. package/dist/core/recognition-util.js +4 -5
  85. package/dist/core/warn.js +21 -4
  86. package/dist/core/write-source.js +3 -3
  87. package/dist/execution/target-ref.js +1 -2
  88. package/dist/indexer/search/db-search.js +4 -19
  89. package/dist/indexer/search/fts-query.js +0 -19
  90. package/dist/integrations/agent/engine-resolution.js +5 -2
  91. package/dist/integrations/agent/index.js +1 -1
  92. package/dist/integrations/agent/profiles.js +41 -98
  93. package/dist/integrations/agent/request-lowering.js +3 -3
  94. package/dist/integrations/harnesses/aider/index.js +5 -0
  95. package/dist/integrations/harnesses/amazonq/index.js +1 -0
  96. package/dist/integrations/harnesses/claude/config-import.js +2 -3
  97. package/dist/integrations/harnesses/claude/index.js +5 -0
  98. package/dist/integrations/harnesses/codex/index.js +1 -0
  99. package/dist/integrations/harnesses/copilot/index.js +1 -0
  100. package/dist/integrations/harnesses/gemini/index.js +1 -0
  101. package/dist/integrations/harnesses/index.js +18 -39
  102. package/dist/integrations/harnesses/opencode/agent-builder.js +60 -35
  103. package/dist/integrations/harnesses/opencode/config-import.js +121 -52
  104. package/dist/integrations/harnesses/opencode/index.js +22 -0
  105. package/dist/integrations/harnesses/opencode/plugin-layout.js +68 -0
  106. package/dist/integrations/harnesses/opencode/session-log-types.js +12 -0
  107. package/dist/integrations/harnesses/opencode/session-log-v1.js +72 -0
  108. package/dist/integrations/harnesses/opencode/session-log-v2.js +140 -0
  109. package/dist/integrations/harnesses/opencode/session-log.js +104 -115
  110. package/dist/integrations/harnesses/opencode/version.js +98 -0
  111. package/dist/integrations/harnesses/opencode-sdk/{harness.js → index.js} +5 -17
  112. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +105 -206
  113. package/dist/integrations/harnesses/opencode-sdk/v1-adapter.js +199 -0
  114. package/dist/integrations/harnesses/opencode-sdk/v2-adapter.js +228 -0
  115. package/dist/integrations/harnesses/opencode-sdk/wire.js +66 -0
  116. package/dist/integrations/harnesses/openhands/index.js +1 -0
  117. package/dist/integrations/harnesses/pi/index.js +1 -0
  118. package/dist/integrations/harnesses/shared.js +2 -0
  119. package/dist/integrations/harnesses/types.js +2 -0
  120. package/dist/llm/client.js +41 -11
  121. package/dist/output/cli-hints.js +1 -1
  122. package/dist/output/context.js +10 -19
  123. package/dist/output/format-exempt.js +5 -8
  124. package/dist/output/shapes/helpers.js +1 -22
  125. package/dist/output/shapes/passthrough.js +0 -1
  126. package/dist/output/shapes.js +3 -31
  127. package/dist/output/stdout.js +5 -1
  128. package/dist/output/text/command-format.js +6 -1
  129. package/dist/output/text/workflow-format.js +4 -22
  130. package/dist/scripts/akm-migrate-node.js +13049 -14013
  131. package/dist/scripts/akm-migrate.js +16316 -17276
  132. package/dist/setup/detect.js +10 -4
  133. package/dist/setup/harness-config-import.js +5 -14
  134. package/dist/setup/setup.js +1 -1
  135. package/dist/setup/steps/tasks.js +12 -13
  136. package/dist/storage/repositories/task-history-repository.js +0 -5
  137. package/dist/storage/repositories/workflow-runs-repository.js +5 -15
  138. package/dist/tasks/embedded.js +2 -1
  139. package/dist/tasks/run/task-history.js +0 -2
  140. package/dist/tasks/schedule-gates.js +39 -0
  141. package/dist/tasks/scheduler-binding.js +0 -16
  142. package/dist/tasks/scheduler-sync.js +3 -97
  143. package/dist/tasks/source/bounded-document.js +1 -1
  144. package/dist/tasks/source/task-source-v4.js +2 -12
  145. package/dist/tasks/task-run-reserved-flags.js +2 -31
  146. package/dist/workflows/authoring/authoring.js +6 -6
  147. package/dist/workflows/compile.js +8 -65
  148. package/dist/workflows/exec/child-workflow.js +22 -3
  149. package/dist/workflows/exec/native-executor.js +12 -147
  150. package/dist/workflows/exec/run-workflow.js +31 -103
  151. package/dist/workflows/exec/step-work.js +9 -178
  152. package/dist/workflows/exec/unit-dispatch.js +0 -10
  153. package/dist/workflows/freeze/environment.js +3 -44
  154. package/dist/workflows/freeze/freeze.js +8 -41
  155. package/dist/workflows/freeze/targets/child-workflow.js +27 -63
  156. package/dist/workflows/freeze/targets/command.js +2 -16
  157. package/dist/workflows/freeze/targets/shell.js +0 -2
  158. package/dist/workflows/freeze/task-bindings.js +2 -28
  159. package/dist/workflows/ir/params.js +12 -2
  160. package/dist/workflows/parser.js +82 -232
  161. package/dist/workflows/plan.js +1 -1
  162. package/dist/workflows/renderer.js +10 -19
  163. package/dist/workflows/runtime/run-plan.js +11 -53
  164. package/dist/workflows/runtime/runs.js +4 -27
  165. package/dist/workflows/source-files.js +12 -16
  166. package/dist/workflows/source-semantics.js +1 -93
  167. package/docs/README.md +1 -1
  168. package/docs/agents/README.md +3 -3
  169. package/docs/agents/agent-install.md +1 -1
  170. package/docs/architecture/adapters.md +17 -31
  171. package/docs/architecture/architecture.md +4 -5
  172. package/docs/architecture/improvement.md +52 -21
  173. package/docs/architecture/internals/improve-workflow.md +15 -8
  174. package/docs/architecture/internals/indexing.md +1 -1
  175. package/docs/architecture/internals/search.md +1 -2
  176. package/docs/architecture/internals/storage-locations.md +5 -5
  177. package/docs/architecture/persisted-data-compat.md +13 -4
  178. package/docs/architecture/specs/0.9.0-decisions.md +6 -2
  179. package/docs/architecture/specs/improve-collapse-churn-detector-design.md +2 -2
  180. package/docs/architecture/specs/workflow-format-unification.md +1 -6
  181. package/docs/architecture/testing/manual-testing-checklist.md +20 -29
  182. package/docs/architecture/testing/task-workflow-manual-tests.md +106 -203
  183. package/docs/architecture/testing/testing-workflow.md +1 -1
  184. package/docs/architecture/workflow-engine.md +5 -56
  185. package/docs/guides/author-workflows.md +39 -69
  186. package/docs/guides/capture-knowledge.md +1 -1
  187. package/docs/guides/claude-code-vs-akm-workflows.md +1 -2
  188. package/docs/guides/discover-and-load.md +4 -4
  189. package/docs/guides/getting-started.md +1 -1
  190. package/docs/guides/improve-the-library.md +7 -7
  191. package/docs/guides/recipes/headless-install.md +2 -2
  192. package/docs/guides/run-workflows.md +1 -1
  193. package/docs/guides/use-with-any-agent.md +49 -4
  194. package/docs/integration/bundling-akm.md +7 -6
  195. package/docs/maintainers/curate-workmap.md +1 -2
  196. package/docs/maintainers/release-checklist.md +30 -1
  197. package/docs/posts/akm-command-tour.md +3 -3
  198. package/docs/product-surface.md +1 -1
  199. package/docs/reference/README.md +1 -0
  200. package/docs/reference/api.md +65 -0
  201. package/docs/reference/asset-types.md +3 -5
  202. package/docs/reference/bundle-types.md +2 -4
  203. package/docs/reference/cli.md +129 -171
  204. package/docs/reference/configuration.md +84 -19
  205. package/docs/reference/data-and-telemetry.md +1 -1
  206. package/docs/reference/env-and-secrets.md +2 -2
  207. package/docs/reference/supported-formats.md +3 -4
  208. package/docs/reference/tasks.md +12 -19
  209. package/docs/reference/website-sources.md +1 -1
  210. package/docs/reference/workflow-schema.md +89 -317
  211. package/docs/reference/workflows.md +20 -48
  212. package/package.json +12 -2
  213. package/schemas/akm-config.json +2 -243
  214. package/schemas/akm-task.json +2 -3
  215. package/schemas/akm-workflow.json +47 -84
  216. package/dist/assets/improve-strategies/catchup.json +0 -41
  217. package/dist/assets/improve-strategies/proactive-maintenance.json +0 -24
  218. package/dist/assets/improve-strategies/quick.json +0 -17
  219. package/dist/assets/improve-strategies/reflect-distill.json +0 -29
  220. package/dist/assets/improve-strategies/thorough.json +0 -49
  221. package/dist/assets/tasks/improve/akm-improve-catchup.yml +0 -6
  222. package/dist/assets/tasks/improve/akm-improve-frequent.yml +0 -4
  223. package/dist/assets/tasks/improve/akm-improve-nightly.yml +0 -4
  224. package/dist/cli/retired-commands.js +0 -118
  225. package/dist/commands/tasks/validate.js +0 -176
  226. package/dist/core/config/legacy-source-shape-shim.js +0 -117
  227. package/dist/integrations/harnesses/ids.js +0 -28
  228. package/dist/tasks/source/task-source-v3-frozen.js +0 -424
  229. package/dist/tasks/source/task-to-v4.js +0 -863
  230. package/dist/workflows/exec/worktree.js +0 -372
  231. package/dist/workflows/freeze/child-output-references.js +0 -83
  232. package/dist/workflows/freeze/identity.js +0 -22
  233. package/dist/workflows/freeze/targets/script.js +0 -50
  234. package/dist/workflows/freeze/targets/task.js +0 -124
  235. package/dist/workflows/github-yaml.js +0 -506
  236. package/dist/workflows/runtime/run-outputs.js +0 -80
@@ -0,0 +1,199 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * OpenCode 1 wire adapter, on `@opencode-ai/sdk` 1.2.20.
6
+ *
7
+ * Everything here is the V1 protocol and nothing else: the readiness line
8
+ * (`opencode server listening on <url>`), the SDK client, `session.create` /
9
+ * `session.prompt` / `session.abort` with the per-call `query.directory`, the
10
+ * `/event` stream, and `postSessionIdPermissionsPermissionId`. `agent`,
11
+ * `system` and `tools` ride on the prompt body. The shared server lifecycle
12
+ * lives in `sdk-runner.ts`.
13
+ */
14
+ import { isRecord } from "../../../core/common.js";
15
+ import { decodeRetryStatus, } from "./wire.js";
16
+ const queryOf = (spec) => spec.directory ? { query: { directory: spec.directory } } : {};
17
+ /**
18
+ * Best-effort token usage from a prompt response. Only numeric fields the
19
+ * server actually reported are copied; undefined when nothing usable is
20
+ * present (older servers, test fakes).
21
+ */
22
+ function extractUsage(info) {
23
+ const tokens = info?.tokens;
24
+ if (!tokens)
25
+ return undefined;
26
+ const usage = {};
27
+ if (typeof tokens.input === "number" && Number.isFinite(tokens.input))
28
+ usage.inputTokens = tokens.input;
29
+ if (typeof tokens.output === "number" && Number.isFinite(tokens.output))
30
+ usage.outputTokens = tokens.output;
31
+ if (typeof tokens.reasoning === "number" && Number.isFinite(tokens.reasoning)) {
32
+ usage.reasoningTokens = tokens.reasoning;
33
+ }
34
+ return Object.keys(usage).length > 0 ? usage : undefined;
35
+ }
36
+ /**
37
+ * Map an SDK `{ error }` result or a reply's `info.error` onto a failure
38
+ * (#1015). Both are opencode NamedErrors, `{ name, data: { message } }`. An
39
+ * aborted message is `aborted`, a reply cut off at the output limit is
40
+ * `parse_error`, and every other error (auth, API, unknown, an HTTP error
41
+ * body) is `non_zero_exit`.
42
+ */
43
+ function v1ErrorFailure(error) {
44
+ if (!isRecord(error) || typeof error.name !== "string") {
45
+ return { reason: "non_zero_exit", message: typeof error === "string" ? error : JSON.stringify(error) };
46
+ }
47
+ const detail = isRecord(error.data) ? error.data.message : undefined;
48
+ const message = typeof detail === "string" ? `${error.name}: ${detail}` : error.name;
49
+ if (error.name === "MessageAbortedError")
50
+ return { reason: "aborted", message };
51
+ if (error.name === "MessageOutputLengthError")
52
+ return { reason: "parse_error", message };
53
+ return { reason: "non_zero_exit", message };
54
+ }
55
+ /**
56
+ * Subscribe to the server's event stream. The SDK's own `event.subscribe`
57
+ * cannot be closed safely: aborting it leaves an unhandled `AbortError`
58
+ * rejection (from its un-awaited `reader.cancel()`), which akm's global
59
+ * handler turns into a process exit. This reader ends quietly on abort.
60
+ */
61
+ async function subscribeEvents(baseUrl, args) {
62
+ const eventUrl = new URL("/event", baseUrl);
63
+ if (args.query?.directory)
64
+ eventUrl.searchParams.set("directory", args.query.directory);
65
+ const response = await fetch(eventUrl, { signal: args.signal, headers: { accept: "text/event-stream" } });
66
+ if (!response.ok || !response.body)
67
+ throw new Error(`OpenCode event stream failed: HTTP ${response.status}`);
68
+ const reader = response.body.getReader();
69
+ async function* stream() {
70
+ const decoder = new TextDecoder();
71
+ let buffer = "";
72
+ try {
73
+ while (true) {
74
+ const { done, value } = await reader.read();
75
+ if (done)
76
+ return;
77
+ buffer += decoder.decode(value, { stream: true });
78
+ const frames = buffer.split("\n\n");
79
+ buffer = frames.pop() ?? "";
80
+ for (const frame of frames) {
81
+ const data = frame
82
+ .split("\n")
83
+ .filter((line) => line.startsWith("data:"))
84
+ .map((line) => line.replace(/^data:\s*/, ""))
85
+ .join("\n");
86
+ if (!data)
87
+ continue;
88
+ try {
89
+ yield JSON.parse(data);
90
+ }
91
+ catch {
92
+ /* not JSON: not an event we act on */
93
+ }
94
+ }
95
+ }
96
+ }
97
+ catch (err) {
98
+ if (!args.signal.aborted)
99
+ throw err;
100
+ }
101
+ }
102
+ return { stream: stream() };
103
+ }
104
+ /** The SDK `createOpencodeClient`, with the quiet event reader swapped in. */
105
+ async function connectV1(baseUrl) {
106
+ const { createOpencodeClient } = (await import("@opencode-ai/sdk").catch(() => {
107
+ throw new Error("OpenCode SDK not available. Install @opencode-ai/sdk or configure a CLI agent instead.");
108
+ }));
109
+ const client = createOpencodeClient({ baseUrl });
110
+ client.event = { subscribe: (args) => subscribeEvents(baseUrl, args) };
111
+ return client;
112
+ }
113
+ export const V1_ADAPTER = {
114
+ major: 1,
115
+ serveArgv: (bin, port) => [bin, "serve", "--hostname=127.0.0.1", `--port=${port}`],
116
+ // OpenCode 1 serves unauthenticated on loopback.
117
+ authorize: () => ({ env: {} }),
118
+ connect: (baseUrl) => connectV1(baseUrl),
119
+ async createSession(client, spec) {
120
+ const created = await client.session.create({ body: { title: "akm" }, ...queryOf(spec) });
121
+ return created.data?.id;
122
+ },
123
+ async prompt(client, sessionId, text, spec) {
124
+ // Forward the exact native agent selector, system prompt and tools from the
125
+ // abstract dispatch request (#564).
126
+ const body = { parts: [{ type: "text", text }] };
127
+ if (spec.agent)
128
+ body.agent = spec.agent;
129
+ if (spec.system)
130
+ body.system = spec.system;
131
+ if (spec.tools)
132
+ body.tools = spec.tools;
133
+ const prompted = await client.session.prompt({ path: { id: sessionId }, body, ...queryOf(spec) });
134
+ const parts = prompted.data?.parts ?? [];
135
+ // The last text part is the answer; earlier ones narrate the steps before it.
136
+ const textOut = parts.filter((p) => p.type === "text").at(-1)?.text ?? "";
137
+ const usage = extractUsage(prompted.data?.info);
138
+ const sdkError = prompted.error ?? prompted.data?.info?.error;
139
+ return {
140
+ text: textOut,
141
+ ...(usage ? { usage } : {}),
142
+ ...(sdkError ? { failure: v1ErrorFailure(sdkError) } : {}),
143
+ };
144
+ },
145
+ abort(client, sessionId, spec) {
146
+ void client.session.abort?.({ path: { id: sessionId }, ...queryOf(spec) }).catch(() => { });
147
+ },
148
+ canWatch(client) {
149
+ const c = client;
150
+ return Boolean(c.event?.subscribe && c.postSessionIdPermissionsPermissionId);
151
+ },
152
+ async subscribe(client, spec, signal) {
153
+ const events = client.event;
154
+ if (!events)
155
+ throw new Error("the client has no event surface");
156
+ const { stream } = await events.subscribe.call(events, { ...queryOf(spec), signal });
157
+ return stream;
158
+ },
159
+ decodeEvent(raw) {
160
+ if (!isRecord(raw) || !isRecord(raw.properties))
161
+ return undefined;
162
+ const props = raw.properties;
163
+ if (raw.type === "session.created" && isRecord(props.info)) {
164
+ const { id, parentID } = props.info;
165
+ if (typeof id !== "string")
166
+ return undefined;
167
+ return { kind: "session", id, ...(typeof parentID === "string" ? { parentId: parentID } : {}) };
168
+ }
169
+ // `action` is not in the SDK's retry type yet, but current servers send it for provider/account limits.
170
+ if (raw.type === "session.status")
171
+ return decodeRetryStatus(props.sessionID, props.status);
172
+ // `permission.asked` (OpenCode >= 1.3) / `permission.updated` (older).
173
+ if (raw.type !== "permission.asked" && raw.type !== "permission.updated")
174
+ return undefined;
175
+ const { id, sessionID } = props;
176
+ if (typeof id !== "string" || typeof sessionID !== "string")
177
+ return undefined;
178
+ const patterns = Array.isArray(props.patterns) ? props.patterns : props.pattern ? [props.pattern] : [];
179
+ return {
180
+ kind: "permission",
181
+ requestId: id,
182
+ sessionId: sessionID,
183
+ description: `${String(props.permission ?? props.type ?? "unknown")} (${patterns.join(", ")})`,
184
+ };
185
+ },
186
+ async rejectPermission(client, event, spec) {
187
+ const c = client;
188
+ const reply = c.postSessionIdPermissionsPermissionId;
189
+ if (!reply)
190
+ throw new Error("the client cannot answer permission requests");
191
+ const r = await reply.call(c, {
192
+ path: { id: event.sessionId, permissionID: event.requestId },
193
+ body: { response: "reject" },
194
+ ...queryOf(spec),
195
+ });
196
+ if (r.error)
197
+ throw new Error(JSON.stringify(r.error));
198
+ },
199
+ };
@@ -0,0 +1,228 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * OpenCode 2 wire adapter, on
6
+ * `@opencode/client` 2.0.26 (the promise client, generated types).
7
+ *
8
+ * akm owns a private `opencode serve` child (never the v2 background
9
+ * `service`). What differs from OpenCode 1, verified against the 2.0.26
10
+ * binary:
11
+ *
12
+ * - Readiness is `server listening on <url>` (no `opencode` prefix).
13
+ * - The server is basic-auth protected. akm chooses the password and hands it
14
+ * to the child as `OPENCODE_SERVER_PASSWORD` (and `OPENCODE_PASSWORD`,
15
+ * which wins when set), so nothing is scraped from stdout; the username is
16
+ * the server's default, `opencode`.
17
+ * - The working directory is the session's `location.directory`, set once at
18
+ * `session.create`. Later calls address the session by id.
19
+ * - The agent, the tool permissions (`permissions`: action/resource/effect
20
+ * rules) and the title are `session.create` fields. The prompt carries only
21
+ * text, so the system text is a session instruction entry.
22
+ * - `session.prompt` only enqueues. `session.wait` blocks until the session is
23
+ * idle, and `session.context` then holds the turn: our user message, the
24
+ * assistant messages and a closing `idle` message with the outcome.
25
+ * - Cancellation is `session.interrupt`; permission requests are answered with
26
+ * `permission.reply`; the event stream is global, so events are matched to
27
+ * the dispatch's sessions by id.
28
+ *
29
+ * Config (`OPENCODE_CONFIG_CONTENT`, built by the runner) keeps the V1 shape,
30
+ * which the 2.0.26 server translates (`model`, `provider`, `agent`, `permission`).
31
+ */
32
+ import { randomBytes } from "node:crypto";
33
+ import { OpenCode } from "@opencode/client";
34
+ import { isRecord, toErrorMessage } from "../../../core/common.js";
35
+ import { sleep } from "../../../runtime.js";
36
+ import { parseOpencodeMajor } from "../opencode/version.js";
37
+ import { decodeRetryStatus, OTHER_MAJOR_REMEDY, } from "./wire.js";
38
+ /** Key of the session instruction entry carrying the dispatch's system text (`^[a-z0-9][a-z0-9._-]*$`). */
39
+ const SYSTEM_INSTRUCTION_KEY = "akm.system";
40
+ /** The server's default basic-auth username. */
41
+ const SERVER_USERNAME = "opencode";
42
+ const POLL_INTERVAL_MS = 100;
43
+ /**
44
+ * The turn that began with the user message `userId`, once its closing `idle`
45
+ * message exists; undefined while the turn is still running.
46
+ */
47
+ export function v2Turn(messages, userId) {
48
+ const start = messages.findIndex((m) => m.id === userId);
49
+ if (start < 0)
50
+ return undefined;
51
+ const assistants = [];
52
+ for (const message of messages.slice(start + 1)) {
53
+ if (message.type === "assistant")
54
+ assistants.push(message);
55
+ else if (message.type === "idle")
56
+ return { outcome: message.outcome, assistants };
57
+ }
58
+ return undefined;
59
+ }
60
+ /** Decode a finished turn into the answer, the usage of every step and the failure, if any. */
61
+ function v2PromptResult(turn) {
62
+ const text = turn.assistants
63
+ .flatMap((a) => a.content)
64
+ .filter((c) => c.type === "text")
65
+ .at(-1)?.text ?? "";
66
+ const usage = {};
67
+ const add = (key, value) => {
68
+ if (typeof value === "number" && Number.isFinite(value))
69
+ usage[key] = (usage[key] ?? 0) + value;
70
+ };
71
+ for (const a of turn.assistants) {
72
+ add("inputTokens", a.tokens?.input);
73
+ add("outputTokens", a.tokens?.output);
74
+ add("reasoningTokens", a.tokens?.reasoning);
75
+ }
76
+ const failure = v2Failure(turn);
77
+ return {
78
+ text,
79
+ ...(Object.keys(usage).length > 0 ? { usage } : {}),
80
+ ...(failure ? { failure } : {}),
81
+ };
82
+ }
83
+ function v2Failure(turn) {
84
+ if (turn.outcome === "interrupted") {
85
+ return { reason: "aborted", message: "OpenCode session was interrupted" };
86
+ }
87
+ const last = turn.assistants.at(-1);
88
+ if (turn.outcome === "failed") {
89
+ const error = [...turn.assistants].reverse().find((a) => a.error)?.error;
90
+ return {
91
+ reason: "non_zero_exit",
92
+ message: error ? `${error.type}: ${error.message}` : "OpenCode session failed",
93
+ };
94
+ }
95
+ // A reply cut off at the output limit has no error of its own.
96
+ if (last?.finish === "length") {
97
+ return { reason: "parse_error", message: "OpenCode reply was cut off at the output limit" };
98
+ }
99
+ return undefined;
100
+ }
101
+ /** Tool enable/disable map -> session permission rules. */
102
+ function toolPermissions(tools) {
103
+ return Object.entries(tools ?? {}).map(([action, enabled]) => ({
104
+ action,
105
+ resource: "*",
106
+ effect: enabled ? "allow" : "deny",
107
+ }));
108
+ }
109
+ export const V2_ADAPTER = {
110
+ major: 2,
111
+ serveArgv: (bin, port) => [bin, "serve", "--hostname", "127.0.0.1", "--port", String(port)],
112
+ authorize() {
113
+ const password = randomBytes(24).toString("base64url");
114
+ return { env: { OPENCODE_SERVER_PASSWORD: password, OPENCODE_PASSWORD: password }, credentials: password };
115
+ },
116
+ async connect(baseUrl, credentials) {
117
+ const client = OpenCode.make({
118
+ baseUrl,
119
+ headers: { authorization: `Basic ${Buffer.from(`${SERVER_USERNAME}:${credentials ?? ""}`).toString("base64")}` },
120
+ });
121
+ let version;
122
+ try {
123
+ version = (await client.server.info()).version;
124
+ }
125
+ catch (err) {
126
+ throw new Error(`Could not read the OpenCode 2 server info at ${baseUrl}: ${toErrorMessage(err)}. ${OTHER_MAJOR_REMEDY}`);
127
+ }
128
+ // Only OpenCode 1 is refused: a newer major than akm knows runs on these adapters (`../opencode/version.ts`).
129
+ if (parseOpencodeMajor(version) === 1) {
130
+ throw new Error(`The server is OpenCode ${version}, not OpenCode 2. ${OTHER_MAJOR_REMEDY}`);
131
+ }
132
+ return client;
133
+ },
134
+ async createSession(client, spec) {
135
+ const c = client;
136
+ const permissions = toolPermissions(spec.tools);
137
+ const session = await c.session.create({
138
+ title: "akm",
139
+ ...(spec.agent ? { agent: spec.agent } : {}),
140
+ ...(spec.directory ? { location: { directory: spec.directory } } : {}),
141
+ ...(permissions.length > 0 ? { permissions } : {}),
142
+ });
143
+ if (spec.system) {
144
+ await c.session.instructions.entry.put({
145
+ sessionID: session.id,
146
+ key: SYSTEM_INSTRUCTION_KEY,
147
+ value: spec.system,
148
+ });
149
+ }
150
+ return session.id;
151
+ },
152
+ async prompt(client, sessionId, text, _spec, signal) {
153
+ const c = client;
154
+ const user = await c.session.prompt({ sessionID: sessionId, text }, { signal });
155
+ for (;;) {
156
+ await c.session.wait({ sessionID: sessionId }, { signal });
157
+ const turn = v2Turn(await c.session.context({ sessionID: sessionId }, { signal }), user.id);
158
+ if (turn)
159
+ return v2PromptResult(turn);
160
+ // `wait` can return before the queued prompt starts; look again.
161
+ await sleep(POLL_INTERVAL_MS);
162
+ if (signal.aborted)
163
+ throw new Error("OpenCode prompt cancelled");
164
+ }
165
+ },
166
+ abort(client, sessionId) {
167
+ void client.session.interrupt({ sessionID: sessionId }).catch(() => { });
168
+ },
169
+ async subscribe(client, _spec, signal) {
170
+ const source = client.event.subscribe({ signal })[Symbol.asyncIterator]();
171
+ // The stream's first event is `server.connected`: reading it proves the subscription is open.
172
+ const first = await source.next();
173
+ async function* stream() {
174
+ try {
175
+ if (!first.done)
176
+ yield first.value;
177
+ while (true) {
178
+ const next = await source.next();
179
+ if (next.done)
180
+ return;
181
+ yield next.value;
182
+ }
183
+ }
184
+ catch (err) {
185
+ if (!signal.aborted)
186
+ throw err;
187
+ }
188
+ }
189
+ return stream();
190
+ },
191
+ decodeEvent(raw) {
192
+ if (!isRecord(raw) || typeof raw.type !== "string" || !isRecord(raw.data))
193
+ return undefined;
194
+ // Shape-checked below, field by field; the generated `V2Event` union documents what each carries.
195
+ const data = raw.data;
196
+ if (raw.type === "session.created") {
197
+ if (typeof data.sessionID !== "string")
198
+ return undefined;
199
+ return {
200
+ kind: "session",
201
+ id: data.sessionID,
202
+ ...(typeof data.parentID === "string" ? { parentId: data.parentID } : {}),
203
+ };
204
+ }
205
+ if (raw.type === "session.status")
206
+ return decodeRetryStatus(data.sessionID, data.status);
207
+ if (raw.type === "permission.asked") {
208
+ const { id, sessionID, action, resources } = data;
209
+ if (typeof id !== "string" || typeof sessionID !== "string")
210
+ return undefined;
211
+ const patterns = Array.isArray(resources) ? resources : [];
212
+ return {
213
+ kind: "permission",
214
+ requestId: id,
215
+ sessionId: sessionID,
216
+ description: `${String(action ?? "unknown")} (${patterns.join(", ")})`,
217
+ };
218
+ }
219
+ return undefined;
220
+ },
221
+ async rejectPermission(client, event) {
222
+ await client.permission.reply({
223
+ sessionID: event.sessionId,
224
+ requestID: event.requestId,
225
+ decision: "reject",
226
+ });
227
+ },
228
+ };
@@ -0,0 +1,66 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * The seam between the OpenCode SDK runner and one OpenCode major's wire
6
+ * protocol (#1049).
7
+ *
8
+ * `sdk-runner.ts` owns everything the two majors share: the managed
9
+ * `opencode serve` child (spawn, port, registry, cleanup), the child
10
+ * environment, deadlines, abort handling and result shaping. A
11
+ * {@link OpencodeWireAdapter} owns what differs: how to ask the binary to
12
+ * serve, how to read its readiness line, how to authenticate, and how to
13
+ * create a session, prompt it, abort it, watch its events and answer its
14
+ * permission requests. `v1-adapter.ts` speaks OpenCode 1 through
15
+ * `@opencode-ai/sdk`; `v2-adapter.ts` speaks OpenCode 2 through
16
+ * `@opencode/client`.
17
+ *
18
+ * Selection is by detection (`../opencode/version.ts` runs `<bin> --version`
19
+ * once per binary). An adapter that meets a server of the other major fails and
20
+ * names the remedy ({@link otherMajorRemedy}); it never falls back to the other
21
+ * protocol.
22
+ */
23
+ import { isRecord } from "../../../core/common.js";
24
+ /** The npm client package each major's adapter imports. Setup and health check the one the engine selects. */
25
+ export const OPENCODE_CLIENT_PACKAGE = {
26
+ 1: "@opencode-ai/sdk",
27
+ 2: "@opencode/client",
28
+ };
29
+ /** The sentence every wrong-major failure ends with. */
30
+ export const OTHER_MAJOR_REMEDY = `Upgrade to OpenCode 2, or set this engine's "bin" to the OpenCode binary you want to run.`;
31
+ /** What each major's `serve` prints once it listens; the other major's line names a wrong binary. */
32
+ const READINESS_PREFIX = {
33
+ 1: "opencode server listening",
34
+ 2: "server listening on",
35
+ };
36
+ /** Judge one output line of a server started for `own`: its readiness line, the other major's, or neither. */
37
+ export function parseReadiness(line, own) {
38
+ const other = own === 1 ? 2 : 1;
39
+ if (line.startsWith(READINESS_PREFIX[own])) {
40
+ const match = line.match(/on\s+(https?:\/\/\S+)/);
41
+ return match?.[1] ? { url: match[1] } : { error: `Failed to parse the OpenCode server url from: ${line}` };
42
+ }
43
+ if (line.startsWith(READINESS_PREFIX[other])) {
44
+ return { error: `The binary is OpenCode ${other} (${line.trim()}). ${OTHER_MAJOR_REMEDY}` };
45
+ }
46
+ return undefined;
47
+ }
48
+ /**
49
+ * A `session.status` payload as the runner's retry event, or undefined for any
50
+ * other status. Both majors carry the same fields; `action` names the provider
51
+ * and, for an account usage limit, `reason: "account_rate_limit"` (#1108).
52
+ */
53
+ export function decodeRetryStatus(sessionID, status) {
54
+ if (typeof sessionID !== "string" || !isRecord(status) || status.type !== "retry")
55
+ return undefined;
56
+ const action = isRecord(status.action) ? status.action : undefined;
57
+ return {
58
+ kind: "retry",
59
+ sessionId: sessionID,
60
+ ...(typeof status.attempt === "number" ? { attempt: status.attempt } : {}),
61
+ message: typeof status.message === "string" ? status.message : "OpenCode is retrying",
62
+ ...(typeof status.next === "number" ? { next: status.next } : {}),
63
+ ...(typeof action?.provider === "string" ? { provider: action.provider } : {}),
64
+ accountLimit: action?.reason === "account_rate_limit",
65
+ };
66
+ }
@@ -27,6 +27,7 @@ export class OpenhandsHarness extends BaseHarness {
27
27
  id = "openhands";
28
28
  displayName = "OpenHands";
29
29
  agentBuilder = openhandsBuilder;
30
+ profile = { bin: "openhands", args: [], envPassthrough: ["LLM_MODEL", "LLM_API_KEY", "LLM_BASE_URL"] };
30
31
  resultExtractor = openhandsResultExtractor;
31
32
  // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
32
33
  // No flag-shaped resume: per the matrix OpenHands resumes from workspace state, not a
@@ -27,6 +27,7 @@ export class PiHarness extends BaseHarness {
27
27
  id = "pi";
28
28
  displayName = "Pi";
29
29
  agentBuilder = piBuilder;
30
+ profile = { bin: "pi", args: [], envPassthrough: ["PI_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"] };
30
31
  resultExtractor = piResultExtractor;
31
32
  // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
32
33
  // Session-id env marker only — the matrix's bare PI_* presence vars must
@@ -7,6 +7,8 @@ export function caps(c) {
7
7
  agentDispatch: false,
8
8
  detection: false,
9
9
  configImport: false,
10
+ modelWork: false,
11
+ nativeAgent: false,
10
12
  ...c,
11
13
  };
12
14
  }
@@ -46,6 +46,8 @@ export function isSessionLogHarness(h) {
46
46
  export class BaseHarness {
47
47
  setupDetectionDir;
48
48
  agentBuilder;
49
+ profile;
50
+ configImporter;
49
51
  executionLowerer;
50
52
  identityEnv;
51
53
  presenceEnv;
@@ -49,10 +49,14 @@ function htmlExcerpt(body) {
49
49
  export class LlmCallError extends Error {
50
50
  code;
51
51
  statusCode;
52
- constructor(message, code, statusCode) {
52
+ namesResponseFormat;
53
+ constructor(message, code, statusCode,
54
+ /** A 5xx whose body names the response format; set from the whole body, since the message keeps only its first 200 characters. */
55
+ namesResponseFormat = false) {
53
56
  super(message);
54
57
  this.code = code;
55
58
  this.statusCode = statusCode;
59
+ this.namesResponseFormat = namesResponseFormat;
56
60
  this.name = "LlmCallError";
57
61
  }
58
62
  }
@@ -275,6 +279,8 @@ const THINKING_FIELD_NAMED = /chat_template_kwargs|enable_thinking/;
275
279
  const TEMPERATURE_NAMED = /'temperature'/;
276
280
  /** `Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead.` */
277
281
  const MAX_TOKENS_REPLACED = /max_completion_tokens/;
282
+ /** A gateway that cannot honour the schema may answer 5xx: `502 {"error":{"code":"format_ignored"}}` (#1150). */
283
+ const RESPONSE_FORMAT_NAMED = /format_ignored|response_format|json_schema/;
278
284
  /**
279
285
  * A single chat-completion attempt: one HTTP request/response cycle, with an
280
286
  * inline fallback-once when a schema was requested and the provider 4xx's
@@ -295,24 +301,41 @@ const MAX_TOKENS_REPLACED = /max_completion_tokens/;
295
301
  async function chatCompletionAttempt(config, messages, options, timeoutMs, includeThinking = (options?.enableThinking ?? config.enableThinking) !== undefined &&
296
302
  !isThinkingControlKnownRejected(config)) {
297
303
  const wantsSchema = Boolean(options?.responseSchema) && config.supportsJsonSchema !== false && !isJsonSchemaKnownUnsupported(config);
304
+ // The schema attempt's error record is held, not emitted: when the retry without the schema
305
+ // recovers the call, that call is ONE success, not a failure plus a success. Every other exit
306
+ // flushes the held record, so a failed call is still recorded once.
307
+ const held = wantsSchema ? {} : undefined;
308
+ const flushHeld = () => {
309
+ if (held?.record)
310
+ emitLlmUsage(held.record);
311
+ };
298
312
  try {
299
- return await chatCompletionAttemptOnce(config, messages, options, timeoutMs, wantsSchema, includeThinking);
313
+ return await chatCompletionAttemptOnce(config, messages, options, timeoutMs, wantsSchema, includeThinking, held);
300
314
  }
301
315
  catch (err) {
302
- if (!isRejectedRequest(err))
316
+ // A 5xx that names the response format is answered like the 4xx rejection below (#1150).
317
+ if (wantsSchema && err instanceof LlmCallError && err.namesResponseFormat) {
318
+ return retryWithoutSchema(config, messages, options, timeoutMs, includeThinking, err);
319
+ }
320
+ if (!isRejectedRequest(err)) {
321
+ flushHeld();
303
322
  throw err;
323
+ }
304
324
  const key = connectionKey(config);
305
325
  if (TEMPERATURE_NAMED.test(err.message) && !temperatureRejectedConnections.has(key)) {
326
+ flushHeld();
306
327
  warnVerbose(`[akm] LLM rejected temperature (${err.statusCode}); retrying once without it: ${err.message}`);
307
328
  temperatureRejectedConnections.add(key);
308
329
  return chatCompletionAttempt(config, messages, options, timeoutMs, includeThinking);
309
330
  }
310
331
  if (MAX_TOKENS_REPLACED.test(err.message) && !maxCompletionTokensConnections.has(key)) {
332
+ flushHeld();
311
333
  warnVerbose(`[akm] LLM rejected max_tokens (${err.statusCode}); retrying once with max_completion_tokens: ${err.message}`);
312
334
  maxCompletionTokensConnections.add(key);
313
335
  return chatCompletionAttempt(config, messages, options, timeoutMs, includeThinking);
314
336
  }
315
337
  if (includeThinking && THINKING_FIELD_NAMED.test(err.message)) {
338
+ flushHeld();
316
339
  warnVerbose(`[akm] LLM rejected chat_template_kwargs/enable_thinking (${err.statusCode}); retrying once without them: ${err.message}`);
317
340
  const fallback = await chatCompletionAttempt(config, messages, options, timeoutMs, false);
318
341
  thinkingControlRejectedConnections.add(connectionKey(config));
@@ -320,13 +343,16 @@ async function chatCompletionAttempt(config, messages, options, timeoutMs, inclu
320
343
  }
321
344
  if (!wantsSchema)
322
345
  throw err;
323
- warnVerbose(`[akm] LLM rejected response_format:json_schema (${err.statusCode}); retrying once without it: ${err.message}`);
324
- const fallback = await chatCompletionAttemptOnce(config, messages, options, timeoutMs, false, includeThinking);
325
- jsonSchemaUnsupportedConnections.add(connectionKey(config));
326
- return fallback;
346
+ return retryWithoutSchema(config, messages, options, timeoutMs, includeThinking, err);
327
347
  }
328
348
  }
329
- async function chatCompletionAttemptOnce(config, messages, options, timeoutMs, includeSchema, includeThinking) {
349
+ async function retryWithoutSchema(config, messages, options, timeoutMs, includeThinking, err) {
350
+ warnVerbose(`[akm] LLM rejected response_format:json_schema (${err.statusCode}); retrying once without it: ${err.message}`);
351
+ const fallback = await chatCompletionAttemptOnce(config, messages, options, timeoutMs, false, includeThinking);
352
+ jsonSchemaUnsupportedConnections.add(connectionKey(config));
353
+ return fallback;
354
+ }
355
+ async function chatCompletionAttemptOnce(config, messages, options, timeoutMs, includeSchema, includeThinking, held) {
330
356
  if (config.extraParams !== undefined) {
331
357
  const issue = validateExtraParams(config.extraParams)[0];
332
358
  if (issue)
@@ -440,7 +466,7 @@ async function chatCompletionAttemptOnce(config, messages, options, timeoutMs, i
440
466
  throw new LlmCallError(`LLM provider returned HTML instead of JSON (${status}) ${config.endpoint}: ${redactSensitiveText(htmlExcerpt(rawBody), resolvedKey ? [resolvedKey] : [])}`, "provider_html_error", status);
441
467
  }
442
468
  if (status >= 500) {
443
- throw new LlmCallError(`LLM provider error (${status}) ${config.endpoint}: ${safeBody}`, "provider_error", status);
469
+ throw new LlmCallError(`LLM provider error (${status}) ${config.endpoint}: ${safeBody}`, "provider_error", status, RESPONSE_FORMAT_NAMED.test(rawBody));
444
470
  }
445
471
  throw new LlmCallError(`LLM request failed (${status}) ${config.endpoint}: ${safeBody}`, "provider_error", status);
446
472
  }
@@ -499,12 +525,16 @@ async function chatCompletionAttemptOnce(config, messages, options, timeoutMs, i
499
525
  return result;
500
526
  }
501
527
  catch (err) {
502
- emitLlmUsage({
528
+ const errorRecord = {
503
529
  ...terminalFields,
504
530
  outcome: "error",
505
531
  durationMs: Date.now() - requestStartedAt,
506
532
  errorCode: err instanceof LlmCallError ? err.code : "unknown_error",
507
- });
533
+ };
534
+ if (held)
535
+ held.record = errorRecord;
536
+ else
537
+ emitLlmUsage(errorRecord);
508
538
  throw err;
509
539
  }
510
540
  }
@@ -2,7 +2,7 @@
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
- * Embedded agent CLI guide rendered by `akm hints` and `akm help agents`.
5
+ * Embedded agent CLI guide rendered by `akm hints`.
6
6
  *
7
7
  * Extracted from `src/cli.ts` so it does not bloat the CLI module and so
8
8
  * docs/CI tooling can re-use the same constants. Two flavors: