lark-remote 0.1.5 → 0.1.7

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 (190) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.en.md +5 -1
  3. package/README.md +10 -2
  4. package/dist/bridge/approval-coordinator.d.ts +131 -0
  5. package/dist/bridge/approval-coordinator.js +409 -0
  6. package/dist/bridge/inbound-media.d.ts +62 -0
  7. package/dist/bridge/inbound-media.js +282 -0
  8. package/dist/bridge/index.d.ts +138 -5
  9. package/dist/bridge/index.js +569 -65
  10. package/dist/bridge/queue-manager.d.ts +11 -1
  11. package/dist/bridge/queue-manager.js +20 -11
  12. package/dist/card/approval-render.d.ts +16 -0
  13. package/dist/card/approval-render.js +363 -0
  14. package/dist/card/bash-card-session.d.ts +2 -24
  15. package/dist/card/bash-card-session.js +6 -73
  16. package/dist/card/bash-renderer.js +2 -3
  17. package/dist/card/card-budget.js +29 -75
  18. package/dist/card/card-session.d.ts +48 -0
  19. package/dist/card/card-session.js +86 -0
  20. package/dist/card/card-shared.d.ts +13 -10
  21. package/dist/card/card-shared.js +27 -24
  22. package/dist/card/run-card-session.d.ts +2 -55
  23. package/dist/card/run-card-session.js +6 -104
  24. package/dist/card/run-renderer.js +140 -202
  25. package/dist/card/run-state.d.ts +38 -3
  26. package/dist/card/run-state.js +221 -11
  27. package/dist/card/text-truncate.d.ts +13 -12
  28. package/dist/card/text-truncate.js +13 -63
  29. package/dist/common/fs.d.ts +8 -0
  30. package/dist/common/fs.js +17 -0
  31. package/dist/common/guards.d.ts +0 -20
  32. package/dist/common/guards.js +0 -43
  33. package/dist/common/sleep.d.ts +2 -0
  34. package/dist/common/sleep.js +5 -0
  35. package/dist/common/truncate.d.ts +39 -0
  36. package/dist/common/truncate.js +92 -0
  37. package/dist/config/codex-config.d.ts +1 -1
  38. package/dist/config/codex-config.js +1 -1
  39. package/dist/config/dir.js +1 -1
  40. package/dist/config/index.d.ts +116 -9
  41. package/dist/config/index.js +122 -29
  42. package/dist/connector/index.d.ts +74 -2
  43. package/dist/connector/index.js +119 -1
  44. package/dist/index.js +139 -99
  45. package/dist/instance-lock.js +4 -12
  46. package/dist/order/alias-resolve.d.ts +13 -0
  47. package/dist/order/alias-resolve.js +22 -0
  48. package/dist/order/alias-store.d.ts +38 -0
  49. package/dist/order/alias-store.js +106 -0
  50. package/dist/persistence/atomic-write.d.ts +8 -2
  51. package/dist/persistence/atomic-write.js +23 -7
  52. package/dist/restart.js +2 -1
  53. package/dist/router/card-helpers.d.ts +63 -0
  54. package/dist/router/card-helpers.js +146 -0
  55. package/dist/router/config/claude.js +25 -1
  56. package/dist/router/config/codex.d.ts +1 -1
  57. package/dist/router/config/codex.js +33 -5
  58. package/dist/router/config/common/model-patch.d.ts +22 -0
  59. package/dist/router/config/common/model-patch.js +26 -0
  60. package/dist/router/config/common/render.js +9 -4
  61. package/dist/router/config/index.d.ts +12 -0
  62. package/dist/router/config/index.js +25 -1
  63. package/dist/router/config/kimi.d.ts +4 -1
  64. package/dist/router/config/kimi.js +27 -0
  65. package/dist/router/config/opencode.js +18 -5
  66. package/dist/router/config/pi.js +4 -5
  67. package/dist/router/config/types.d.ts +12 -3
  68. package/dist/router/index.d.ts +77 -17
  69. package/dist/router/index.js +455 -549
  70. package/dist/router/utils.d.ts +4 -0
  71. package/dist/router/utils.js +9 -2
  72. package/dist/runner/agent-choices-common.d.ts +16 -0
  73. package/dist/runner/agent-choices-common.js +31 -0
  74. package/dist/runner/claude/runner.d.ts +47 -10
  75. package/dist/runner/claude/runner.js +117 -46
  76. package/dist/runner/claude/session.d.ts +166 -0
  77. package/dist/runner/claude/session.js +749 -0
  78. package/dist/runner/codex/app-server/protocol-types.d.ts +519 -0
  79. package/dist/runner/codex/app-server/protocol-types.js +56 -0
  80. package/dist/runner/codex/app-server/runner.d.ts +104 -0
  81. package/dist/runner/codex/app-server/runner.js +377 -0
  82. package/dist/runner/codex/app-server/translator.d.ts +89 -0
  83. package/dist/runner/codex/app-server/translator.js +716 -0
  84. package/dist/runner/codex/index.d.ts +1 -1
  85. package/dist/runner/codex/index.js +1 -1
  86. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  87. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  88. package/dist/runner/common/acp/protocol-types.d.ts +238 -0
  89. package/dist/runner/common/acp/protocol-types.js +75 -0
  90. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  91. package/dist/runner/common/connection-based-runner.js +261 -0
  92. package/dist/runner/common/jsonrpc/client.d.ts +89 -0
  93. package/dist/runner/common/jsonrpc/client.js +243 -0
  94. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  95. package/dist/runner/common/jsonrpc/connection-manager.js +205 -0
  96. package/dist/runner/common/jsonrpc/transport.d.ts +50 -0
  97. package/dist/runner/common/jsonrpc/transport.js +230 -0
  98. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  99. package/dist/runner/common/jsonrpc/types.js +9 -0
  100. package/dist/runner/common/process-stopper.d.ts +2 -1
  101. package/dist/runner/common/process-stopper.js +1 -0
  102. package/dist/runner/common/runner-utils.d.ts +6 -13
  103. package/dist/runner/common/runner-utils.js +6 -47
  104. package/dist/runner/common/spawning-runner.d.ts +24 -70
  105. package/dist/runner/common/spawning-runner.js +98 -152
  106. package/dist/runner/common/usage.d.ts +2 -0
  107. package/dist/runner/common/usage.js +2 -0
  108. package/dist/runner/index.d.ts +5 -5
  109. package/dist/runner/index.js +4 -4
  110. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  111. package/dist/runner/kimi/acp/runner.js +448 -0
  112. package/dist/runner/kimi/acp/translator.d.ts +124 -0
  113. package/dist/runner/kimi/acp/translator.js +336 -0
  114. package/dist/runner/kimi/index.d.ts +1 -1
  115. package/dist/runner/kimi/index.js +1 -1
  116. package/dist/runner/opencode/acp/index.d.ts +1 -0
  117. package/dist/runner/opencode/acp/index.js +2 -0
  118. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  119. package/dist/runner/opencode/acp/runner.js +429 -0
  120. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  121. package/dist/runner/opencode/acp/translator.js +345 -0
  122. package/dist/runner/opencode/index.d.ts +1 -1
  123. package/dist/runner/opencode/index.js +1 -1
  124. package/dist/runner/pi/index.d.ts +1 -1
  125. package/dist/runner/pi/index.js +1 -1
  126. package/dist/runner/pi/rpc/client.d.ts +52 -0
  127. package/dist/runner/pi/rpc/client.js +114 -0
  128. package/dist/runner/pi/rpc/index.d.ts +4 -0
  129. package/dist/runner/pi/rpc/index.js +4 -0
  130. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  131. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  132. package/dist/runner/pi/rpc/runner.d.ts +80 -0
  133. package/dist/runner/pi/rpc/runner.js +199 -0
  134. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  135. package/dist/runner/pi/rpc/translator.js +258 -0
  136. package/dist/runner/registry.d.ts +7 -8
  137. package/dist/runner/registry.js +3 -4
  138. package/dist/runner/resolve-agent-choices.js +18 -59
  139. package/dist/runner/sync-agent-choices.js +16 -67
  140. package/dist/runner/types.d.ts +153 -2
  141. package/dist/session/claude/session-index.js +2 -3
  142. package/dist/session/claude/session-reader.js +3 -6
  143. package/dist/session/claude/sessions.js +6 -13
  144. package/dist/session/codex/rollout-reader.d.ts +2 -2
  145. package/dist/session/codex/rollout-reader.js +57 -17
  146. package/dist/session/codex/sessions.d.ts +1 -1
  147. package/dist/session/codex/sessions.js +2 -2
  148. package/dist/session/common/content-blocks.d.ts +7 -0
  149. package/dist/session/common/content-blocks.js +13 -9
  150. package/dist/session/common/jsonl.d.ts +8 -25
  151. package/dist/session/common/jsonl.js +126 -261
  152. package/dist/session/common/pagination.d.ts +28 -0
  153. package/dist/session/common/pagination.js +33 -0
  154. package/dist/session/kimi/sessions.d.ts +34 -1
  155. package/dist/session/kimi/sessions.js +208 -98
  156. package/dist/session/opencode/sessions.d.ts +17 -3
  157. package/dist/session/opencode/sessions.js +29 -29
  158. package/dist/session/pi/sessions.d.ts +5 -1
  159. package/dist/session/pi/sessions.js +12 -16
  160. package/dist/session/session-store.d.ts +11 -1
  161. package/dist/session/session-store.js +75 -178
  162. package/dist/update/index.d.ts +2 -2
  163. package/dist/update/index.js +2 -2
  164. package/dist/update/install.d.ts +20 -2
  165. package/dist/update/install.js +51 -7
  166. package/package.json +1 -1
  167. package/dist/runner/codex/argv.d.ts +0 -33
  168. package/dist/runner/codex/argv.js +0 -40
  169. package/dist/runner/codex/jsonl.d.ts +0 -37
  170. package/dist/runner/codex/jsonl.js +0 -221
  171. package/dist/runner/codex/runner.d.ts +0 -63
  172. package/dist/runner/codex/runner.js +0 -124
  173. package/dist/runner/common/exec-translator.d.ts +0 -73
  174. package/dist/runner/common/exec-translator.js +0 -105
  175. package/dist/runner/kimi/jsonl.d.ts +0 -28
  176. package/dist/runner/kimi/jsonl.js +0 -143
  177. package/dist/runner/kimi/runner.d.ts +0 -49
  178. package/dist/runner/kimi/runner.js +0 -105
  179. package/dist/runner/opencode/argv.d.ts +0 -32
  180. package/dist/runner/opencode/argv.js +0 -35
  181. package/dist/runner/opencode/jsonl.d.ts +0 -68
  182. package/dist/runner/opencode/jsonl.js +0 -295
  183. package/dist/runner/opencode/runner.d.ts +0 -61
  184. package/dist/runner/opencode/runner.js +0 -93
  185. package/dist/runner/pi/jsonl.d.ts +0 -128
  186. package/dist/runner/pi/jsonl.js +0 -249
  187. package/dist/runner/pi/runner.d.ts +0 -36
  188. package/dist/runner/pi/runner.js +0 -76
  189. package/dist/session/common/catch-up.d.ts +0 -12
  190. package/dist/session/common/catch-up.js +0 -31
