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
@@ -2,8 +2,11 @@
2
2
  * KimiAcpTranslator: translates ACP session/update notifications and
3
3
  * session/request_permission server requests into AgentEvents.
4
4
  *
5
- * Per-turn stateful: accumulates text/thinking deltas, tracks tool calls,
6
- * and produces turn_started events with operationKind.
5
+ * Extends BaseAcpTranslator (shared envelope dispatch, prompt-response
6
+ * stopReason mapping, tool_call normalization, usage occupancy). Kimi's
7
+ * divergence from opencode is the content channel: per-turn stateful —
8
+ * accumulates text/thinking deltas and emits turn_diff snapshots (NOT
9
+ * assistant deltas — see handleAgentMessageChunk), plus question elicitation.
7
10
  *
8
11
  * Wire envelope (R1, source: kimi-code packages/acp-server/src/events-map.ts,
9
12
  * verified live 2026-08-15/16 against kimi 0.36.0):
@@ -13,74 +16,28 @@
13
16
  * Event mapping (design doc §4.2):
14
17
  * agent_message_chunk → turn_diff text snapshot (content.text accumulation)
15
18
  * 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)
19
+ * tool_call / tool_call_update / usage_update / plan / control-plane noise →
20
+ * base class (BaseAcpTranslator)
25
21
  * prompt stopReason:'end_turn' → result success
26
22
  * prompt stopReason:'cancelled' → result interrupted (独立终态)
27
23
  * prompt other stopReason/error → result error
28
24
  * turn_started (self-produced) → operationKind 'turn'/'compaction'
29
- * session/request_permission → approval_requested
25
+ * session/request_permission → approval_requested(含 question 桥)
26
+ * elicitation/create → question approval event
30
27
  */
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 {
28
+ import { type RequestPermissionParams, type AgentMessageChunkEvent, type AgentThoughtChunkEvent } from '../../common/acp/protocol-types.js';
29
+ import { BaseAcpTranslator, type AcpTranslatorEvent } from '../../common/acp/base-translator.js';
30
+ export type { AcpTurnStartedEvent, AcpLiveUsage, AcpUsageEvent, AcpTranslatorEvent, } from '../../common/acp/base-translator.js';
31
+ export declare class KimiAcpTranslator extends BaseAcpTranslator {
32
+ protected readonly logTag = "kimi-acp-translator";
62
33
  /** Accumulated text per tool-call or message (keyed by a logical item id). */
63
34
  private textByItem;
64
35
  /** Accumulated thinking per item. */
65
36
  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[];
37
+ protected handleAgentMessageChunk(event: AgentMessageChunkEvent, sessionId: string): AcpTranslatorEvent[];
38
+ protected handleAgentThoughtChunk(event: AgentThoughtChunkEvent, sessionId: string): AcpTranslatorEvent[];
39
+ protected handleRequestPermission(requestId: number | string, params: RequestPermissionParams): AcpTranslatorEvent[];
40
+ protected handleOtherServerRequest(requestId: number | string, method: string, params: unknown): AcpTranslatorEvent[];
84
41
  /**
85
42
  * Kimi elicitation form:requestedSchema.properties 按顺序解析为 UserQuestion[]
86
43
  * (type:array → 多选;oneOf/anyOf const → 选项 label;title → header??question)。
@@ -89,50 +46,10 @@ export declare class KimiAcpTranslator {
89
46
  * form 会丢弃非声明选项值 → isOther=false(卡片隐藏自定义答案输入)。
90
47
  */
91
48
  private handleElicitationCreate;
92
- /**
93
- * Produce a turn_started event. Called by the runner when session/prompt
94
- * is sent (no wire notification for this — self-produced).
95
- */
96
- produceTurnStarted(sessionId: string, turnId: string): AcpTurnStartedEvent;
97
- /**
98
- * Translate a prompt response into a result event. Carries the live usage
99
- * snapshot only when a usage_update was seen (otherwise undefined, so the
100
- * bridge falls back to wire.jsonl usage.record readback).
101
- */
102
- handlePromptResponse(sessionId: string, result: {
103
- stopReason: string;
104
- }): AgentEvent;
105
- /**
106
- * Produce an error result event (for JSON-RPC errors during prompt).
107
- */
108
- produceErrorResult(sessionId: string, errorMessage: string): AgentEvent;
109
- /**
110
- * Set the operation kind (turn vs compact).
111
- */
112
- setOperationKind(kind: 'turn' | 'compact'): void;
113
- private handleAgentMessageChunk;
114
- private handleAgentThoughtChunk;
115
- private handleToolCall;
116
- private handleToolCallUpdate;
117
- private handleUsageUpdate;
118
- private handleRequestPermission;
119
49
  /**
120
50
  * request_permission 兜底桥(elicitation/create 失败或旧版 kimi):
121
51
  * 选项为 q{n}_opt_{i}(allow_once,label 直取 name)+ q{n}_skip(reject_once,
122
52
  * 跳过语义)。单题单选、无 Other(kimi 桥丢弃非声明值)。
123
53
  */
124
54
  private handlePermissionQuestion;
125
- /**
126
- * Derive the approval decision list from the server's offered options.
127
- * accept/decline/cancel are universal; acceptForSession requires an
128
- * always-class option (kimi `approve_always`, opencode `allow_always`).
129
- */
130
- private deriveAvailableDecisions;
131
- /**
132
- * ResultEvent.usage requires input_tokens/output_tokens: number, but the
133
- * ACP wire has no such split — the live snapshot is context occupancy only.
134
- * Cast is deliberate: consumers must tolerate undefined input/output (the
135
- * bridge's kimi path already falls back to wire.jsonl readback).
136
- */
137
- private liveUsageSnapshot;
138
55
  }
@@ -2,8 +2,11 @@
2
2
  * KimiAcpTranslator: translates ACP session/update notifications and
3
3
  * session/request_permission server requests into AgentEvents.
4
4
  *
5
- * Per-turn stateful: accumulates text/thinking deltas, tracks tool calls,
6
- * and produces turn_started events with operationKind.
5
+ * Extends BaseAcpTranslator (shared envelope dispatch, prompt-response
6
+ * stopReason mapping, tool_call normalization, usage occupancy). Kimi's
7
+ * divergence from opencode is the content channel: per-turn stateful —
8
+ * accumulates text/thinking deltas and emits turn_diff snapshots (NOT
9
+ * assistant deltas — see handleAgentMessageChunk), plus question elicitation.
7
10
  *
8
11
  * Wire envelope (R1, source: kimi-code packages/acp-server/src/events-map.ts,
9
12
  * verified live 2026-08-15/16 against kimi 0.36.0):
@@ -13,24 +16,20 @@
13
16
  * Event mapping (design doc §4.2):
14
17
  * agent_message_chunk → turn_diff text snapshot (content.text accumulation)
15
18
  * 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)
