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.
- package/CHANGELOG.md +39 -0
- package/README.en.md +5 -1
- package/README.md +10 -2
- package/dist/bridge/approval-coordinator.d.ts +131 -0
- package/dist/bridge/approval-coordinator.js +409 -0
- package/dist/bridge/inbound-media.d.ts +62 -0
- package/dist/bridge/inbound-media.js +282 -0
- package/dist/bridge/index.d.ts +138 -5
- package/dist/bridge/index.js +569 -65
- package/dist/bridge/queue-manager.d.ts +11 -1
- package/dist/bridge/queue-manager.js +20 -11
- package/dist/card/approval-render.d.ts +16 -0
- package/dist/card/approval-render.js +363 -0
- package/dist/card/bash-card-session.d.ts +2 -24
- package/dist/card/bash-card-session.js +6 -73
- package/dist/card/bash-renderer.js +2 -3
- package/dist/card/card-budget.js +29 -75
- package/dist/card/card-session.d.ts +48 -0
- package/dist/card/card-session.js +86 -0
- package/dist/card/card-shared.d.ts +13 -10
- package/dist/card/card-shared.js +27 -24
- package/dist/card/run-card-session.d.ts +2 -55
- package/dist/card/run-card-session.js +6 -104
- package/dist/card/run-renderer.js +140 -202
- package/dist/card/run-state.d.ts +38 -3
- package/dist/card/run-state.js +221 -11
- package/dist/card/text-truncate.d.ts +13 -12
- package/dist/card/text-truncate.js +13 -63
- package/dist/common/fs.d.ts +8 -0
- package/dist/common/fs.js +17 -0
- package/dist/common/guards.d.ts +0 -20
- package/dist/common/guards.js +0 -43
- package/dist/common/sleep.d.ts +2 -0
- package/dist/common/sleep.js +5 -0
- package/dist/common/truncate.d.ts +39 -0
- package/dist/common/truncate.js +92 -0
- package/dist/config/codex-config.d.ts +1 -1
- package/dist/config/codex-config.js +1 -1
- package/dist/config/dir.js +1 -1
- package/dist/config/index.d.ts +116 -9
- package/dist/config/index.js +122 -29
- package/dist/connector/index.d.ts +74 -2
- package/dist/connector/index.js +119 -1
- package/dist/index.js +139 -99
- package/dist/instance-lock.js +4 -12
- package/dist/order/alias-resolve.d.ts +13 -0
- package/dist/order/alias-resolve.js +22 -0
- package/dist/order/alias-store.d.ts +38 -0
- package/dist/order/alias-store.js +106 -0
- package/dist/persistence/atomic-write.d.ts +8 -2
- package/dist/persistence/atomic-write.js +23 -7
- package/dist/restart.js +2 -1
- package/dist/router/card-helpers.d.ts +63 -0
- package/dist/router/card-helpers.js +146 -0
- package/dist/router/config/claude.js +25 -1
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +33 -5
- package/dist/router/config/common/model-patch.d.ts +22 -0
- package/dist/router/config/common/model-patch.js +26 -0
- package/dist/router/config/common/render.js +9 -4
- package/dist/router/config/index.d.ts +12 -0
- package/dist/router/config/index.js +25 -1
- package/dist/router/config/kimi.d.ts +4 -1
- package/dist/router/config/kimi.js +27 -0
- package/dist/router/config/opencode.js +18 -5
- package/dist/router/config/pi.js +4 -5
- package/dist/router/config/types.d.ts +12 -3
- package/dist/router/index.d.ts +77 -17
- package/dist/router/index.js +455 -549
- package/dist/router/utils.d.ts +4 -0
- package/dist/router/utils.js +9 -2
- package/dist/runner/agent-choices-common.d.ts +16 -0
- package/dist/runner/agent-choices-common.js +31 -0
- package/dist/runner/claude/runner.d.ts +47 -10
- package/dist/runner/claude/runner.js +117 -46
- package/dist/runner/claude/session.d.ts +166 -0
- package/dist/runner/claude/session.js +749 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +519 -0
- package/dist/runner/codex/app-server/protocol-types.js +56 -0
- package/dist/runner/codex/app-server/runner.d.ts +104 -0
- package/dist/runner/codex/app-server/runner.js +377 -0
- package/dist/runner/codex/app-server/translator.d.ts +89 -0
- package/dist/runner/codex/app-server/translator.js +716 -0
- package/dist/runner/codex/index.d.ts +1 -1
- package/dist/runner/codex/index.js +1 -1
- package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
- package/dist/runner/common/acp/protocol-helpers.js +21 -0
- package/dist/runner/common/acp/protocol-types.d.ts +238 -0
- package/dist/runner/common/acp/protocol-types.js +75 -0
- package/dist/runner/common/connection-based-runner.d.ts +111 -0
- package/dist/runner/common/connection-based-runner.js +261 -0
- package/dist/runner/common/jsonrpc/client.d.ts +89 -0
- package/dist/runner/common/jsonrpc/client.js +243 -0
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +205 -0
- package/dist/runner/common/jsonrpc/transport.d.ts +50 -0
- package/dist/runner/common/jsonrpc/transport.js +230 -0
- package/dist/runner/common/jsonrpc/types.d.ts +32 -0
- package/dist/runner/common/jsonrpc/types.js +9 -0
- package/dist/runner/common/process-stopper.d.ts +2 -1
- package/dist/runner/common/process-stopper.js +1 -0
- package/dist/runner/common/runner-utils.d.ts +6 -13
- package/dist/runner/common/runner-utils.js +6 -47
- package/dist/runner/common/spawning-runner.d.ts +24 -70
- package/dist/runner/common/spawning-runner.js +98 -152
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +5 -5
- package/dist/runner/index.js +4 -4
- package/dist/runner/kimi/acp/runner.d.ts +106 -0
- package/dist/runner/kimi/acp/runner.js +448 -0
- package/dist/runner/kimi/acp/translator.d.ts +124 -0
- package/dist/runner/kimi/acp/translator.js +336 -0
- package/dist/runner/kimi/index.d.ts +1 -1
- package/dist/runner/kimi/index.js +1 -1
- package/dist/runner/opencode/acp/index.d.ts +1 -0
- package/dist/runner/opencode/acp/index.js +2 -0
- package/dist/runner/opencode/acp/runner.d.ts +121 -0
- package/dist/runner/opencode/acp/runner.js +429 -0
- package/dist/runner/opencode/acp/translator.d.ts +119 -0
- package/dist/runner/opencode/acp/translator.js +345 -0
- package/dist/runner/opencode/index.d.ts +1 -1
- package/dist/runner/opencode/index.js +1 -1
- package/dist/runner/pi/index.d.ts +1 -1
- package/dist/runner/pi/index.js +1 -1
- package/dist/runner/pi/rpc/client.d.ts +52 -0
- package/dist/runner/pi/rpc/client.js +114 -0
- package/dist/runner/pi/rpc/index.d.ts +4 -0
- package/dist/runner/pi/rpc/index.js +4 -0
- package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
- package/dist/runner/pi/rpc/protocol-types.js +21 -0
- package/dist/runner/pi/rpc/runner.d.ts +80 -0
- package/dist/runner/pi/rpc/runner.js +199 -0
- package/dist/runner/pi/rpc/translator.d.ts +55 -0
- package/dist/runner/pi/rpc/translator.js +258 -0
- package/dist/runner/registry.d.ts +7 -8
- package/dist/runner/registry.js +3 -4
- package/dist/runner/resolve-agent-choices.js +18 -59
- package/dist/runner/sync-agent-choices.js +16 -67
- package/dist/runner/types.d.ts +153 -2
- package/dist/session/claude/session-index.js +2 -3
- package/dist/session/claude/session-reader.js +3 -6
- package/dist/session/claude/sessions.js +6 -13
- package/dist/session/codex/rollout-reader.d.ts +2 -2
- package/dist/session/codex/rollout-reader.js +57 -17
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +2 -2
- package/dist/session/common/content-blocks.d.ts +7 -0
- package/dist/session/common/content-blocks.js +13 -9
- package/dist/session/common/jsonl.d.ts +8 -25
- package/dist/session/common/jsonl.js +126 -261
- package/dist/session/common/pagination.d.ts +28 -0
- package/dist/session/common/pagination.js +33 -0
- package/dist/session/kimi/sessions.d.ts +34 -1
- package/dist/session/kimi/sessions.js +208 -98
- package/dist/session/opencode/sessions.d.ts +17 -3
- package/dist/session/opencode/sessions.js +29 -29
- package/dist/session/pi/sessions.d.ts +5 -1
- package/dist/session/pi/sessions.js +12 -16
- package/dist/session/session-store.d.ts +11 -1
- package/dist/session/session-store.js +75 -178
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/install.d.ts +20 -2
- package/dist/update/install.js +51 -7
- package/package.json +1 -1
- package/dist/runner/codex/argv.d.ts +0 -33
- package/dist/runner/codex/argv.js +0 -40
- package/dist/runner/codex/jsonl.d.ts +0 -37
- package/dist/runner/codex/jsonl.js +0 -221
- package/dist/runner/codex/runner.d.ts +0 -63
- package/dist/runner/codex/runner.js +0 -124
- package/dist/runner/common/exec-translator.d.ts +0 -73
- package/dist/runner/common/exec-translator.js +0 -105
- package/dist/runner/kimi/jsonl.d.ts +0 -28
- package/dist/runner/kimi/jsonl.js +0 -143
- package/dist/runner/kimi/runner.d.ts +0 -49
- package/dist/runner/kimi/runner.js +0 -105
- package/dist/runner/opencode/argv.d.ts +0 -32
- package/dist/runner/opencode/argv.js +0 -35
- package/dist/runner/opencode/jsonl.d.ts +0 -68
- package/dist/runner/opencode/jsonl.js +0 -295
- package/dist/runner/opencode/runner.d.ts +0 -61
- package/dist/runner/opencode/runner.js +0 -93
- package/dist/runner/pi/jsonl.d.ts +0 -128
- package/dist/runner/pi/jsonl.js +0 -249
- package/dist/runner/pi/runner.d.ts +0 -36
- package/dist/runner/pi/runner.js +0 -76
- package/dist/session/common/catch-up.d.ts +0 -12
- 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
|
+
}
|