lark-remote 0.1.12 → 0.2.0

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 (160) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.en.md +2 -6
  3. package/README.md +3 -7
  4. package/dist/bridge/final-usage.d.ts +24 -0
  5. package/dist/bridge/final-usage.js +55 -0
  6. package/dist/bridge/inbound-media.d.ts +0 -3
  7. package/dist/bridge/inbound-media.js +0 -1
  8. package/dist/bridge/index.d.ts +7 -7
  9. package/dist/bridge/index.js +100 -133
  10. package/dist/bridge/queue-manager.js +4 -3
  11. package/dist/card/approval-render.js +39 -94
  12. package/dist/card/bash-renderer.js +12 -21
  13. package/dist/card/card-shared.d.ts +14 -5
  14. package/dist/card/card-shared.js +23 -34
  15. package/dist/card/run-renderer.d.ts +0 -22
  16. package/dist/card/run-renderer.js +206 -153
  17. package/dist/card/run-state.d.ts +29 -1
  18. package/dist/card/run-state.js +172 -84
  19. package/dist/card/text-truncate.d.ts +0 -9
  20. package/dist/card/text-truncate.js +0 -9
  21. package/dist/card/tool-render.d.ts +4 -1
  22. package/dist/card/tool-render.js +76 -32
  23. package/dist/cli.js +2 -2
  24. package/dist/common/fs.d.ts +14 -0
  25. package/dist/common/fs.js +57 -3
  26. package/dist/common/truncate.d.ts +0 -6
  27. package/dist/common/truncate.js +1 -1
  28. package/dist/common/ttl-cache.d.ts +24 -0
  29. package/dist/common/ttl-cache.js +53 -0
  30. package/dist/config/codex-config.js +14 -11
  31. package/dist/config/index.d.ts +0 -8
  32. package/dist/config/index.js +0 -16
  33. package/dist/config/kimi-config.d.ts +1 -1
  34. package/dist/config/kimi-config.js +2 -1
  35. package/dist/config/opencode-config.d.ts +1 -1
  36. package/dist/config/opencode-config.js +16 -10
  37. package/dist/config/qr.d.ts +36 -0
  38. package/dist/config/qr.js +124 -0
  39. package/dist/config/wizard.js +69 -27
  40. package/dist/connector/index.d.ts +3 -2
  41. package/dist/connector/index.js +5 -3
  42. package/dist/index.js +14 -41
  43. package/dist/instance-lock.js +7 -0
  44. package/dist/persistence/atomic-write.d.ts +2 -0
  45. package/dist/persistence/atomic-write.js +20 -5
  46. package/dist/persistence/load-json-file.d.ts +1 -2
  47. package/dist/persistence/load-json-file.js +2 -3
  48. package/dist/platform/agent-stopper.d.ts +22 -0
  49. package/dist/platform/agent-stopper.js +19 -0
  50. package/dist/platform/command.d.ts +11 -0
  51. package/dist/platform/command.js +107 -0
  52. package/dist/platform/identity.d.ts +33 -0
  53. package/dist/platform/identity.js +187 -0
  54. package/dist/platform/path.d.ts +24 -0
  55. package/dist/platform/path.js +110 -0
  56. package/dist/platform/select.d.ts +9 -0
  57. package/dist/platform/select.js +12 -0
  58. package/dist/platform/shell.d.ts +51 -0
  59. package/dist/platform/shell.js +75 -0
  60. package/dist/platform/spawn.d.ts +41 -0
  61. package/dist/platform/spawn.js +77 -0
  62. package/dist/platform/terminator-posix.d.ts +38 -0
  63. package/dist/platform/terminator-posix.js +103 -0
  64. package/dist/platform/terminator-win32.d.ts +21 -0
  65. package/dist/platform/terminator-win32.js +110 -0
  66. package/dist/platform/terminator.d.ts +44 -0
  67. package/dist/platform/terminator.js +24 -0
  68. package/dist/platform/types.d.ts +23 -0
  69. package/dist/platform/types.js +5 -0
  70. package/dist/restart.js +4 -2
  71. package/dist/router/card-helpers.d.ts +33 -6
  72. package/dist/router/card-helpers.js +67 -11
  73. package/dist/router/config/common/render.d.ts +1 -1
  74. package/dist/router/config/common/render.js +1 -1
  75. package/dist/router/config-diff.d.ts +4 -0
  76. package/dist/router/config-diff.js +70 -0
  77. package/dist/router/index.d.ts +73 -6
  78. package/dist/router/index.js +663 -590
  79. package/dist/router/utils.d.ts +6 -0
  80. package/dist/router/utils.js +12 -0
  81. package/dist/runner/bash/runner.d.ts +9 -2
  82. package/dist/runner/bash/runner.js +45 -20
  83. package/dist/runner/claude/session.d.ts +4 -4
  84. package/dist/runner/claude/session.js +18 -9
  85. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
  86. package/dist/runner/codex/app-server/runner.d.ts +1 -16
  87. package/dist/runner/codex/app-server/runner.js +2 -2
  88. package/dist/runner/codex/app-server/translator.d.ts +18 -0
  89. package/dist/runner/codex/app-server/translator.js +146 -0
  90. package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
  91. package/dist/runner/common/acp/base-acp-runner.js +248 -0
  92. package/dist/runner/common/acp/base-translator.d.ts +141 -0
  93. package/dist/runner/common/acp/base-translator.js +271 -0
  94. package/dist/runner/common/acp/protocol-helpers.d.ts +70 -0
  95. package/dist/runner/common/acp/protocol-helpers.js +82 -0
  96. package/dist/runner/common/connection-based-runner.d.ts +0 -2
  97. package/dist/runner/common/connection-based-runner.js +1 -1
  98. package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
  99. package/dist/runner/common/jsonrpc/transport.js +25 -5
  100. package/dist/runner/common/process-stopper.d.ts +4 -26
  101. package/dist/runner/common/process-stopper.js +4 -76
  102. package/dist/runner/common/runner-utils.d.ts +0 -5
  103. package/dist/runner/common/runner-utils.js +0 -7
  104. package/dist/runner/common/spawning-runner.d.ts +42 -132
  105. package/dist/runner/common/spawning-runner.js +52 -271
  106. package/dist/runner/common/usage.d.ts +2 -0
  107. package/dist/runner/common/usage.js +2 -0
  108. package/dist/runner/dsh/index.d.ts +0 -2
  109. package/dist/runner/dsh/index.js +0 -2
  110. package/dist/runner/dsh/runner.d.ts +0 -2
  111. package/dist/runner/dsh/runner.js +1 -1
  112. package/dist/runner/index.d.ts +1 -1
  113. package/dist/runner/kimi/acp/runner.d.ts +63 -53
  114. package/dist/runner/kimi/acp/runner.js +189 -334
  115. package/dist/runner/kimi/acp/translator.d.ts +18 -101
  116. package/dist/runner/kimi/acp/translator.js +61 -255
  117. package/dist/runner/opencode/acp/runner.d.ts +22 -46
  118. package/dist/runner/opencode/acp/runner.js +74 -297
  119. package/dist/runner/opencode/acp/translator.d.ts +28 -104
  120. package/dist/runner/opencode/acp/translator.js +54 -249
  121. package/dist/runner/pi/rpc/index.d.ts +0 -2
  122. package/dist/runner/pi/rpc/index.js +0 -2
  123. package/dist/runner/pi/rpc/translator.d.ts +3 -0
  124. package/dist/runner/pi/rpc/translator.js +47 -9
  125. package/dist/runner/probe.d.ts +8 -11
  126. package/dist/runner/probe.js +17 -21
  127. package/dist/runner/registry.d.ts +4 -4
  128. package/dist/runner/registry.js +4 -4
  129. package/dist/runner/resolve-agent-choices.js +6 -2
  130. package/dist/runner/sync-agent-choices.js +1 -1
  131. package/dist/runner/types.d.ts +38 -1
  132. package/dist/session/claude/session-index.js +3 -1
  133. package/dist/session/claude/sessions.js +37 -64
  134. package/dist/session/codex/rollout-reader.d.ts +10 -14
  135. package/dist/session/codex/rollout-reader.js +126 -48
  136. package/dist/session/common/constants.d.ts +7 -0
  137. package/dist/session/common/constants.js +9 -0
  138. package/dist/session/common/pagination.d.ts +0 -2
  139. package/dist/session/common/pagination.js +1 -1
  140. package/dist/session/common/recency.d.ts +7 -0
  141. package/dist/session/common/recency.js +10 -0
  142. package/dist/session/common/two-pass.d.ts +22 -0
  143. package/dist/session/common/two-pass.js +70 -0
  144. package/dist/session/common/usage-accumulator.d.ts +14 -0
  145. package/dist/session/common/usage-accumulator.js +15 -0
  146. package/dist/session/dsh/sessions.js +15 -11
  147. package/dist/session/kimi/sessions.d.ts +24 -4
  148. package/dist/session/kimi/sessions.js +99 -103
  149. package/dist/session/opencode/sessions.d.ts +2 -14
  150. package/dist/session/opencode/sessions.js +35 -15
  151. package/dist/session/pi/sessions.js +64 -95
  152. package/dist/update/index.d.ts +2 -3
  153. package/dist/update/index.js +1 -2
  154. package/dist/update/install.d.ts +2 -1
  155. package/dist/update/install.js +58 -9
  156. package/dist/update/version-check.d.ts +5 -0
  157. package/dist/update/version-check.js +9 -0
  158. package/package.json +9 -7
  159. package/dist/update/startup-hint.d.ts +0 -5
  160. package/dist/update/startup-hint.js +0 -11