19
+ * tool_call / tool_call_update / usage_update / plan / control-plane noise →
20
+ * base class (BaseAcpTranslator)
25
21
  * prompt stopReason:'end_turn' → result success
26
22
  * prompt stopReason:'cancelled' → result interrupted (独立终态)
27
23
  * prompt other stopReason/error → result error
28
24
  * turn_started (self-produced) → operationKind 'turn'/'compaction'
29
- * session/request_permission → approval_requested
25
+ * session/request_permission → approval_requested(含 question 桥)
26
+ * elicitation/create → question approval event
30
27
  */
31
28
  import { makeQuestionApprovalEvent } from '../../question-common.js';
32
- import { NotificationMethod, ServerRequestMethod, SessionEventType, } from '../../common/acp/protocol-types.js';
29
+ import { ServerRequestMethod, } from '../../common/acp/protocol-types.js';
30
+ import { deriveAcpAvailableDecisions, truncateWithEllipsis, } from '../../common/acp/protocol-helpers.js';
33
31
  import { getLogger } from '../../../logger/index.js';
32
+ import { BaseAcpTranslator } from '../../common/acp/base-translator.js';
34
33
  /**
35
34
  * Fixed snapshot item ids: ACP has exactly one text stream and one thinking
36
35
  * stream per turn (no itemId on the wire), so turn_diff snapshots use these
@@ -41,153 +40,14 @@ const THINKING_ITEM_ID = 'thinking';
41
40
  // =============================================================================
42
41
  // Translator
43
42
  // =============================================================================
44
- export class KimiAcpTranslator {
43
+ export class KimiAcpTranslator extends BaseAcpTranslator {
44
+ logTag = 'kimi-acp-translator';
45
45
  /** Accumulated text per tool-call or message (keyed by a logical item id). */
46
46
  textByItem = new Map();
47
47
  /** Accumulated thinking per item. */
