lark-remote 0.1.6 → 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 +23 -1
- package/README.en.md +9 -1
- package/README.md +14 -2
- package/dist/bridge/approval-coordinator.d.ts +57 -34
- package/dist/bridge/approval-coordinator.js +191 -40
- package/dist/bridge/inbound-media.d.ts +62 -0
- package/dist/bridge/inbound-media.js +282 -0
- package/dist/bridge/index.d.ts +78 -7
- package/dist/bridge/index.js +202 -79
- package/dist/bridge/queue-manager.d.ts +2 -1
- package/dist/bridge/queue-manager.js +1 -0
- package/dist/card/approval-render.js +152 -36
- 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 +1 -12
- package/dist/card/card-shared.js +1 -26
- 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 +99 -232
- package/dist/card/run-state.d.ts +13 -0
- package/dist/card/run-state.js +25 -16
- 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/index.d.ts +88 -23
- package/dist/config/index.js +105 -34
- package/dist/connector/index.d.ts +69 -2
- package/dist/connector/index.js +107 -1
- package/dist/index.js +123 -105
- 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.js +29 -50
- 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 +5 -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 +11 -2
- package/dist/router/index.d.ts +56 -21
- package/dist/router/index.js +373 -548
- package/dist/router/utils.d.ts +2 -0
- package/dist/router/utils.js +4 -0
- 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 +31 -39
- package/dist/runner/codex/app-server/protocol-types.js +1 -12
- package/dist/runner/codex/app-server/runner.d.ts +42 -59
- package/dist/runner/codex/app-server/runner.js +160 -337
- package/dist/runner/codex/app-server/translator.d.ts +1 -21
- package/dist/runner/codex/app-server/translator.js +0 -18
- package/dist/runner/codex/index.d.ts +0 -2
- package/dist/runner/codex/index.js +0 -2
- 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/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
- package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
- package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
- package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
- package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
- 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/index.d.ts +4 -4
- 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 +43 -13
- 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 +14 -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/app-server/connection-manager.d.ts +0 -56
- package/dist/runner/codex/argv.d.ts +0 -33
- package/dist/runner/codex/argv.js +0 -40
- package/dist/runner/codex/factory.d.ts +0 -29
- package/dist/runner/codex/factory.js +0 -39
- 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 -66
- package/dist/runner/codex/runner.js +0 -129
- 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,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KimiAcpRunner: workspace-lifetime runner using the Kimi ACP protocol
|
|
3
|
+
* (`kimi acp`, JSON-RPC over stdio).
|
|
4
|
+
*
|
|
5
|
+
* Flow per run: acquire persistent connection → session/new or session/resume
|
|
6
|
+
* → synthetic init → session/prompt → consume session/update notifications
|
|
7
|
+
* until prompt settles → result event.
|
|
8
|
+
*
|
|
9
|
+
* Structurally aligned with codex AppServerRunner (design doc §3.1) via the
|
|
10
|
+
* shared ConnectionBasedRunner base: same notification queue + waitResolve
|
|
11
|
+
* pattern, same forceFinish/stopRequested semantics, same turn idle timeout,
|
|
12
|
+
* same ConnectionLostError retry.
|
|
13
|
+
*/
|
|
14
|
+
import { ConnectionManager as KimiAcpConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
|
|
15
|
+
import { KimiAcpTranslator } from './translator.js';
|
|
16
|
+
import { RpcErrorCode, ServerRequestMethod, } from '../../common/acp/protocol-types.js';
|
|
17
|
+
import { findOptionIdByKind } from '../../common/acp/protocol-helpers.js';
|
|
18
|
+
import { getLogger } from '../../../logger/index.js';
|
|
19
|
+
import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
|
|
20
|
+
/** R2: how long to wait for the background compaction record after the
|
|
21
|
+
* /compact prompt settles (design doc §6.1). Real kimi 0.36.0 lands
|
|
22
|
+
* context.apply_compaction ~8s after end_turn. */
|
|
23
|
+
const COMPACT_POLL_TIMEOUT_MS = 30_000;
|
|
24
|
+
/** Poll interval for the wire.jsonl compaction record. */
|
|
25
|
+
const COMPACT_POLL_INTERVAL_MS = 1_000;
|
|
26
|
+
/**
|
|
27
|
+
* Map user-facing permission mode to ACP mode id.
|
|
28
|
+
*
|
|
29
|
+
* ACP layer mode ids (acp-server/src/modes.ts:49):
|
|
30
|
+
* default → permission manual
|
|
31
|
+
* auto → permission auto
|
|
32
|
+
* yolo → permission yolo
|
|
33
|
+
* plan → not exposed in v1
|
|
34
|
+
*
|
|
35
|
+
* This is not self-invented naming — both are official enums at their
|
|
36
|
+
* respective layers. The mapping happens only at the ACP boundary.
|
|
37
|
+
*/
|
|
38
|
+
function toAcpMode(mode) {
|
|
39
|
+
switch (mode) {
|
|
40
|
+
case 'manual':
|
|
41
|
+
return 'default';
|
|
42
|
+
case 'auto':
|
|
43
|
+
return 'auto';
|
|
44
|
+
case 'yolo':
|
|
45
|
+
return 'yolo';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build the ACP protocol response for a permission approval decision.
|
|
50
|
+
*
|
|
51
|
+
* accept → {outcome:{outcome:'selected', optionId:<approve_once kind>}}
|
|
52
|
+
* accept_for_session → {outcome:{outcome:'selected', optionId:<approve_always kind>}}
|
|
53
|
+
* decline → {outcome:{outcome:'selected', optionId:<reject kind>}}
|
|
54
|
+
* cancel → {outcome:{outcome:'cancelled'}}
|
|
55
|
+
*
|
|
56
|
+
* optionId is looked up from the request's options[] by kind (approval.ts:28-29):
|
|
57
|
+
* approve_once / approve_always / allow_once for accept;
|
|
58
|
+
* approve_always / allow_always for accept_for_session;
|
|
59
|
+
* reject / reject_once for decline. If not found, fall back to cancelled
|
|
60
|
+
* (safe universal default — the server treats it as "user declined").
|
|
61
|
+
*
|
|
62
|
+
* 2026-08-15 live test: approve_once + approve_always + reject observed.
|
|
63
|
+
*/
|
|
64
|
+
function buildApprovalResponse(action, pending) {
|
|
65
|
+
if (action === 'cancel') {
|
|
66
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
67
|
+
}
|
|
68
|
+
if (action === 'accept') {
|
|
69
|
+
const optionId = findOptionIdByKind(pending.options, [
|
|
70
|
+
'approve_once',
|
|
71
|
+
'approve_always',
|
|
72
|
+
'allow_once',
|
|
73
|
+
]);
|
|
74
|
+
if (optionId) {
|
|
75
|
+
return { outcome: { outcome: 'selected', optionId } };
|
|
76
|
+
}
|
|
77
|
+
// Fallback: can't find allow option → cancel is safe
|
|
78
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
79
|
+
}
|
|
80
|
+
if (action === 'accept_for_session') {
|
|
81
|
+
// §P4: 卡片「允许本次会话」→ always 类 optionId(kimi approve_always)
|
|
82
|
+
const optionId = findOptionIdByKind(pending.options, ['approve_always', 'allow_always']);
|
|
83
|
+
if (optionId) {
|
|
84
|
+
return { outcome: { outcome: 'selected', optionId } };
|
|
85
|
+
}
|
|
86
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
87
|
+
}
|
|
88
|
+
// decline
|
|
89
|
+
const optionId = findOptionIdByKind(pending.options, ['reject', 'reject_once']);
|
|
90
|
+
if (optionId) {
|
|
91
|
+
return { outcome: { outcome: 'selected', optionId } };
|
|
92
|
+
}
|
|
93
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
94
|
+
}
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// Runner
|
|
97
|
+
// =============================================================================
|
|
98
|
+
export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
99
|
+
connectionManager;
|
|
100
|
+
currentTranslator = null;
|
|
101
|
+
activeSessionId = null;
|
|
102
|
+
model;
|
|
103
|
+
permissionMode;
|
|
104
|
+
compactPollTimeoutMs;
|
|
105
|
+
/** Pending approval requests: requestId → kind + view + options. */
|
|
106
|
+
pendingApprovals = new Map();
|
|
107
|
+
/** Tracker for the in-flight prompt request — needed for cancellation. */
|
|
108
|
+
promptSettled = false;
|
|
109
|
+
constructor(opts) {
|
|
110
|
+
super({ kind: opts.kind, sessionReader: opts.sessionReader }, opts.turnIdleTimeoutMs);
|
|
111
|
+
this.model = opts.model;
|
|
112
|
+
this.permissionMode = opts.permissionMode ?? 'manual';
|
|
113
|
+
this.compactPollTimeoutMs = opts.compactPollTimeoutMs ?? COMPACT_POLL_TIMEOUT_MS;
|
|
114
|
+
const managerOpts = {
|
|
115
|
+
binary: opts.binary ?? 'kimi',
|
|
116
|
+
args: opts.acpArgs ?? ['acp'],
|
|
117
|
+
env: opts.env,
|
|
118
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
119
|
+
idleTtlMs: opts.idleTtlMs,
|
|
120
|
+
initializeParams: {
|
|
121
|
+
protocolVersion: 1,
|
|
122
|
+
clientCapabilities: {
|
|
123
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
124
|
+
terminal: false,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
this.connectionManager = new KimiAcpConnectionManager(managerOpts);
|
|
129
|
+
}
|
|
130
|
+
get logTag() {
|
|
131
|
+
return 'kimi-acp-runner';
|
|
132
|
+
}
|
|
133
|
+
get turnTimeoutErrorMessage() {
|
|
134
|
+
return 'Kimi ACP turn timed out';
|
|
135
|
+
}
|
|
136
|
+
get turnInterruptedErrorMessage() {
|
|
137
|
+
return 'Kimi ACP turn interrupted';
|
|
138
|
+
}
|
|
139
|
+
currentSessionId() {
|
|
140
|
+
return this.activeSessionId;
|
|
141
|
+
}
|
|
142
|
+
shouldDeferStop() {
|
|
143
|
+
return !this.promptSettled;
|
|
144
|
+
}
|
|
145
|
+
async cancelCurrentTurn() {
|
|
146
|
+
if (!this.currentClient || !this.activeSessionId)
|
|
147
|
+
return;
|
|
148
|
+
try {
|
|
149
|
+
// session/cancel is a NOTIFICATION (no id, no response) per ACP spec.
|
|
150
|
+
const cancelParams = { sessionId: this.activeSessionId };
|
|
151
|
+
this.currentClient.notify('session/cancel', cancelParams);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
getLogger().warn(`[${this.logTag}] session/cancel failed: ${err.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
clearTurnState() {
|
|
158
|
+
this.currentTranslator = null;
|
|
159
|
+
this.promptSettled = false;
|
|
160
|
+
this.activeSessionId = null;
|
|
161
|
+
}
|
|
162
|
+
async releaseConnection(cwd) {
|
|
163
|
+
await this.connectionManager.release(cwd);
|
|
164
|
+
}
|
|
165
|
+
notifyIdle(cwd) {
|
|
166
|
+
this.connectionManager.notifyIdle(cwd);
|
|
167
|
+
}
|
|
168
|
+
async disposeConnections() {
|
|
169
|
+
await this.connectionManager.disposeAll();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Run a compact operation on the current session.
|
|
173
|
+
*
|
|
174
|
+
* Design doc §6.1: acquire → session/resume → session/prompt with "/compact"
|
|
175
|
+
* text (builtin slash command, host intercepts) → consume notifications until
|
|
176
|
+
* prompt settles.
|
|
177
|
+
*/
|
|
178
|
+
async *runCompact(_message, opts) {
|
|
179
|
+
yield* this.executeTurn(opts, async () => {
|
|
180
|
+
const client = await this.connectionManager.acquire(opts.cwd);
|
|
181
|
+
this.connectionManager.notifyActivity(opts.cwd);
|
|
182
|
+
this.currentClient = client;
|
|
183
|
+
client.setHooks({
|
|
184
|
+
onNotification: (method, params) => this.handleNotification(method, params),
|
|
185
|
+
onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
|
|
186
|
+
onClose: () => this.failTurn('Kimi ACP connection closed'),
|
|
187
|
+
});
|
|
188
|
+
const sessionId = opts.sessionId;
|
|
189
|
+
if (!sessionId) {
|
|
190
|
+
throw new Error('compact requires a sessionId');
|
|
191
|
+
}
|
|
192
|
+
this.activeSessionId = sessionId;
|
|
193
|
+
// Cold-connection fallback: session/resume loads the session into memory.
|
|
194
|
+
// On a reused connection the session is already loaded — resume is
|
|
195
|
+
// idempotent and harmless; after connection rebuild it's required.
|
|
196
|
+
const resumeParams = {
|
|
197
|
+
sessionId,
|
|
198
|
+
cwd: opts.cwd,
|
|
199
|
+
};
|
|
200
|
+
await client.request('session/resume', resumeParams);
|
|
201
|
+
const translator = new KimiAcpTranslator();
|
|
202
|
+
translator.setOperationKind('compact');
|
|
203
|
+
this.currentTranslator = translator;
|
|
204
|
+
// Emit turn_started with operationKind='compaction' before the prompt
|
|
205
|
+
// (consumeTurn yields it while the background compaction runs).
|
|
206
|
+
const turnId = `compact-${Date.now()}`;
|
|
207
|
+
const turnStarted = translator.produceTurnStarted(sessionId, turnId);
|
|
208
|
+
this.currentTurnId = turnId;
|
|
209
|
+
this.pushEvents([turnStarted]);
|
|
210
|
+
// Send /compact as prompt text
|
|
211
|
+
const promptParams = {
|
|
212
|
+
sessionId,
|
|
213
|
+
prompt: [{ type: 'text', text: '/compact' }],
|
|
214
|
+
};
|
|
215
|
+
// R2: compaction is a BACKGROUND task — the prompt settles before the
|
|
216
|
+
// wire.jsonl compaction record lands (real kimi: ~8s). Baseline the
|
|
217
|
+
// record count, fire the prompt without awaiting (notifications flow
|
|
218
|
+
// through consumeTurn), then poll for a NEW record before producing
|
|
219
|
+
// the result. This keeps the connection alive while the background
|
|
220
|
+
// compaction finishes — otherwise dispose/exit kills it and the record
|
|
221
|
+
// never lands (S5/S6 打回复现).
|
|
222
|
+
const compactionReader = this.sessionReader.readCompactionRecords;
|
|
223
|
+
const baselineCount = compactionReader
|
|
224
|
+
? compactionReader.call(this.sessionReader, sessionId, opts.cwd).length
|
|
225
|
+
: 0;
|
|
226
|
+
this.promptSettled = false;
|
|
227
|
+
// ACP holds the session/prompt response for the ENTIRE turn (unlike
|
|
228
|
+
// codex turn/start), so the RPC-level timeout must not apply here —
|
|
229
|
+
// turn liveness is guarded by the rolling idle watchdog instead.
|
|
230
|
+
const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
|
|
231
|
+
promptPromise.then(async (result) => {
|
|
232
|
+
this.promptSettled = true;
|
|
233
|
+
if (this.stopRequested)
|
|
234
|
+
return; // already cancelled
|
|
235
|
+
if (compactionReader) {
|
|
236
|
+
await this.waitForCompactionRecord(compactionReader, opts, baselineCount);
|
|
237
|
+
}
|
|
238
|
+
if (this.forceFinish)
|
|
239
|
+
return; // stopped while polling
|
|
240
|
+
// Translate prompt result into a result event
|
|
241
|
+
const resultEvent = translator.handlePromptResponse(sessionId, result);
|
|
242
|
+
this.pushEvents([resultEvent]);
|
|
243
|
+
}, (err) => {
|
|
244
|
+
this.promptSettled = true;
|
|
245
|
+
if (this.stopRequested) {
|
|
246
|
+
getLogger().warn(`[${this.logTag}] compact prompt rejected after stopRequested: ${err.message}`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const resultEvent = translator.produceErrorResult(sessionId, err.message);
|
|
250
|
+
this.pushEvents([resultEvent]);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Respond to an approval server request. `response` is the bridge
|
|
256
|
+
* ApprovalAction (`{ action: 'accept' | 'decline' | 'cancel' }`).
|
|
257
|
+
*/
|
|
258
|
+
async respondApproval(requestId, response) {
|
|
259
|
+
const client = this.currentClient;
|
|
260
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
261
|
+
if (!client || !pending)
|
|
262
|
+
return;
|
|
263
|
+
const action = response?.action ?? 'decline';
|
|
264
|
+
const acpResponse = buildApprovalResponse(action, pending);
|
|
265
|
+
client.respond(requestId, acpResponse);
|
|
266
|
+
this.pendingApprovals.delete(requestId);
|
|
267
|
+
getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} action=${action}`);
|
|
268
|
+
}
|
|
269
|
+
getStatusInfo() {
|
|
270
|
+
return {
|
|
271
|
+
kind: this.kind,
|
|
272
|
+
model: this.model ?? '(kimi-acp)',
|
|
273
|
+
extras: {
|
|
274
|
+
mode: 'acp',
|
|
275
|
+
permissionMode: this.permissionMode,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Hot-apply a permission-mode change to the live ACP session (§P5).
|
|
281
|
+
*
|
|
282
|
+
* The local cache is always updated so `getStatusInfo()` (and therefore
|
|
283
|
+
* `/s`) reflects the new mode immediately. When a session is connected,
|
|
284
|
+
* re-sends `session/set_mode` so the running session picks up the new mode
|
|
285
|
+
* without waiting for the runner to be evicted/recreated. Failure is
|
|
286
|
+
* non-fatal: the next setupTurn re-applies the cached mode unconditionally.
|
|
287
|
+
*/
|
|
288
|
+
async updateApprovalMode(settings) {
|
|
289
|
+
if (settings.permissionMode !== undefined) {
|
|
290
|
+
this.permissionMode = settings.permissionMode;
|
|
291
|
+
}
|
|
292
|
+
if (!this.currentClient || !this.activeSessionId)
|
|
293
|
+
return;
|
|
294
|
+
const modeParams = {
|
|
295
|
+
sessionId: this.activeSessionId,
|
|
296
|
+
modeId: toAcpMode(this.permissionMode),
|
|
297
|
+
};
|
|
298
|
+
try {
|
|
299
|
+
await this.currentClient.request('session/set_mode', modeParams);
|
|
300
|
+
getLogger().info(`[${this.logTag}] session/set_mode hot-applied session=${this.activeSessionId} modeId=${modeParams.modeId}`);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// =========================================================================
|
|
307
|
+
// Internal
|
|
308
|
+
// =========================================================================
|
|
309
|
+
/**
|
|
310
|
+
* Acquire the connection and set up the turn:
|
|
311
|
+
* session/new (new session) or session/resume (existing sessionId)
|
|
312
|
+
* → set permission mode if needed → session/prompt.
|
|
313
|
+
*/
|
|
314
|
+
async setupTurn(message, opts) {
|
|
315
|
+
const client = await this.connectionManager.acquire(opts.cwd);
|
|
316
|
+
this.connectionManager.notifyActivity(opts.cwd);
|
|
317
|
+
this.currentClient = client;
|
|
318
|
+
client.setHooks({
|
|
319
|
+
onNotification: (method, params) => this.handleNotification(method, params),
|
|
320
|
+
onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
|
|
321
|
+
onClose: () => {
|
|
322
|
+
getLogger().warn(`[${this.logTag}] client connection closed`);
|
|
323
|
+
this.failTurn('Kimi ACP connection closed');
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
let sessionId;
|
|
327
|
+
if (opts.sessionId) {
|
|
328
|
+
const resumeParams = {
|
|
329
|
+
sessionId: opts.sessionId,
|
|
330
|
+
cwd: opts.cwd,
|
|
331
|
+
};
|
|
332
|
+
await client.request('session/resume', resumeParams);
|
|
333
|
+
sessionId = opts.sessionId;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const newParams = { cwd: opts.cwd, mcpServers: [] };
|
|
337
|
+
const newResult = await client.request('session/new', newParams);
|
|
338
|
+
sessionId = newResult.sessionId;
|
|
339
|
+
}
|
|
340
|
+
this.activeSessionId = sessionId;
|
|
341
|
+
// Set permission mode (§5.1): EVERY fresh ACP session starts in
|
|
342
|
+
// 'default' (= manual approvals, acp-server modes.ts DEFAULT_MODE_ID),
|
|
343
|
+
// so the mode must be sent unconditionally — skipping yolo leaves the
|
|
344
|
+
// session in manual and tool calls stall on unanswered approvals
|
|
345
|
+
// (live dark-test repro: approval_requested → idle timeout).
|
|
346
|
+
const modeParams = {
|
|
347
|
+
sessionId,
|
|
348
|
+
modeId: toAcpMode(this.permissionMode),
|
|
349
|
+
};
|
|
350
|
+
try {
|
|
351
|
+
await client.request('session/set_mode', modeParams);
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
|
|
355
|
+
}
|
|
356
|
+
const translator = new KimiAcpTranslator();
|
|
357
|
+
this.currentTranslator = translator;
|
|
358
|
+
// Emit turn_started before prompt
|
|
359
|
+
const turnId = `turn-${Date.now()}`;
|
|
360
|
+
const turnStarted = translator.produceTurnStarted(sessionId, turnId);
|
|
361
|
+
this.currentTurnId = turnId;
|
|
362
|
+
this.pushEvents([turnStarted]);
|
|
363
|
+
// Send the prompt
|
|
364
|
+
const promptParams = {
|
|
365
|
+
sessionId,
|
|
366
|
+
prompt: [{ type: 'text', text: message }],
|
|
367
|
+
};
|
|
368
|
+
// Fire the prompt request; notifications will be buffered in the queue.
|
|
369
|
+
// We process them concurrently.
|
|
370
|
+
this.promptSettled = false;
|
|
371
|
+
const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
|
|
372
|
+
// Wait for prompt to settle, then push the result event.
|
|
373
|
+
// Notifications are pushed concurrently by the hook handlers.
|
|
374
|
+
promptPromise.then((result) => {
|
|
375
|
+
this.promptSettled = true;
|
|
376
|
+
if (this.stopRequested)
|
|
377
|
+
return; // already cancelled
|
|
378
|
+
const resultEvent = translator.handlePromptResponse(sessionId, result);
|
|
379
|
+
this.pushEvents([resultEvent]);
|
|
380
|
+
}, (err) => {
|
|
381
|
+
this.promptSettled = true;
|
|
382
|
+
if (this.stopRequested) {
|
|
383
|
+
getLogger().warn(`[${this.logTag}] prompt rejected after stopRequested: ${err.message}`);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const resultEvent = translator.produceErrorResult(sessionId, err.message);
|
|
387
|
+
this.pushEvents([resultEvent]);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* R2: poll wire.jsonl until a NEW compaction record
|
|
392
|
+
* (context.apply_compaction / full_compaction.complete) appears.
|
|
393
|
+
*
|
|
394
|
+
* The /compact prompt settles before the background compaction lands its
|
|
395
|
+
* record; returning early would let the caller dispose the connection and
|
|
396
|
+
* kill the background job. Timeout is WARN-only (the compact may have been
|
|
397
|
+
* a no-op) — never fails the turn. Aborts early on stop/forceFinish.
|
|
398
|
+
*/
|
|
399
|
+
async waitForCompactionRecord(reader, opts, baselineCount) {
|
|
400
|
+
const sessionId = this.activeSessionId ?? opts.sessionId ?? '';
|
|
401
|
+
const deadline = Date.now() + this.compactPollTimeoutMs;
|
|
402
|
+
while (Date.now() < deadline) {
|
|
403
|
+
if (this.forceFinish || this.stopRequested)
|
|
404
|
+
return;
|
|
405
|
+
const records = reader.call(this.sessionReader, sessionId, opts.cwd);
|
|
406
|
+
if (records.length > baselineCount) {
|
|
407
|
+
getLogger().info(`[${this.logTag}] compaction record observed (${records.length} > baseline ${baselineCount})`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
await new Promise((resolve) => setTimeout(resolve, COMPACT_POLL_INTERVAL_MS));
|
|
411
|
+
}
|
|
412
|
+
getLogger().warn(`[${this.logTag}] compaction record not observed within ${this.compactPollTimeoutMs}ms (baseline=${baselineCount}); proceeding without confirmation`);
|
|
413
|
+
}
|
|
414
|
+
handleNotification(method, params) {
|
|
415
|
+
const events = this.currentTranslator?.handleNotification(method, params) ?? [];
|
|
416
|
+
this.pushEvents(events);
|
|
417
|
+
}
|
|
418
|
+
handleServerRequest(id, method, params) {
|
|
419
|
+
const events = this.currentTranslator?.handleServerRequest(id, method, params) ?? [];
|
|
420
|
+
if (events.length === 0) {
|
|
421
|
+
// Unhandled/unsupported server request must be explicitly responded to
|
|
422
|
+
// (error = "rejected"), otherwise the server hangs waiting for a
|
|
423
|
+
// response. For question elicitation (§5.4), auto-respond cancelled.
|
|
424
|
+
const rpParams = params;
|
|
425
|
+
if (method === ServerRequestMethod.REQUEST_PERMISSION &&
|
|
426
|
+
(rpParams.isQuestion || !rpParams.toolCall)) {
|
|
427
|
+
this.currentClient?.respond(id, { outcome: { outcome: 'cancelled' } });
|
|
428
|
+
getLogger().info(`[${this.logTag}] auto-responded cancelled to question elicitation requestId=${id}`);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
this.currentClient?.respondError(id, RpcErrorCode.METHOD_NOT_FOUND, `Unsupported server request: ${method}`);
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
for (const ev of events) {
|
|
436
|
+
if (ev.type === 'approval_requested') {
|
|
437
|
+
this.pendingApprovals.set(ev.requestId, {
|
|
438
|
+
kind: ev.kind,
|
|
439
|
+
view: ev.view,
|
|
440
|
+
options: params.options,
|
|
441
|
+
});
|
|
442
|
+
getLogger().info(`[${this.logTag}] approval requested requestId=${ev.requestId} kind=${ev.kind}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
this.pushEvents(events);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KimiAcpTranslator: translates ACP session/update notifications and
|
|
3
|
+
* session/request_permission server requests into AgentEvents.
|
|
4
|
+
*
|
|
5
|
+
* Per-turn stateful: accumulates text/thinking deltas, tracks tool calls,
|
|
6
|
+
* and produces turn_started events with operationKind.
|
|
7
|
+
*
|
|
8
|
+
* Wire envelope (R1, source: kimi-code packages/acp-server/src/events-map.ts,
|
|
9
|
+
* verified live 2026-08-15/16 against kimi 0.36.0):
|
|
10
|
+
* session/update params = {sessionId, update: {sessionUpdate: '<kind>', ...}}
|
|
11
|
+
* — the discriminator is `update.sessionUpdate`, NOT a nested `event.type`.
|
|
12
|
+
*
|
|
13
|
+
* Event mapping (design doc §4.2):
|
|
14
|
+
* agent_message_chunk → turn_diff text snapshot (content.text accumulation)
|
|
15
|
+
* agent_thought_chunk → turn_diff reasoning snapshot (content.text accumulation)
|
|
16
|
+
* tool_call → assistant/tool_use (rawInput object passthrough,
|
|
17
|
+
* string → JSON.parse fallback)
|
|
18
|
+
* tool_call_update → user/tool_result (status:'failed' → is_error:true)
|
|
19
|
+
* plan / available_commands_update / session_info_update /
|
|
20
|
+
* current_mode_update / config_option_update → discard
|
|
21
|
+
* usage_update → live context occupancy {used, size} →
|
|
22
|
+
* {total_tokens, context_limit}; NO input/output
|
|
23
|
+
* split exists on the wire — cumulative token stats
|
|
24
|
+
* fall back to wire.jsonl usage.record (dual path)
|
|
25
|
+
* prompt stopReason:'end_turn' → result success
|
|
26
|
+
* prompt stopReason:'cancelled' → result interrupted (独立终态)
|
|
27
|
+
* prompt other stopReason/error → result error
|
|
28
|
+
* turn_started (self-produced) → operationKind 'turn'/'compaction'
|
|
29
|
+
* session/request_permission → approval_requested
|
|
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 AcpTurnStartedEvent {
|
|
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 only carries context
|
|
43
|
+
* occupancy (`used`) and the model context window (`size`) — there is NO
|
|
44
|
+
* input/output token split (events-map.ts:505-516). input_tokens/output_tokens
|
|
45
|
+
* stay undefined so the bridge falls back to wire.jsonl usage.record for
|
|
46
|
+
* cumulative token stats (R1 dual path).
|
|
47
|
+
*/
|
|
48
|
+
export interface AcpLiveUsage {
|
|
49
|
+
total_tokens?: number;
|
|
50
|
+
context_limit?: number;
|
|
51
|
+
input_tokens?: number;
|
|
52
|
+
output_tokens?: number;
|
|
53
|
+
}
|
|
54
|
+
/** Usage update event for live token display. */
|
|
55
|
+
export interface AcpUsageEvent {
|
|
56
|
+
type: 'usage';
|
|
57
|
+
usage: AcpLiveUsage;
|
|
58
|
+
timestamp?: string;
|
|
59
|
+
}
|
|
60
|
+
export type AcpTranslatorEvent = AgentEvent | AcpTurnStartedEvent | AcpUsageEvent | ApprovalRequestedEvent;
|
|
61
|
+
export declare class KimiAcpTranslator {
|
|
62
|
+
/** Accumulated text per tool-call or message (keyed by a logical item id). */
|
|
63
|
+
private textByItem;
|
|
64
|
+
/** Accumulated thinking per item. */
|
|
65
|
+
private thinkingByItem;
|
|
66
|
+
/** Pending tool calls keyed by toolCallId (for matching tool_call_update). */
|
|
67
|
+
private pendingToolCalls;
|
|
68
|
+
/** Current operation kind (turn vs compact). */
|
|
69
|
+
private operationKind;
|
|
70
|
+
/** Live usage snapshot from usage_update events (context occupancy only). */
|
|
71
|
+
private liveUsage;
|
|
72
|
+
/** Whether any usage_update has been seen this turn (no update → no usage). */
|
|
73
|
+
private hasLiveUsage;
|
|
74
|
+
/** Current turn id, set by produceTurnStarted; carried on turn_diff events. */
|
|
75
|
+
private currentTurnId;
|
|
76
|
+
/**
|
|
77
|
+
* Handle a notification from the ACP server and return translated events.
|
|
78
|
+
*/
|
|
79
|
+
handleNotification(method: string, params: unknown): AcpTranslatorEvent[];
|
|
80
|
+
/**
|
|
81
|
+
* Handle a server request (reverse RPC from the ACP server).
|
|
82
|
+
*/
|
|
83
|
+
handleServerRequest(id: number | string, method: string, params: unknown): AcpTranslatorEvent[];
|
|
84
|
+
/**
|
|
85
|
+
* Produce a turn_started event. Called by the runner when session/prompt
|
|
86
|
+
* is sent (no wire notification for this — self-produced).
|
|
87
|
+
*/
|
|
88
|
+
produceTurnStarted(sessionId: string, turnId: string): AcpTurnStartedEvent;
|
|
89
|
+
/**
|
|
90
|
+
* Translate a prompt response into a result event. Carries the live usage
|
|
91
|
+
* snapshot only when a usage_update was seen (otherwise undefined, so the
|
|
92
|
+
* bridge falls back to wire.jsonl usage.record readback).
|
|
93
|
+
*/
|
|
94
|
+
handlePromptResponse(sessionId: string, result: {
|
|
95
|
+
stopReason: string;
|
|
96
|
+
}): AgentEvent;
|
|
97
|
+
/**
|
|
98
|
+
* Produce an error result event (for JSON-RPC errors during prompt).
|
|
99
|
+
*/
|
|
100
|
+
produceErrorResult(sessionId: string, errorMessage: string): AgentEvent;
|
|
101
|
+
/**
|
|
102
|
+
* Set the operation kind (turn vs compact).
|
|
103
|
+
*/
|
|
104
|
+
setOperationKind(kind: 'turn' | 'compact'): void;
|
|
105
|
+
private handleAgentMessageChunk;
|
|
106
|
+
private handleAgentThoughtChunk;
|
|
107
|
+
private handleToolCall;
|
|
108
|
+
private handleToolCallUpdate;
|
|
109
|
+
private handleUsageUpdate;
|
|
110
|
+
private handleRequestPermission;
|
|
111
|
+
/**
|
|
112
|
+
* Derive the approval decision list from the server's offered options.
|
|
113
|
+
* accept/decline/cancel are universal; acceptForSession requires an
|
|
114
|
+
* always-class option (kimi `approve_always`, opencode `allow_always`).
|
|
115
|
+
*/
|
|
116
|
+
private deriveAvailableDecisions;
|
|
117
|
+
/**
|
|
118
|
+
* ResultEvent.usage requires input_tokens/output_tokens: number, but the
|
|
119
|
+
* ACP wire has no such split — the live snapshot is context occupancy only.
|
|
120
|
+
* Cast is deliberate: consumers must tolerate undefined input/output (the
|
|
121
|
+
* bridge's kimi path already falls back to wire.jsonl readback).
|
|
122
|
+
*/
|
|
123
|
+
private liveUsageSnapshot;
|
|
124
|
+
}
|