@@ -0,0 +1,429 @@
1
+ /**
2
+ * OpencodeAcpRunner: workspace-lifetime runner using opencode's ACP mode
3
+ * (`opencode acp`, JSON-RPC over stdio). Registered as the opencode agent's
4
+ * runner.
5
+ *
6
+ * Flow per run: acquire persistent connection → session/new or session/resume
7
+ * → synthetic init → session/prompt → consume session/update notifications
8
+ * until prompt settles → result event.
9
+ *
10
+ * Structurally aligned with KimiAcpRunner via the shared ConnectionBasedRunner
11
+ * base (same notification queue + waitResolve pattern, same forceFinish/
12
+ * stopRequested semantics, same turn idle timeout, same ConnectionLostError
13
+ * retry). Differences from kimi:
14
+ * - session/set_mode applies the configured mode (build/plan; default 'build'
15
+ * keeps manual approvals flowing); opencode sends no set_mode notification,
16
+ * so the client refreshes the local mode view itself
17
+ * - no wire.jsonl compaction polling (opencode /compact is synchronous:
18
+ * service.ts:555-571 summarize completes before the prompt response)
19
+ * - text/thinking go to the assistant incremental channel (opencode wire
20
+ * deltas), not turn_diff snapshots
21
+ */
22
+ import { ConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
23
+ import { OpencodeAcpTranslator } from './translator.js';
24
+ import { RpcErrorCode, } from '../../common/acp/protocol-types.js';
25
+ import { findOptionIdByKind } from '../../common/acp/protocol-helpers.js';
26
+ import { getLogger } from '../../../logger/index.js';
27
+ import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
28
+ /**
29
+ * Build the ACP protocol response for a permission approval decision.
30
+ *
31
+ * accept → {outcome:{outcome:'selected', optionId:<allow_once kind>}}
32
+ * accept_for_session → {outcome:{outcome:'selected', optionId:<allow_always kind>}}
33
+ * decline → {outcome:{outcome:'selected', optionId:<reject kind>}}
34
+ * cancel → {outcome:{outcome:'cancelled'}}
35
+ *
36
+ * optionId is opaque and echoed back as-is (opencode permission.ts:20-24
37
+ * offers once/always/reject with kinds allow_once/allow_always/reject_once;
38
+ * the server maps optionId 'once'/'always' → approve, anything else →
39
+ * reject — permission.ts:219-223). If no matching option is found, fall
40
+ * back to cancelled (safe universal default).
41
+ */
42
+ function buildApprovalResponse(action, pending) {
43
+ if (action === 'cancel') {
44
+ return { outcome: { outcome: 'cancelled' } };
45
+ }
46
+ if (action === 'accept') {
47
+ const optionId = findOptionIdByKind(pending.options, ['allow_once', 'allow_always']);
48
+ if (optionId) {
49
+ return { outcome: { outcome: 'selected', optionId } };
50
+ }
51
+ return { outcome: { outcome: 'cancelled' } };
52
+ }
53
+ if (action === 'accept_for_session') {
54
+ // §P4: 卡片「允许本次会话」→ always 类 optionId(opencode 'always')
55
+ const optionId = findOptionIdByKind(pending.options, ['allow_always', 'approve_always']);
56
+ if (optionId) {
57
+ return { outcome: { outcome: 'selected', optionId } };
58
+ }
59
+ return { outcome: { outcome: 'cancelled' } };
60
+ }
61
+ // decline
62
+ const optionId = findOptionIdByKind(pending.options, ['reject_once', 'reject']);
63
+ if (optionId) {
64
+ return { outcome: { outcome: 'selected', optionId } };
65
+ }
66
+ return { outcome: { outcome: 'cancelled' } };
67
+ }
68
+ // =============================================================================
69
+ // Runner
70
+ // =============================================================================
71
+ export class OpencodeAcpRunner extends ConnectionBasedRunner {
72
+ connectionManager;
73
+ currentTranslator = null;
74
+ activeSessionId = null;
75
+ model;
76
+ /** Configured session mode applied via session/set_mode (§P5). */
77
+ configuredMode;
78
+ /** Current session mode id, parsed from session/new|resume configOptions. */
79
+ currentModeId;
80
+ /** Current model value (`provider/model`), parsed from configOptions. */
81
+ currentModelValue;
82
+ /** Pending approval requests: requestId → kind + view + options. */
83
+ pendingApprovals = new Map();
84
+ /** Tracker for the in-flight prompt request — needed for cancellation. */
85
+ promptSettled = false;
86
+ /** Whether the prompt request has been fired (distinguishes "stop before
87
+ * prompt sent" from "stop while prompt in-flight" — the latter sends
88
+ * session/cancel immediately). */
89
+ promptSent = false;
90
+ constructor(opts) {
91
+ super({ kind: opts.kind, sessionReader: opts.sessionReader }, opts.turnIdleTimeoutMs);
92
+ this.model = opts.model;
93
+ this.configuredMode = opts.mode ?? 'build';
94
+ const managerOpts = {
95
+ binary: opts.binary ?? 'opencode',
96
+ args: opts.acpArgs ?? ['acp'],
97
+ env: opts.env,
98
+ requestTimeoutMs: opts.requestTimeoutMs,
99
+ idleTtlMs: opts.idleTtlMs,
100
+ // ACP handshake: declare no fs/terminal capabilities — opencode only
101
+ // reads _meta['terminal-auth'] (service.ts:102) and gates edit-apply on
102
+ // the client's fs capability (permission.ts:99-115), which we refuse.
103
+ initializeParams: {
104
+ protocolVersion: 1,
105
+ clientCapabilities: {
106
+ fs: { readTextFile: false, writeTextFile: false },
107
+ terminal: false,
108
+ },
109
+ },
110
+ };
111
+ this.connectionManager = new ConnectionManager(managerOpts);
112
+ }
113
+ get logTag() {
114
+ return 'opencode-acp-runner';
115
+ }
116
+ get turnTimeoutErrorMessage() {
117
+ return 'opencode ACP turn timed out';
118
+ }
119
+ get turnInterruptedErrorMessage() {
120
+ return 'opencode ACP turn interrupted';
121
+ }
122
+ currentSessionId() {
123
+ return this.activeSessionId;
124
+ }
125
+ shouldDeferStop() {
126
+ return !this.promptSent;
127
+ }
128
+ async cancelCurrentTurn() {
129
+ if (!this.currentClient || !this.activeSessionId)
130
+ return;
131
+ try {
132
+ // session/cancel is a NOTIFICATION (no id, no response) per ACP spec.
133
+ const cancelParams = { sessionId: this.activeSessionId };
134
+ this.currentClient.notify('session/cancel', cancelParams);
135
+ }
136
+ catch (err) {
137
+ getLogger().warn(`[${this.logTag}] session/cancel failed: ${err.message}`);
138
+ }
139
+ }
140
+ clearTurnState() {
141
+ this.currentTranslator = null;
142
+ this.promptSettled = false;
143
+ this.promptSent = false;
144
+ this.activeSessionId = null;
145
+ }
146
+ async releaseConnection(cwd) {
147
+ await this.connectionManager.release(cwd);
148
+ }
149
+ notifyIdle(cwd) {
150
+ this.connectionManager.notifyIdle(cwd);
151
+ }
152
+ async disposeConnections() {
153
+ await this.connectionManager.disposeAll();
154
+ }
155
+ /**
156
+ * Run a compact operation on the current session.
157
+ *
158
+ * opencode special-cases the `/compact` text prompt server-side
159
+ * (service.ts:555-571): it runs session.summarize synchronously and the
160
+ * prompt response settles only after compaction completes — no background
161
+ * record polling needed (unlike kimi).
162
+ */
163
+ async *runCompact(_message, opts) {
164
+ yield* this.executeTurn(opts, async () => {
165
+ const client = await this.connectionManager.acquire(opts.cwd);
166
+ this.connectionManager.notifyActivity(opts.cwd);
167
+ this.currentClient = client;
168
+ client.setHooks({
169
+ onNotification: (method, params) => this.handleNotification(method, params),
170
+ onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
171
+ onClose: () => this.failTurn('opencode ACP connection closed'),
172
+ });
173
+ const sessionId = opts.sessionId;
174
+ if (!sessionId) {
175
+ throw new Error('compact requires a sessionId');
176
+ }
177
+ this.activeSessionId = sessionId;
178
+ // Cold-connection fallback: session/resume loads the session into
179
+ // memory. On a reused connection the session is already loaded —
180
+ // resume is idempotent and harmless.
181
+ const resumeParams = { sessionId, cwd: opts.cwd };
182
+ const resumeResult = await client.request('session/resume', resumeParams);
183
+ this.trackConfigOptions(resumeResult);
184
+ const translator = new OpencodeAcpTranslator();
185
+ translator.setOperationKind('compact');
186
+ this.currentTranslator = translator;
187
+ // Emit turn_started with operationKind='compaction' before the prompt.
188
+ const turnId = `compact-${Date.now()}`;
189
+ const turnStarted = translator.produceTurnStarted(sessionId, turnId);
190
+ this.currentTurnId = turnId;
191
+ this.pushEvents([turnStarted]);
192
+ // Send /compact as prompt text (opencode intercepts it server-side).
193
+ const promptParams = {
194
+ sessionId,
195
+ prompt: [{ type: 'text', text: '/compact' }],
196
+ };
197
+ this.promptSettled = false;
198
+ // ACP holds the session/prompt response for the ENTIRE turn, so the
199
+ // RPC-level timeout must not apply — turn liveness is guarded by the
200
+ // rolling idle watchdog instead.
201
+ const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
202
+ this.promptSent = true;
203
+ promptPromise.then((result) => {
204
+ this.promptSettled = true;
205
+ if (this.stopRequested)
206
+ return; // already cancelled
207
+ const resultEvent = translator.handlePromptResponse(sessionId, result);
208
+ this.pushEvents([resultEvent]);
209
+ }, (err) => {
210
+ this.promptSettled = true;
211
+ if (this.stopRequested) {
212
+ getLogger().warn(`[${this.logTag}] compact prompt rejected after stopRequested: ${err.message}`);
213
+ return;
214
+ }
215
+ const resultEvent = translator.produceErrorResult(sessionId, err.message);
216
+ this.pushEvents([resultEvent]);
217
+ });
218
+ });
219
+ }
220
+ /**
221
+ * Respond to an approval server request. `response` is the bridge
222
+ * ApprovalAction (`{ action: 'accept' | 'decline' | 'cancel' }`).
223
+ */
224
+ async respondApproval(requestId, response) {
225
+ const client = this.currentClient;
226
+ const pending = this.pendingApprovals.get(requestId);
227
+ if (!client || !pending)
228
+ return;
229
+ const action = response?.action ?? 'decline';
230
+ const acpResponse = buildApprovalResponse(action, pending);
231
+ client.respond(requestId, acpResponse);
232
+ this.pendingApprovals.delete(requestId);
233
+ getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} action=${action}`);
234
+ }
235
+ getStatusInfo() {
236
+ return {
237
+ kind: this.kind,
238
+ model: this.model ?? '(opencode-acp)',
239
+ extras: {
240
+ mode: 'acp',
241
+ ...(this.currentModeId ? { sessionMode: this.currentModeId } : {}),
242
+ },
243
+ };
244
+ }
245
+ /**
246
+ * Hot-apply a mode change to the live ACP session (§P5).
247
+ *
248
+ * opencode's session/set_mode sends no notification — the client refreshes
249
+ * the local mode view itself. The local mode is always updated so
250
+ * `getStatusInfo()` (and therefore `/s`) reflects the new value
251
+ * immediately; when a session is connected, also re-sends session/set_mode
252
+ * so the running session picks up the new mode without waiting for the
253
+ * runner to be evicted/recreated. Failure is non-fatal: the next setupTurn
254
+ * re-applies the cached mode when it differs from the session's current
255
+ * mode.
256
+ */
257
+ async updateApprovalMode(settings) {
258
+ if (settings.mode !== undefined) {
259
+ this.configuredMode = settings.mode;
260
+ this.currentModeId = settings.mode;
261
+ }
262
+ if (!this.currentClient || !this.activeSessionId)
263
+ return;
264
+ const modeParams = {
265
+ sessionId: this.activeSessionId,
266
+ modeId: this.configuredMode,
267
+ };
268
+ try {
269
+ await this.currentClient.request('session/set_mode', modeParams);
270
+ getLogger().info(`[${this.logTag}] session/set_mode hot-applied session=${this.activeSessionId} modeId=${modeParams.modeId}`);
271
+ }
272
+ catch (err) {
273
+ getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
274
+ }
275
+ }
276
+ // =========================================================================
277
+ // Internal
278
+ // =========================================================================
279
+ /**
280
+ * Acquire the connection and set up the turn:
281
+ * session/new (new session) or session/resume (existing sessionId)
282
+ * → session/prompt.
283
+ */
284
+ async setupTurn(message, opts) {
285
+ const client = await this.connectionManager.acquire(opts.cwd);
286
+ this.connectionManager.notifyActivity(opts.cwd);
287
+ this.currentClient = client;
288
+ client.setHooks({
289
+ onNotification: (method, params) => this.handleNotification(method, params),
290
+ onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
291
+ onClose: () => {
292
+ getLogger().warn(`[${this.logTag}] client connection closed`);
293
+ this.failTurn('opencode ACP connection closed');
294
+ },
295
+ });
296
+ let sessionId;
297
+ if (opts.sessionId) {
298
+ const resumeParams = {
299
+ sessionId: opts.sessionId,
300
+ cwd: opts.cwd,
301
+ };
302
+ const resumeResult = await client.request('session/resume', resumeParams);
303
+ this.trackConfigOptions(resumeResult);
304
+ sessionId = opts.sessionId;
305
+ }
306
+ else {
307
+ const newParams = { cwd: opts.cwd, mcpServers: [] };
308
+ const newResult = await client.request('session/new', newParams);
309
+ this.trackConfigOptions(newResult);
310
+ sessionId = newResult.sessionId;
311
+ }
312
+ this.activeSessionId = sessionId;
313
+ // §P5: apply the configured mode (build/plan). If the session already
314
+ // runs it (from session/new|resume configOptions), skip the wire call;
315
+ // otherwise send session/set_mode and refresh the local view (opencode
316
+ // sends no notification for set_mode).
317
+ if (this.currentModeId !== this.configuredMode) {
318
+ const modeParams = {
319
+ sessionId,
320
+ modeId: this.configuredMode,
321
+ };
322
+ try {
323
+ await client.request('session/set_mode', modeParams);
324
+ this.currentModeId = this.configuredMode;
325
+ }
326
+ catch (err) {
327
+ getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
328
+ }
329
+ }
330
+ // Apply the configured model (`provider/model`) via set_config_option.
331
+ // Without this the session runs opencode's own default model (e.g.
332
+ // opencode/big-pickle), which may be unusable — 2026-08-17 live smoke:
333
+ // default zen model left session/prompt hanging with no response.
334
+ // Non-fatal on failure (unknown model → server InvalidModelError).
335
+ if (this.model && this.currentModelValue !== this.model) {
336
+ try {
337
+ await client.request('session/set_config_option', {
338
+ sessionId,
339
+ configId: 'model',
340
+ value: this.model,
341
+ });
342
+ this.currentModelValue = this.model;
343
+ }
344
+ catch (err) {
345
+ getLogger().warn(`[${this.logTag}] set_config_option model failed (non-fatal): ${err.message}`);
346
+ }
347
+ }
348
+ const translator = new OpencodeAcpTranslator();
349
+ this.currentTranslator = translator;
350
+ // Emit turn_started before prompt
351
+ const turnId = `turn-${Date.now()}`;
352
+ const turnStarted = translator.produceTurnStarted(sessionId, turnId);
353
+ this.currentTurnId = turnId;
354
+ this.pushEvents([turnStarted]);
355
+ // Send the prompt. The prompt request races with notifications (the
356
+ // server streams session/update before the prompt response); the
357
+ // response settles the turn and is translated into the result event.
358
+ const promptParams = {
359
+ sessionId,
360
+ prompt: [{ type: 'text', text: message }],
361
+ };
362
+ this.promptSettled = false;
363
+ const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
364
+ this.promptSent = true;
365
+ promptPromise.then((result) => {
366
+ this.promptSettled = true;
367
+ if (this.stopRequested)
368
+ return; // already cancelled
369
+ const resultEvent = translator.handlePromptResponse(sessionId, result);
370
+ this.pushEvents([resultEvent]);
371
+ }, (err) => {
372
+ this.promptSettled = true;
373
+ if (this.stopRequested) {
374
+ getLogger().warn(`[${this.logTag}] prompt rejected after stopRequested: ${err.message}`);
375
+ return;
376
+ }
377
+ const resultEvent = translator.produceErrorResult(sessionId, err.message);
378
+ this.pushEvents([resultEvent]);
379
+ });
380
+ }
381
+ /**
382
+ * Track the current session mode and model from a session/new|resume
383
+ * result's configOptions (opencode buildModeSelectOption/buildModelSelectOption:
384
+ * {id, category, currentValue}).
385
+ */
386
+ trackConfigOptions(result) {
387
+ const options = result.configOptions;
388
+ if (!Array.isArray(options))
389
+ return;
390
+ const mode = options.find((opt) => opt?.category === 'mode' || opt?.id === 'mode');
391
+ if (typeof mode?.currentValue === 'string') {
392
+ this.currentModeId = mode.currentValue;
393
+ }
394
+ const model = options.find((opt) => opt?.category === 'model' || opt?.id === 'model');
395
+ if (typeof model?.currentValue === 'string') {
396
+ this.currentModelValue = model.currentValue;
397
+ }
398
+ }
399
+ handleNotification(method, params) {
400
+ const events = this.currentTranslator?.handleNotification(method, params) ?? [];
401
+ this.pushEvents(events);
402
+ }
403
+ handleServerRequest(id, method, params) {
404
+ const events = this.currentTranslator?.handleServerRequest(id, method, params) ?? [];
405
+ if (events.length === 0) {
406
+ // Unhandled/unsupported server requests must be explicitly responded
407
+ // to (error = "rejected"), otherwise the server hangs waiting.
408
+ // This includes fs/write_text_file: we do NOT declare the fs
409
+ // capability, and if opencode still calls it (edit approved →
410
+ // writeProposedEdit), we refuse with method-not-found — the server
411
+ // side tolerates the error (permission.ts writeProposedEdit catches).
412
+ this.currentClient?.respondError(id, RpcErrorCode.METHOD_NOT_FOUND, `Unsupported server request: ${method}`);
413
+ getLogger().info(`[${this.logTag}] rejected unsupported server request method=${method} id=${id}`);
414
+ return;
415
+ }
416
+ for (const ev of events) {
417
+ if (ev.type === 'approval_requested') {
418
+ this.pendingApprovals.set(ev.requestId, {
419
+ kind: ev.kind,
420
+ view: ev.view,
421
+ options: params.options,
422
+ });
423
+ getLogger().info(`[${this.logTag}] approval requested requestId=${ev.requestId} kind=${ev.kind}`);
424
+ }
425
+ }
426
+ this.pushEvents(events);
427
+ }
428
+ }
429
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1,119 @@
1
+ /**
2
+ * OpencodeAcpTranslator: translates opencode ACP session/update notifications
3
+ * and session/request_permission server requests into AgentEvents.
4
+ *
5
+ * Channel choice (event contract in common/acp/protocol-types.ts header):
6
+ * opencode's wire `content.text` IS an incremental delta
7
+ * (opencode acp/event.ts:231-258 handlePartDelta sends `props.delta`
8
+ * verbatim), so text/thinking chunks go straight onto the `assistant`
9
+ * incremental channel — NO accumulation in the translator, NO turn_diff
10
+ * snapshot. (Contrast kimi: same field is also a delta but kimi's contract
11
+ * accumulates into turn_diff snapshots; both end with exactly one
12
+ * concatenation in run-state, enforced by each side's seam test.)
13
+ *
14
+ * Event mapping (source: opencode acp/service.ts + event.ts + tool.ts,
15
+ * dev@1c965451b5):
16
+ * agent_message_chunk → assistant/text delta (content.text passthrough)
17
+ * agent_thought_chunk → assistant/thinking delta (content.text passthrough)
18
+ * tool_call → assistant/tool_use (rawInput object passthrough;
19
+ * missing → {} per contract, string → JSON.parse)
20
+ * tool_call_update → user/tool_result (status:'failed' → is_error:true;
21
+ * rawOutput is an OBJECT: {output} / {error})
22
+ * usage_update → live context occupancy {used, size} + cost →
23
+ * {total_tokens, context_limit, total_cost_usd}
24
+ * available_commands_update / plan / session_info_update /
25
+ * current_mode_update / config_option_update / user_message_chunk → discard
26
+ * prompt stopReason:'end_turn' → result success
27
+ * prompt stopReason:'cancelled' → result interrupted (独立终态)
28
+ * prompt other stopReason/error → result error
29
+ * session/request_permission → approval_requested (optionId echo)
30
+ */
31
+ import type { AgentEvent } from '../../types.js';
32
+ import type { ApprovalRequestedEvent } from '../../types.js';
33
+ /** Self-produced turn_started event (no wire equivalent — produced on prompt start). */
34
+ export interface OpencodeAcpTurnStartedEvent {
35
+ type: 'turn_started';
36
+ threadId: string;
37
+ turnId: string;
38
+ operationKind: 'turn' | 'compaction';
39
+ timestamp?: string;
40
+ }
41
+ /**
42
+ * Live usage snapshot from usage_update. The wire carries context occupancy
43
+ * (`used`), the model context window (`size`) and session cost
44
+ * (`cost.amount`) — there is NO input/output token split.
45
+ */
46
+ export interface OpencodeAcpLiveUsage {
47
+ total_tokens?: number;
48
+ context_limit?: number;
49
+ input_tokens?: number;
50
+ output_tokens?: number;
51
+ }
52
+ /** Usage update event for live token display. */
53
+ export interface OpencodeAcpUsageEvent {
54
+ type: 'usage';
55
+ usage: OpencodeAcpLiveUsage;
56
+ timestamp?: string;
57
+ }
58
+ export type OpencodeAcpTranslatorEvent = AgentEvent | OpencodeAcpTurnStartedEvent | OpencodeAcpUsageEvent | ApprovalRequestedEvent;
59
+ export declare class OpencodeAcpTranslator {
60
+ /** Pending tool calls keyed by toolCallId (for matching tool_call_update). */
61
+ private pendingToolCalls;
62
+ /** Current operation kind (turn vs compact). */
63
+ private operationKind;
64
+ /** Live usage snapshot from usage_update events (context occupancy only). */
65
+ private liveUsage;
66
+ /** Session cost from usage_update cost.amount (USD). */
67
+ private liveCostUsd?;
68
+ /** Whether any usage_update has been seen this turn (no update → no usage). */
69
+ private hasLiveUsage;
70
+ /** Current turn id, set by produceTurnStarted. */
71
+ private currentTurnId;
72
+ /**
73
+ * Handle a notification from the ACP server and return translated events.
74
+ */
75
+ handleNotification(method: string, params: unknown): OpencodeAcpTranslatorEvent[];
76
+ /**
77
+ * Handle a server request (reverse RPC from the ACP server).
78
+ */
79
+ handleServerRequest(id: number | string, method: string, params: unknown): OpencodeAcpTranslatorEvent[];
80
+ /**
81
+ * Produce a turn_started event. Called by the runner when session/prompt
82
+ * is sent (no wire notification for this — self-produced).
83
+ */
84
+ produceTurnStarted(sessionId: string, turnId: string): OpencodeAcpTurnStartedEvent;
85
+ /**
86
+ * Translate a prompt response into a result event.
87
+ * stopReason: end_turn → success; cancelled → interrupted; anything else
88
+ * (max_tokens / refusal / …) → error.
89
+ */
90
+ handlePromptResponse(sessionId: string, result: {
91
+ stopReason: string;
92
+ }): AgentEvent;
93
+ /**
94
+ * Produce an error result event (for JSON-RPC errors during prompt).
95
+ */
96
+ produceErrorResult(sessionId: string, errorMessage: string): AgentEvent;
97
+ /**
98
+ * Set the operation kind (turn vs compact).
99
+ */
100
+ setOperationKind(kind: 'turn' | 'compact'): void;
101
+ private handleAgentMessageChunk;
102
+ private handleAgentThoughtChunk;
103
+ private handleToolCall;
104
+ private handleToolCallUpdate;
105
+ private handleUsageUpdate;
106
+ private handleRequestPermission;
107
+ /**
108
+ * Derive the approval decision list from the server's offered options.
109
+ * accept/decline/cancel are universal; acceptForSession requires an
110
+ * always-class option (opencode `allow_always`, kimi `approve_always`).
111
+ */
112
+ private deriveAvailableDecisions;
113
+ /**
114
+ * ResultEvent.usage nominally requires input_tokens/output_tokens, but the
115
+ * ACP wire has no such split — the live snapshot is context occupancy only.
116
+ * Cast is deliberate: consumers must tolerate undefined input/output.
117
+ */
118
+ private liveUsageSnapshot;
119
+ }