48
48
  thinkingByItem = new Map();
49
- /** Pending tool calls keyed by toolCallId (for matching tool_call_update). */
50
- pendingToolCalls = new Map();
51
- /** Current operation kind (turn vs compact). */
52
- operationKind = 'turn';
53
- /** Live usage snapshot from usage_update events (context occupancy only). */
54
- liveUsage = {};
55
- /** Whether any usage_update has been seen this turn (no update → no usage). */
56
- hasLiveUsage = false;
57
- /** Current turn id, set by produceTurnStarted; carried on turn_diff events. */
58
- currentTurnId = '';
59
- /**
60
- * Handle a notification from the ACP server and return translated events.
61
- */
62
- handleNotification(method, params) {
63
- if (method !== NotificationMethod.SESSION_UPDATE) {
64
- return [];
65
- }
66
- // R1: real envelope is {sessionId, update: {sessionUpdate: ...}}.
67
- const notif = params;
68
- const update = notif.update;
69
- switch (update.sessionUpdate) {
70
- case SessionEventType.AGENT_MESSAGE_CHUNK:
71
- return this.handleAgentMessageChunk(update, notif.sessionId);
72
- case SessionEventType.AGENT_THOUGHT_CHUNK:
73
- return this.handleAgentThoughtChunk(update, notif.sessionId);
74
- case SessionEventType.TOOL_CALL:
75
- return this.handleToolCall(update, notif.sessionId);
76
- case SessionEventType.TOOL_CALL_UPDATE:
77
- return this.handleToolCallUpdate(update, notif.sessionId);
78
- case SessionEventType.PLAN:
79
- getLogger().debug(`[kimi-acp-translator] discarding plan event`);
80
- return [];
81
- case SessionEventType.AVAILABLE_COMMANDS_UPDATE:
82
- case SessionEventType.SESSION_INFO_UPDATE:
83
- case SessionEventType.CURRENT_MODE_UPDATE:
84
- case SessionEventType.CONFIG_OPTION_UPDATE:
85
- // Control-plane noise (command list, mode/config echoes) — not content.
86
- return [];
87
- case SessionEventType.USAGE_UPDATE:
88
- return this.handleUsageUpdate(update);
89
- default:
90
- return [];
91
- }
92
- }
93
- /**
94
- * Handle a server request (reverse RPC from the ACP server).
95
- */
96
- handleServerRequest(id, method, params) {
97
- if (method === ServerRequestMethod.REQUEST_PERMISSION) {
98
- return this.handleRequestPermission(id, params);
99
- }
100
- if (method === ServerRequestMethod.ELICITATION_CREATE) {
101
- return this.handleElicitationCreate(id, params);
102
- }
103
- return [];
104
- }
105
- /**
106
- * Kimi elicitation form:requestedSchema.properties 按顺序解析为 UserQuestion[]
107
- * (type:array → 多选;oneOf/anyOf const → 选项 label;title → header??question)。
108
- * 题面完整文本只在 form 级 message 里合并出现(逐字段 title 可能只是短 header),
109
- * 因此 message 作为卡片概要行(view.intro)透传。
110
- * form 会丢弃非声明选项值 → isOther=false(卡片隐藏自定义答案输入)。
111
- */
112
- handleElicitationCreate(requestId, params) {
113
- const properties = params.requestedSchema?.properties ?? {};
114
- const questions = [];
115
- for (const [key, prop] of Object.entries(properties)) {
116
- const q = elicitationPropertyToQuestion(prop);
117
- if (!q) {
118
- getLogger().warn(`[kimi-acp-translator] elicitation property skipped: key=${key}`);
119
- continue;
120
- }
121
- questions.push(q);
122
- }
123
- if (questions.length === 0) {
124
- // 无法解析 → 交还 runner 自动响应(不悬挂服务端)。
125
- return [];
126
- }
127
- const message = params.message?.trim();
128
- const single = questions.length === 1;
129
- const intro = message && (!single || message !== questions[0].question) ? message : undefined;
130
- const event = makeQuestionApprovalEvent(requestId, questions, params.sessionId);
131
- if (intro)
132
- event.view.intro = intro;
133
- return [event];
134
- }
135
- /**
136
- * Produce a turn_started event. Called by the runner when session/prompt
137
- * is sent (no wire notification for this — self-produced).
138
- */
139
- produceTurnStarted(sessionId, turnId) {
140
- this.currentTurnId = turnId;
141
- return {
142
- type: 'turn_started',
143
- threadId: sessionId,
144
- turnId,
145
- operationKind: this.operationKind === 'compact' ? 'compaction' : 'turn',
146
- timestamp: new Date().toISOString(),
147
- };
148
- }
149
- /**
150
- * Translate a prompt response into a result event. Carries the live usage
151
- * snapshot only when a usage_update was seen (otherwise undefined, so the
152
- * bridge falls back to wire.jsonl usage.record readback).
153
- */
154
- handlePromptResponse(sessionId, result) {
155
- // §4.2: cancelled is independent terminal state, must NOT merge into error
156
- const subtype = result.stopReason === 'end_turn'
157
- ? 'success'
158
- : result.stopReason === 'cancelled'
159
- ? 'interrupted'
160
- : 'error';
161
- return {
162
- type: 'result',
163
- subtype,
164
- session_id: sessionId,
165
- ...(this.hasLiveUsage ? { usage: this.liveUsageSnapshot() } : {}),
166
- ...(subtype === 'error'
167
- ? { errorMessage: `Prompt ended with stopReason: ${result.stopReason}` }
168
- : {}),
169
- };
170
- }
171
- /**
172
- * Produce an error result event (for JSON-RPC errors during prompt).
173
- */
174
- produceErrorResult(sessionId, errorMessage) {
175
- return {
176
- type: 'result',
177
- subtype: 'error',
178
- session_id: sessionId,
179
- errorMessage,
180
- ...(this.hasLiveUsage ? { usage: this.liveUsageSnapshot() } : {}),
181
- };
182
- }
183
- /**
184
- * Set the operation kind (turn vs compact).
185
- */
186
- setOperationKind(kind) {
187
- this.operationKind = kind;
188
- }
189
49
  // =========================================================================