@@ -0,0 +1,116 @@
1
+ /**
2
+ * BaseAcpRunner: shared turn machinery for the kimi / opencode ACP runners.
3
+ *
4
+ * Both runners extend ConnectionBasedRunner and were structurally aligned
5
+ * (kimi's header even said so) — but session/resume→set_config→turn_started→
6
+ * prompt orchestration, compact orchestration, approval response dispatch,
7
+ * cancel/clear/release plumbing and the pendingApprovals registry were
8
+ * line-for-line duplicated with only constants/field names differing
9
+ * (2026-09 round-2 simplification). This base absorbs them; what stays in
10
+ * subclasses:
11
+ * - applyTurnSettings: mode/model下发 semantics differ (kimi unconditional,
12
+ * opencode conditional on cached values)
13
+ * - onCompactPromptFired: kimi polls wire.jsonl compaction records (§5.2,
14
+ * background compaction), opencode settles synchronously
15
+ * - shouldDeferStop: kimi defers while prompt unsettled, opencode defers
16
+ * until the prompt has been SENT (deliberately narrower window)
17
+ * - handleNotification/handleServerRequest: kimi filters terminal-backed
18
+ * Bash notifications and serves terminal/* reverse RPC; opencode is plain
19
+ * - getStatusInfo / updateApprovalMode: agent-specific extras & mode vocab
20
+ */
21
+ import type { AgentEvent, AgentKind, AgentSessionReader, SpawnOptions } from '../../types.js';
22
+ import { ConnectionManager } from '../jsonrpc/connection-manager.js';
23
+ import { JsonRpcClient } from '../jsonrpc/client.js';
24
+ import { type SessionNewResult, type SessionResumeResult, type SessionPromptResult, type RequestPermissionParams } from './protocol-types.js';
25
+ import { type AcpPendingApproval } from './protocol-helpers.js';
26
+ import { ConnectionBasedRunner } from '../connection-based-runner.js';
27
+ import { BaseAcpTranslator, type AcpTranslatorEvent } from './base-translator.js';
28
+ export type { AcpTranslatorEvent } from './base-translator.js';
29
+ export declare abstract class BaseAcpRunner<TTranslator extends BaseAcpTranslator> extends ConnectionBasedRunner<JsonRpcClient, AcpTranslatorEvent> {
30
+ protected readonly connectionManager: ConnectionManager;
31
+ protected currentTranslator: TTranslator | null;
32
+ protected activeSessionId: string | null;
33
+ protected model?: string;
34
+ /** Pending approval requests: requestId → kind + view + options. */
35
+ protected pendingApprovals: Map<string | number, AcpPendingApproval>;
36
+ /** Tracker for the in-flight prompt request — needed for cancellation. */
37
+ protected promptSettled: boolean;
38
+ /** Whether the prompt request has been fired (opencode defer-stop window). */
39
+ protected promptSent: boolean;
40
+ protected constructor(opts: {
41
+ kind: AgentKind;
42
+ sessionReader: AgentSessionReader;
43
+ turnIdleTimeoutMs?: number;
44
+ }, connectionManager: ConnectionManager);
45
+ /** failTurn message when the ACP connection drops mid-turn. */
46
+ protected abstract get connectionClosedMessage(): string;
47
+ /** Create the per-turn translator instance. */
48
+ protected abstract createTranslator(): TTranslator;
49
+ /**
50
+ * Apply turn-scoped settings on a fresh session: configured model and
51
+ * permission/mode. kimi sends both unconditionally (fresh ACP sessions
52
+ * always start in 'default'), opencode skips when the cached value already
53
+ * matches. Failures are non-fatal (warn only).
54
+ */
55
+ protected abstract applyTurnSettings(client: JsonRpcClient, sessionId: string): Promise<void>;
56
+ /**
57
+ * Dispatch the compact prompt result once fired. kimi: background
58
+ * compaction — poll wire.jsonl for a NEW terminal record before translating
59
+ * (§5.2). opencode: synchronous — standard prompt-settle translation.
60
+ */
61
+ protected abstract onCompactPromptFired(promptPromise: Promise<SessionPromptResult>, translator: TTranslator, sessionId: string, opts: SpawnOptions, baselineTotalCount: number): void;
62
+ protected currentSessionId(): string | null;
63
+ protected cancelCurrentTurn(): Promise<void>;
64
+ protected clearTurnState(): void;
65
+ protected releaseConnection(cwd: string): Promise<void>;
66
+ protected notifyIdle(cwd: string): void;
67
+ protected disposeConnections(): Promise<void>;
68
+ /**
69
+ * Respond to an approval server request. `response` is the bridge
70
+ * ApprovalAction (`{ action: 'accept' | 'decline' | 'cancel' }`).
71
+ */
72
+ respondApproval(requestId: number | string, response: unknown): Promise<void>;
73
+ /** Agent-specific outcome encoding (kimi: question bridge; opencode: permission). */
74
+ protected abstract buildApprovalOutcome(action: string, pending: AcpPendingApproval, response: unknown): unknown;
75
+ /** Shared tail of updateApprovalMode: re-send session/set_mode to the live session. */
76
+ protected sendApprovalModeUpdate(modeId: string): Promise<void>;
77
+ /**
78
+ * Register approval_requested events yielded by the translator into the
79
+ * pendingApprovals map (shared tail of handleServerRequest).
80
+ */
81
+ protected registerApprovalEvents(events: AcpTranslatorEvent[], options: RequestPermissionParams['options'], proto?: AcpPendingApproval['proto']): void;
82
+ /**
83
+ * Standard prompt-settle translation: on resolve → result event (unless
84
+ * stopRequested); on reject → error result (or warn when already stopped).
85
+ * `context` only decorates the stopRequested warn line.
86
+ */
87
+ protected handlePromptRejected(err: unknown, translator: TTranslator, sessionId: string, context: string): void;
88
+ protected handlePromptResolved(result: SessionPromptResult, translator: TTranslator, sessionId: string): void;
89
+ /**
90
+ * Acquire the connection and set up the turn: session/new (new session) or
91
+ * session/resume (existing sessionId) → applyTurnSettings → session/prompt.
92
+ */
93
+ protected setupTurn(message: string, opts: SpawnOptions): Promise<void>;
94
+ /**
95
+ * Run a compact operation on the current session: resume → '/compact'
96
+ * prompt text (both servers intercept it) → agent-specific result dispatch.
97
+ */
98
+ runCompact(_message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
99
+ /** After the connection is acquired per turn (kimi: record active cwd). */
100
+ protected onConnectionAcquired(_opts: SpawnOptions): void;
101
+ /** When the ACP connection drops (kimi: kill live terminals). */
102
+ protected onConnectionClosed(): void;
103
+ /** After the turn's session id is known (opencode: start log error monitor). */
104
+ protected onTurnSessionReady(_sessionId: string, _opts: SpawnOptions): void;
105
+ /** After session/new|resume resolves (opencode: track configOptions). */
106
+ protected onSessionEstablished(_result: SessionResumeResult | SessionNewResult): void;
107
+ /** Compaction baseline record count, captured before the compact prompt (kimi). */
108
+ protected compactBaseline(_sessionId: string, _opts: SpawnOptions): number;
109
+ /** Wire the active client's JSON-RPC hooks into the subclass dispatcher. */
110
+ protected abstract handleNotification(method: string, params: unknown): void;
111
+ protected abstract handleServerRequest(id: number | string, method: string, params: unknown): void;
112
+ /** Extra per-turn state cleanup (kimi: active cwd; opencode: error monitor). */
113
+ protected onClearTurnState(): void;
114
+ /** Extra dispose-time cleanup before connections are disposed (kimi: terminals). */
115
+ protected beforeDisposeConnections(): Promise<void>;
116
+ }
@@ -0,0 +1,248 @@
1
+ import { respondAcpApproval, sendAcpSetMode } from './protocol-helpers.js';
2
+ import { getLogger } from '../../../logger/index.js';
3
+ import { ConnectionBasedRunner } from '../connection-based-runner.js';
4
+ export class BaseAcpRunner extends ConnectionBasedRunner {
5
+ connectionManager;
6
+ currentTranslator = null;
7
+ activeSessionId = null;
8
+ model;
9
+ /** Pending approval requests: requestId → kind + view + options. */
10
+ pendingApprovals = new Map();
11
+ /** Tracker for the in-flight prompt request — needed for cancellation. */
12
+ promptSettled = false;
13
+ /** Whether the prompt request has been fired (opencode defer-stop window). */
14
+ promptSent = false;
15
+ constructor(opts, connectionManager) {
16
+ super(opts);
17
+ this.connectionManager = connectionManager;
18
+ }
19
+ // =========================================================================
20
+ // Shared plumbing
21
+ // =========================================================================
22
+ currentSessionId() {
23
+ return this.activeSessionId;
24
+ }
25
+ async cancelCurrentTurn() {
26
+ if (!this.currentClient || !this.activeSessionId)
27
+ return;
28
+ try {
29
+ // session/cancel is a NOTIFICATION (no id, no response) per ACP spec.
30
+ const cancelParams = { sessionId: this.activeSessionId };
31
+ this.currentClient.notify('session/cancel', cancelParams);
32
+ }
33
+ catch (err) {
34
+ getLogger().warn(`[${this.logTag}] session/cancel failed: ${err.message}`);
35
+ }
36
+ }
37
+ clearTurnState() {
38
+ this.onClearTurnState();
39
+ this.currentTranslator = null;
40
+ this.promptSettled = false;
41
+ this.promptSent = false;
42
+ this.activeSessionId = null;
43
+ }
44
+ async releaseConnection(cwd) {
45
+ await this.connectionManager.release(cwd);
46
+ }
47
+ notifyIdle(cwd) {
48
+ this.connectionManager.notifyIdle(cwd);
49
+ }
50
+ async disposeConnections() {
51
+ await this.beforeDisposeConnections();
52
+ await this.connectionManager.disposeAll();
53
+ }
54
+ /**
55
+ * Respond to an approval server request. `response` is the bridge
56
+ * ApprovalAction (`{ action: 'accept' | 'decline' | 'cancel' }`).
57
+ */
58
+ async respondApproval(requestId, response) {
59
+ respondAcpApproval({
60
+ client: this.currentClient,
61
+ pendingApprovals: this.pendingApprovals,
62
+ requestId,
63
+ response,
64
+ logTag: this.logTag,
65
+ buildOutcome: (action, pending, resp) => this.buildApprovalOutcome(action, pending, resp),
66
+ });
67
+ }
68
+ /** Shared tail of updateApprovalMode: re-send session/set_mode to the live session. */
69
+ async sendApprovalModeUpdate(modeId) {
70
+ await sendAcpSetMode({
71
+ client: this.currentClient,
72
+ activeSessionId: this.activeSessionId,
73
+ modeId,
74
+ logTag: this.logTag,
75
+ });
76
+ }
77
+ /**
78
+ * Register approval_requested events yielded by the translator into the
79
+ * pendingApprovals map (shared tail of handleServerRequest).
80
+ */
81
+ registerApprovalEvents(events, options, proto) {
82
+ for (const ev of events) {
83
+ if (ev.type === 'approval_requested') {
84
+ this.pendingApprovals.set(ev.requestId, {
85
+ kind: ev.kind,
86
+ view: ev.view,
87
+ options,
88
+ ...(proto ? { proto } : {}),
89
+ });
90
+ getLogger().info(`[${this.logTag}] approval requested requestId=${ev.requestId} kind=${ev.kind}`);
91
+ }
92
+ }
93
+ }
94
+ /**
95
+ * Standard prompt-settle translation: on resolve → result event (unless
96
+ * stopRequested); on reject → error result (or warn when already stopped).
97
+ * `context` only decorates the stopRequested warn line.
98
+ */
99
+ handlePromptRejected(err, translator, sessionId, context) {
100
+ this.promptSettled = true;
101
+ if (this.stopRequested) {
102
+ getLogger().warn(`[${this.logTag}] ${context} prompt rejected after stopRequested: ${err.message}`);
103
+ return;
104
+ }
105
+ const resultEvent = translator.produceErrorResult(sessionId, err.message);
106
+ this.pushEvents([resultEvent]);
107
+ }
108
+ handlePromptResolved(result, translator, sessionId) {
109
+ this.promptSettled = true;
110
+ if (this.stopRequested)
111
+ return; // already cancelled
112
+ const resultEvent = translator.handlePromptResponse(sessionId, result);
113
+ this.pushEvents([resultEvent]);
114
+ }
115
+ // =========================================================================
116
+ // Turn orchestration
117
+ // =========================================================================
118
+ /**
119
+ * Acquire the connection and set up the turn: session/new (new session) or
120
+ * session/resume (existing sessionId) → applyTurnSettings → session/prompt.
121
+ */
122
+ async setupTurn(message, opts) {
123
+ const client = await this.connectionManager.acquire(opts.cwd);
124
+ this.connectionManager.notifyActivity(opts.cwd);
125
+ this.currentClient = client;
126
+ this.onConnectionAcquired(opts);
127
+ client.setHooks({
128
+ onNotification: (method, params) => this.handleNotification(method, params),
129
+ onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
130
+ onClose: () => {
131
+ getLogger().warn(`[${this.logTag}] client connection closed`);
132
+ this.onConnectionClosed();
133
+ this.failTurn(this.connectionClosedMessage);
134
+ },
135
+ });
136
+ let sessionId;
137
+ if (opts.sessionId) {
138
+ const resumeParams = {
139
+ sessionId: opts.sessionId,
140
+ cwd: opts.cwd,
141
+ };
142
+ const resumeResult = await client.request('session/resume', resumeParams);
143
+ this.onSessionEstablished(resumeResult);
144
+ sessionId = opts.sessionId;
145
+ }
146
+ else {
147
+ const newParams = { cwd: opts.cwd, mcpServers: [] };
148
+ const newResult = await client.request('session/new', newParams);
149
+ this.onSessionEstablished(newResult);
150
+ sessionId = newResult.sessionId;
151
+ }
152
+ this.activeSessionId = sessionId;
153
+ this.onTurnSessionReady(sessionId, opts);
154
+ await this.applyTurnSettings(client, sessionId);
155
+ const translator = this.createTranslator();
156
+ this.currentTranslator = translator;
157
+ // Emit turn_started before prompt
158
+ const turnId = `turn-${Date.now()}`;
159
+ const turnStarted = translator.produceTurnStarted(sessionId, turnId);
160
+ this.currentTurnId = turnId;
161
+ this.pushEvents([turnStarted]);
162
+ // Send the prompt. The prompt request races with notifications (the
163
+ // server streams session/update before the prompt response); the
164
+ // response settles the turn and is translated into the result event.
165
+ const promptParams = {
166
+ sessionId,
167
+ prompt: [{ type: 'text', text: message }],
168
+ };
169
+ this.promptSettled = false;
170
+ const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
171
+ this.promptSent = true;
172
+ promptPromise.then((result) => this.handlePromptResolved(result, translator, sessionId), (err) => this.handlePromptRejected(err, translator, sessionId, ''));
173
+ }
174
+ /**
175
+ * Run a compact operation on the current session: resume → '/compact'
176
+ * prompt text (both servers intercept it) → agent-specific result dispatch.
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
+ this.onConnectionAcquired(opts);
184
+ client.setHooks({
185
+ onNotification: (method, params) => this.handleNotification(method, params),
186
+ onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
187
+ onClose: () => {
188
+ this.onConnectionClosed();
189
+ this.failTurn(this.connectionClosedMessage);
190
+ },
191
+ });
192
+ const sessionId = opts.sessionId;
193
+ if (!sessionId) {
194
+ throw new Error('compact requires a sessionId');
195
+ }
196
+ this.activeSessionId = sessionId;
197
+ this.onTurnSessionReady(sessionId, opts);
198
+ // Cold-connection fallback: session/resume loads the session into
199
+ // memory. On a reused connection the session is already loaded —
200
+ // resume is idempotent and harmless.
201
+ const resumeParams = { sessionId, cwd: opts.cwd };
202
+ const resumeResult = await client.request('session/resume', resumeParams);
203
+ this.onSessionEstablished(resumeResult);
204
+ const translator = this.createTranslator();
205
+ translator.setOperationKind('compact');
206
+ this.currentTranslator = translator;
207
+ // Emit turn_started with operationKind='compaction' before the prompt.
208
+ const turnId = `compact-${Date.now()}`;
209
+ const turnStarted = translator.produceTurnStarted(sessionId, turnId);
210
+ this.currentTurnId = turnId;
211
+ this.pushEvents([turnStarted]);
212
+ const promptParams = {
213
+ sessionId,
214
+ prompt: [{ type: 'text', text: '/compact' }],
215
+ };
216
+ // Baseline is captured BEFORE the prompt fires (kimi §5.2: a begin
217
+ // record landing after the fire must count as "new activity").
218
+ const baselineTotalCount = this.compactBaseline(sessionId, opts);
219
+ this.promptSettled = false;
220
+ // ACP holds the session/prompt response for the ENTIRE turn, so the
221
+ // RPC-level timeout must not apply — turn liveness is guarded by the
222
+ // rolling idle watchdog instead.
223
+ const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
224
+ this.promptSent = true;
225
+ this.onCompactPromptFired(promptPromise, translator, sessionId, opts, baselineTotalCount);
226
+ });
227
+ }
228
+ // =========================================================================
229
+ // Optional hooks (defaults: no-op)
230
+ // =========================================================================
231
+ /** After the connection is acquired per turn (kimi: record active cwd). */
232
+ onConnectionAcquired(_opts) { }
233
+ /** When the ACP connection drops (kimi: kill live terminals). */
234
+ onConnectionClosed() { }
235
+ /** After the turn's session id is known (opencode: start log error monitor). */
236
+ onTurnSessionReady(_sessionId, _opts) { }
237
+ /** After session/new|resume resolves (opencode: track configOptions). */
238
+ onSessionEstablished(_result) { }
239
+ /** Compaction baseline record count, captured before the compact prompt (kimi). */
240
+ compactBaseline(_sessionId, _opts) {
241
+ return 0;
242
+ }
243
+ /** Extra per-turn state cleanup (kimi: active cwd; opencode: error monitor). */
244
+ onClearTurnState() { }
245
+ /** Extra dispose-time cleanup before connections are disposed (kimi: terminals). */
246
+ async beforeDisposeConnections() { }
247
+ }
248
+ //# sourceMappingURL=base-acp-runner.js.map
@@ -0,0 +1,141 @@
1
+ /**
2
+ * BaseAcpTranslator: shared skeleton for the kimi / opencode ACP translators.
3
+ *
4
+ * Both agents' session/update notification envelopes, prompt-response
5
+ * stopReason mapping, tool_call/tool_call_update normalization, usage_update
6
+ * occupancy bookkeeping and self-produced turn_started events are
7
+ * line-for-line identical — extracted here in the 2026-09 round-2
8
+ * simplification. What genuinely diverges is the content channel:
9
+ * kimi accumulates wire deltas into turn_diff snapshots (snapshot semantics,
10
+ * see KimiAcpTranslator header), opencode forwards `assistant` deltas
11
+ * directly (delta semantics) — those chunk handlers stay abstract.
12
+ *
13
+ * Extension points (all no-op or passthrough by default):
14
+ * handleAgentMessageChunk / handleAgentThoughtChunk — content channel (abstract)
15
+ * handleRequestPermission — approval rendering (abstract)
16
+ * handleOtherServerRequest — extra reverse RPC (kimi: elicitation)
17
+ * extractToolResultContent — tool_result content (default: rawOutput)
18
+ * onUsageSample — extra usage bookkeeping (opencode: cost)
19
+ */
20
+ import type { AgentEvent, ApprovalRequestedEvent, ResultEvent } from '../../types.js';
21
+ import { type AgentMessageChunkEvent, type AgentThoughtChunkEvent, type ToolCallEvent, type ToolCallUpdateEvent, type UsageUpdateEvent, type RequestPermissionParams, type AcpPlanEvent } from './protocol-types.js';
22
+ /** Self-produced turn_started event (no wire equivalent — produced on prompt start). */
23
+ export interface AcpTurnStartedEvent {
24
+ type: 'turn_started';
25
+ threadId: string;
26
+ turnId: string;
27
+ operationKind: 'turn' | 'compaction';
28
+ timestamp?: string;
29
+ }
30
+ /**
31
+ * Live usage snapshot from usage_update. The ACP wire carries context
32
+ * occupancy (`used`) and the model context window (`size`) — there is NO
33
+ * input/output token split. input_tokens/output_tokens stay undefined so the
34
+ * bridge falls back to per-agent transcript readback for cumulative stats.
35
+ */
36
+ export interface AcpLiveUsage {
37
+ total_tokens?: number;
38
+ context_limit?: number;
39
+ input_tokens?: number;
40
+ output_tokens?: number;
41
+ }
42
+ /** Usage update event for live token display. */
43
+ export interface AcpUsageEvent {
44
+ type: 'usage';
45
+ usage: AcpLiveUsage;
46
+ timestamp?: string;
47
+ }
48
+ export type AcpTranslatorEvent = AgentEvent | AcpTurnStartedEvent | AcpUsageEvent | ApprovalRequestedEvent;
49
+ export declare abstract class BaseAcpTranslator {
50
+ /** Logger tag, e.g. 'kimi-acp-translator'. */
51
+ protected abstract readonly logTag: string;
52
+ /** Current operation kind (turn vs compact). */
53
+ private operationKind;
54
+ /** Live usage snapshot from usage_update events (context occupancy only). */
55
+ protected liveUsage: AcpLiveUsage;
56
+ /** Whether any usage_update has been seen this turn (no update → no usage). */
57
+ protected hasLiveUsage: boolean;
58
+ /** Session cost from usage_update cost.amount (USD). Written by opencode only. */
59
+ protected liveCostUsd?: number;
60
+ /** Current turn id, set by produceTurnStarted. */
61
+ protected currentTurnId: string;
62
+ /**
63
+ * Handle a notification from the ACP server and return translated events.
64
+ * Wire envelope: {sessionId, update: {sessionUpdate: '<kind>', ...}} — the
65
+ * discriminator is `update.sessionUpdate`.
66
+ */
67
+ handleNotification(method: string, params: unknown): AcpTranslatorEvent[];
68
+ /**
69
+ * Handle a server request (reverse RPC from the ACP server). Subclasses
70
+ * hook additional methods via handleOtherServerRequest.
71
+ */
72
+ handleServerRequest(id: number | string, method: string, params: unknown): AcpTranslatorEvent[];
73
+ /** Hook for server requests other than session/request_permission. */
74
+ protected handleOtherServerRequest(_id: number | string, _method: string, _params: unknown): AcpTranslatorEvent[];
75
+ /**
76
+ * Produce a turn_started event. Called by the runner when session/prompt
77
+ * is sent (no wire notification for this — self-produced).
78
+ */
79
+ produceTurnStarted(sessionId: string, turnId: string): AcpTurnStartedEvent;
80
+ /**
81
+ * Translate a prompt response into a result event.
82
+ * stopReason: end_turn → success; cancelled → interrupted (independent
83
+ * terminal state, must NOT merge into error); anything else
84
+ * (max_tokens / refusal / …) → error.
85
+ */
86
+ handlePromptResponse(sessionId: string, result: {
87
+ stopReason: string;
88
+ }): AgentEvent;
89
+ /**
90
+ * Produce an error result event (for JSON-RPC errors during prompt).
91
+ */
92
+ produceErrorResult(sessionId: string, errorMessage: string): AgentEvent;
93
+ /**
94
+ * Set the operation kind (turn vs compact).
95
+ */
96
+ setOperationKind(kind: 'turn' | 'compact'): void;
97
+ /**
98
+ * plan update → PlanEvent(信息保真 C3.2:不再显式丢弃)。
99
+ * ACP PlanEntry: { content, priority, status }(status: pending/in_progress/
100
+ * completed)。基类具体方法:kimi/opencode wire 结构相同,直接复用。
101
+ */
102
+ protected handlePlan(event: AcpPlanEvent): AcpTranslatorEvent[];
103
+ /**
104
+ * tool_call → assistant/tool_use. rawInput normalization is contractual:
105
+ * missing → {} (lazy-create tool_call carries no rawInput field), string →
106
+ * JSON.parse (older agents send JSON strings) — keeps the card from
107
+ * crashing on undefined input.
108
+ *
109
+ * 信息保真 C3.1:kind 命中 KIND_TO_TOOL 时用映射名渲染结构化面板;
110
+ * title 与 name 不同时把 title 存到 content block 的 summary 字段
111
+ * (渲染层做面板副标题)。
112
+ */
113
+ protected handleToolCall(event: ToolCallEvent, _sessionId: string): AcpTranslatorEvent[];
114
+ /**
115
+ * tool_call_update → user/tool_result. status:'failed' → is_error:true.
116
+ */
117
+ protected handleToolCallUpdate(event: ToolCallUpdateEvent, _sessionId: string): AcpTranslatorEvent[];
118
+ /**
119
+ * tool_result content extraction. Default: rawOutput passthrough (kimi
120
+ * semantics). opencode overrides — its rawOutput is an OBJECT
121
+ * ({output} / {error}).
122
+ */
123
+ protected extractToolResultContent(event: ToolCallUpdateEvent): unknown;
124
+ /**
125
+ * usage_update → context occupancy bookkeeping. The wire shape is
126
+ * {used, size} (+ opencode: cost:{amount}) — context occupancy, not
127
+ * cumulative tokens.
128
+ */
129
+ protected handleUsageUpdate(event: UsageUpdateEvent): AcpTranslatorEvent[];
130
+ /** Extra per-sample usage bookkeeping (opencode: session cost amount). */
131
+ protected onUsageSample(_event: UsageUpdateEvent): void;
132
+ /**
133
+ * ResultEvent.usage requires input_tokens/output_tokens: number, but the
134
+ * ACP wire has no such split — the live snapshot is context occupancy only.
135
+ * Cast is deliberate: consumers must tolerate undefined input/output.
136
+ */
137
+ protected liveUsageSnapshot(): ResultEvent['usage'];
138
+ protected abstract handleAgentMessageChunk(event: AgentMessageChunkEvent, sessionId: string): AcpTranslatorEvent[];
139
+ protected abstract handleAgentThoughtChunk(event: AgentThoughtChunkEvent, sessionId: string): AcpTranslatorEvent[];
140
+ protected abstract handleRequestPermission(requestId: number | string, params: RequestPermissionParams): AcpTranslatorEvent[];
141
+ }