190
- // Private notification handlers
50
+ // Content channel: wire deltas → turn_diff snapshots
191
51
  // =========================================================================
192
52
  handleAgentMessageChunk(event, sessionId) {
193
53
  // Use a stable key for the current text stream. Kimi ACP doesn't
@@ -213,6 +73,10 @@ export class KimiAcpTranslator {
213
73
  threadId: sessionId,
214
74
  turnId: this.currentTurnId,
215
75
  timestamp: new Date().toISOString(),
76
+ // 卡片块锚点时间刷新为本次 diff 时间:'text' 是整轮累积流,工具间隙
77
+ // 仍会续写增量;首见时间(如 08:39)会让标题与 move-to-end 后的位置
78
+ // 看起来“乱序”。这里显式声明 diff 时间即块的最新写入时间。
79
+ refreshTimestamp: true,
216
80
  },
217
81
  ];
218
82
  }
@@ -232,80 +96,12 @@ export class KimiAcpTranslator {
232
96
  threadId: sessionId,
233
97
  turnId: this.currentTurnId,
234
98
  timestamp: new Date().toISOString(),
99
+ refreshTimestamp: true,
235
100
  },
236
101
  ];
237
102
  }
238
- handleToolCall(event, _sessionId) {
239
- // rawInput arrives as an object on the wire; older kimi versions sent a
240
- // JSON string — parse defensively in that case. The acp-server's
241
- // lazy-create tool_call (first args delta, events-map.ts
242
- // toolCallLazyCreateToSessionUpdate) carries NO rawInput field at all —
243
- // normalize to an empty object so the card renders an empty args summary
244
- // instead of crashing on undefined input (2026-08-17 live TypeError).
245
- let input = event.rawInput ?? {};
246
- if (typeof event.rawInput === 'string') {
247
- try {
248
- input = JSON.parse(event.rawInput);
249
- }
250
- catch {
251
- // Keep raw string as-is
252
- }
253
- }
254
- this.pendingToolCalls.set(event.toolCallId, {
255
- id: event.toolCallId,
256
- name: event.title,
257
- input,
258
- });
259
- return [
260
- {
261
- type: 'assistant',
262
- message: {
263
- content: [
264
- {
265
- type: 'tool_use',
266
- id: event.toolCallId,
267
- name: event.title,
268
- input,
269
- },
270
- ],
271
- },
272
- timestamp: new Date().toISOString(),
273
- },
274
- ];
275
- }
276
- handleToolCallUpdate(event, _sessionId) {
277
- // §4.2: status:'failed' → is_error:true (fixes CLI mode's is_error always false)
278
- const isError = event.status === 'failed';
279
- const content = event.rawOutput ?? '';
280
- return [
281
- {
282
- type: 'user',
283
- message: {
284
- content: [
285
- {
286
- type: 'tool_result',
287
- tool_use_id: event.toolCallId,
288
- content,
289
- is_error: isError,
290
- },
291
- ],
292
- },
293
- timestamp: new Date().toISOString(),
294
- },
295
- ];
296
- }
297
- handleUsageUpdate(event) {
298
- // R1: usage_update is {sessionUpdate:'usage_update', used, size}
299
- // (events-map.ts:505-516) — context occupancy, not cumulative tokens.
300
- this.liveUsage = {
301
- total_tokens: event.used,
302
- context_limit: event.size,
303
- };
304
- this.hasLiveUsage = true;
305
- return [];
306
- }
307
103
  // =========================================================================
308
- // Private approval handlers
104
+ // Approval + question elicitation
309
105
  // =========================================================================
310
106
  handleRequestPermission(requestId, params) {
311
107
  // §5.4 升级:question elicitation 识别(isQuestion 标记 / AskUserQuestion
@@ -328,10 +124,12 @@ export class KimiAcpTranslator {
328
124
  requestId,
329
125
  kind: 'command',
330
126
  command: params.toolCall?.title ?? undefined,
331
- reason: params.toolCall?.rawInput ? truncate(params.toolCall.rawInput, 200) : undefined,
127
+ reason: params.toolCall?.rawInput
128
+ ? truncateWithEllipsis(params.toolCall.rawInput, 200)
129
+ : undefined,
332
130
  // §P4: 从服务端 options kind 派生——带 approve_always(或 allow_always)
333
131
  // 才提供「本会话总是允许」(acceptForSession);否则与旧行为一致。
334
- availableDecisions: this.deriveAvailableDecisions(params),
132
+ availableDecisions: deriveAcpAvailableDecisions(params.options ?? []),
335
133
  };
336
134
  return [
337
135
  {
@@ -346,6 +144,42 @@ export class KimiAcpTranslator {
346
144
  },
347
145
  ];
348
146
  }
147
+ handleOtherServerRequest(requestId, method, params) {
148
+ if (method !== ServerRequestMethod.ELICITATION_CREATE) {
149
+ return [];
150
+ }
151
+ return this.handleElicitationCreate(requestId, params);
152
+ }
153
+ /**
154
+ * Kimi elicitation form:requestedSchema.properties 按顺序解析为 UserQuestion[]
155
+ * (type:array → 多选;oneOf/anyOf const → 选项 label;title → header??question)。
156
+ * 题面完整文本只在 form 级 message 里合并出现(逐字段 title 可能只是短 header),
157
+ * 因此 message 作为卡片概要行(view.intro)透传。
158
+ * form 会丢弃非声明选项值 → isOther=false(卡片隐藏自定义答案输入)。
159
+ */
160
+ handleElicitationCreate(requestId, params) {
161
+ const properties = params.requestedSchema?.properties ?? {};
162
+ const questions = [];
163
+ for (const [key, prop] of Object.entries(properties)) {
164
+ const q = elicitationPropertyToQuestion(prop);
165
+ if (!q) {
166
+ getLogger().warn(`[kimi-acp-translator] elicitation property skipped: key=${key}`);
167
+ continue;
168
+ }
169
+ questions.push(q);
170
+ }
171
+ if (questions.length === 0) {
172
+ // 无法解析 → 交还 runner 自动响应(不悬挂服务端)。
173
+ return [];
174
+ }
175
+ const message = params.message?.trim();
176
+ const single = questions.length === 1;
177
+ const intro = message && (!single || message !== questions[0].question) ? message : undefined;
178
+ const event = makeQuestionApprovalEvent(requestId, questions, params.sessionId);
179
+ if (intro)
180
+ event.view.intro = intro;
181
+ return [event];
182
+ }
349
183
  /**
350
184
  * request_permission 兜底桥(elicitation/create 失败或旧版 kimi):
351
185
  * 选项为 q{n}_opt_{i}(allow_once,label 直取 name)+ q{n}_skip(reject_once,
@@ -367,38 +201,10 @@ export class KimiAcpTranslator {
367
201
  ];
368
202
  return [makeQuestionApprovalEvent(requestId, questions, params.sessionId)];
369
203
  }
370
- /**
371
- * Derive the approval decision list from the server's offered options.
372
- * accept/decline/cancel are universal; acceptForSession requires an
373
- * always-class option (kimi `approve_always`, opencode `allow_always`).
374
- */
375
- deriveAvailableDecisions(params) {
376
- const decisions = ['accept', 'decline', 'cancel'];
377
- const hasAlwaysOption = (params.options ?? []).some((opt) => opt.kind === 'approve_always' || opt.kind === 'allow_always');
378
- if (hasAlwaysOption) {
379
- decisions.push('acceptForSession');
380
- }
381
- return decisions;
382
- }
383
- // =========================================================================
384
- // Private helpers
385
- // =========================================================================
386
- /**
387
- * ResultEvent.usage requires input_tokens/output_tokens: number, but the
388
- * ACP wire has no such split — the live snapshot is context occupancy only.
389
- * Cast is deliberate: consumers must tolerate undefined input/output (the
390
- * bridge's kimi path already falls back to wire.jsonl readback).
391
- */
392
- liveUsageSnapshot() {
393
- return { ...this.liveUsage };
394
- }
395
204
  }
396
205
  // =============================================================================
397
206
  // Helpers
398
207
  // =============================================================================
399
- function truncate(text, maxLen) {
400
- return text.length <= maxLen ? text : text.slice(0, maxLen) + '…';
401
- }
402
208
  /** elicitation schema property → UserQuestion(无法解析返回 null)。 */
403
209
  function elicitationPropertyToQuestion(prop) {
404
210
  const title = prop.title?.trim();
@@ -7,10 +7,8 @@
7
7
  * → synthetic init → session/prompt → consume session/update notifications
8
8
  * until prompt settles → result event.
9
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:
10
+ * Turn/connection/compact/approval orchestration lives in BaseAcpRunner;
11
+ * opencode-specific parts:
14
12
  * - session/set_mode applies the configured mode (build/plan; default 'build'
15
13
  * keeps manual approvals flowing); opencode sends no set_mode notification,
16
14
  * so the client refreshes the local mode view itself
@@ -18,11 +16,15 @@
18
16
  * service.ts:555-571 summarize completes before the prompt response)
19
17
  * - text/thinking go to the assistant incremental channel (opencode wire
20
18
  * deltas), not turn_diff snapshots
19
+ * - OpencodeLogErrorMonitor tails opencode's own log for LLM stream errors
20
+ * the ACP protocol never forwards
21
21
  */
22
- import type { AgentKind, AgentSessionReader, AgentEvent, AgentStatusInfo, SpawnOptions } from '../../types.js';
22
+ import type { AgentKind, AgentSessionReader, AgentStatusInfo } from '../../types.js';
23
23
  import { JsonRpcClient } from '../../common/jsonrpc/client.js';
24
- import { type OpencodeAcpTranslatorEvent } from './translator.js';
25
- import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
24
+ import { OpencodeAcpTranslator } from './translator.js';
25
+ import { type SessionNewResult, type SessionResumeResult, type SessionPromptResult } from '../../common/acp/protocol-types.js';
26
+ import { type AcpPendingApproval } from '../../common/acp/protocol-helpers.js';
27
+ import { BaseAcpRunner } from '../../common/acp/base-acp-runner.js';
26
28
  export interface OpencodeAcpRunnerOptions {
27
29
  kind: AgentKind;
28
30
  sessionReader: AgentSessionReader;
@@ -48,11 +50,7 @@ export interface OpencodeAcpRunnerOptions {
48
50
  /** Tail poll interval for the opencode log error monitor. Defaults to 2000ms. */
49
51
  errorMonitorPollIntervalMs?: number;
50
52
  }
51
- export declare class OpencodeAcpRunner extends ConnectionBasedRunner<JsonRpcClient, OpencodeAcpTranslatorEvent> {
52
- private connectionManager;
53
- private currentTranslator;
54
- private activeSessionId;
55
- private model?;
53
+ export declare class OpencodeAcpRunner extends BaseAcpRunner<OpencodeAcpTranslator> {
56
54
  /** Configured session mode applied via session/set_mode (§P5). */
57
55
  private configuredMode;
58
56
  /** Current session mode id, parsed from session/new|resume configOptions. */
@@ -62,22 +60,12 @@ export declare class OpencodeAcpRunner extends ConnectionBasedRunner<JsonRpcClie
62
60
  /** Tails opencode's own log to surface LLM stream errors the ACP protocol
63
61
  * never forwards (quota/limit errors back off for ~1h → turn would hang). */
64
62
  private readonly errorMonitor;
65
- /** Pending approval requests: requestId → kind + view + options. */
66
- private pendingApprovals;
67
- /** Tracker for the in-flight prompt request — needed for cancellation. */
68
- private promptSettled;
69
- /** Whether the prompt request has been fired (distinguishes "stop before
70
- * prompt sent" from "stop while prompt in-flight" — the latter sends
71
- * session/cancel immediately). */
72
- private promptSent;
73
63
  constructor(opts: OpencodeAcpRunnerOptions);
74
64
  protected get logTag(): string;
75
65
  protected get turnTimeoutErrorMessage(): string;
76
66
  protected get turnInterruptedErrorMessage(): string;
77
- protected currentSessionId(): string | null;
67
+ protected get connectionClosedMessage(): string;
78
68
  protected shouldDeferStop(): boolean;
79
- protected cancelCurrentTurn(): Promise<void>;
80
- protected clearTurnState(): void;
81
69
  /**
82
70
  * Start watching opencode's own log for LLM stream errors of the active
83
71
  * session. opencode ACP does not surface retry/error state to the client
@@ -88,23 +76,14 @@ export declare class OpencodeAcpRunner extends ConnectionBasedRunner<JsonRpcClie
88
76
  * until the idle watchdog.
89
77
  */
90
78
  private startErrorMonitor;
91
- protected releaseConnection(cwd: string): Promise<void>;
92
- protected notifyIdle(cwd: string): void;
93
- protected disposeConnections(): Promise<void>;
79
+ protected createTranslator(): OpencodeAcpTranslator;
80
+ protected applyTurnSettings(client: JsonRpcClient, sessionId: string): Promise<void>;
94
81
  /**
95
- * Run a compact operation on the current session.
96
- *
97
- * opencode special-cases the `/compact` text prompt server-side
98
- * (service.ts:555-571): it runs session.summarize synchronously and the
99
- * prompt response settles only after compaction completes — no background
100
- * record polling needed (unlike kimi).
101
- */
102
- runCompact(_message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
103
- /**
104
- * Respond to an approval server request. `response` is the bridge
105
- * ApprovalAction (`{ action: 'accept' | 'decline' | 'cancel' }`).
82
+ * opencode /compact is synchronous server-side — standard prompt-settle
83
+ * translation (no wire.jsonl polling, unlike kimi).
106
84
  */
107
- respondApproval(requestId: number | string, response: unknown): Promise<void>;
85
+ protected onCompactPromptFired(promptPromise: Promise<SessionPromptResult>, translator: OpencodeAcpTranslator, sessionId: string, _opts: unknown, _baselineTotalCount: number): void;
86
+ protected buildApprovalOutcome(action: string, pending: AcpPendingApproval, _response: unknown): unknown;
108
87
  getStatusInfo(): AgentStatusInfo;
109
88
  /**
110
89
  * Hot-apply a mode change to the live ACP session (§P5).
@@ -121,18 +100,15 @@ export declare class OpencodeAcpRunner extends ConnectionBasedRunner<JsonRpcClie
121
100
  updateApprovalMode(settings: {
122
101
  mode?: 'build' | 'plan';
123
102
  }): Promise<void>;
124
- /**
125
- * Acquire the connection and set up the turn:
126
- * session/new (new session) or session/resume (existing sessionId)
127
- * → session/prompt.
128
- */
129
- protected setupTurn(message: string, opts: SpawnOptions): Promise<void>;
130
103
  /**
131
104
  * Track the current session mode and model from a session/new|resume
132
105
  * result's configOptions (opencode buildModeSelectOption/buildModelSelectOption:
133
106
  * {id, category, currentValue}).
134
107
  */
135
108
  private trackConfigOptions;
136
- private handleNotification;
137
- private handleServerRequest;
109
+ protected onTurnSessionReady(sessionId: string): void;
110
+ protected onSessionEstablished(result: SessionResumeResult | SessionNewResult): void;
111
+ protected onClearTurnState(): void;
112
+ protected handleNotification(method: string, params: unknown): void;
113
+ protected handleServerRequest(id: number | string, method: string, params: unknown): void;
138
114
  }