lark-remote 0.4.2 → 0.4.4
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 +33 -0
- package/dist/bridge/approval-coordinator.d.ts +2 -0
- package/dist/bridge/approval-coordinator.js +16 -7
- package/dist/bridge/index.d.ts +22 -0
- package/dist/bridge/index.js +138 -56
- package/dist/card/card-budget.js +39 -26
- package/dist/card/run-renderer.js +9 -2
- package/dist/card/run-state.js +81 -31
- package/dist/card/text-truncate.d.ts +9 -10
- package/dist/card/text-truncate.js +48 -43
- package/dist/card/tool-render.js +6 -2
- package/dist/cli.js +17 -0
- package/dist/config/codex-config.d.ts +3 -0
- package/dist/config/codex-config.js +4 -1
- package/dist/config/dir.js +6 -6
- package/dist/config/index.d.ts +7 -5
- package/dist/config/index.js +52 -13
- package/dist/config/kimi-config.d.ts +4 -1
- package/dist/config/kimi-config.js +5 -2
- package/dist/config/pi-config.d.ts +2 -7
- package/dist/config/pi-config.js +6 -6
- package/dist/config/wizard.js +8 -4
- package/dist/connector/index.d.ts +19 -0
- package/dist/connector/index.js +134 -23
- package/dist/inbound/placeholder.d.ts +3 -2
- package/dist/inbound/placeholder.js +17 -7
- package/dist/index.js +28 -6
- package/dist/instance-lock.d.ts +31 -15
- package/dist/instance-lock.js +64 -62
- package/dist/platform/agent-stopper.d.ts +29 -7
- package/dist/platform/agent-stopper.js +40 -8
- package/dist/platform/identity.d.ts +59 -3
- package/dist/platform/identity.js +105 -30
- package/dist/platform/path.d.ts +0 -7
- package/dist/platform/path.js +1 -51
- package/dist/platform/shell.d.ts +14 -0
- package/dist/platform/shell.js +35 -2
- package/dist/platform/sleep-blocker.d.ts +6 -0
- package/dist/platform/sleep-blocker.js +31 -5
- package/dist/platform/spawn.d.ts +1 -1
- package/dist/platform/spawn.js +1 -1
- package/dist/platform/terminator-posix.d.ts +1 -29
- package/dist/platform/terminator-posix.js +32 -30
- package/dist/platform/terminator-win32.js +4 -2
- package/dist/platform/terminator.d.ts +2 -2
- package/dist/platform/terminator.js +17 -2
- package/dist/platform/types.d.ts +3 -0
- package/dist/router/card-action-feedback.d.ts +17 -0
- package/dist/router/card-action-feedback.js +31 -0
- package/dist/router/config/claude.js +8 -5
- package/dist/router/index.d.ts +2 -0
- package/dist/router/index.js +35 -15
- package/dist/runner/claude/runner.js +1 -1
- package/dist/runner/claude/session.d.ts +2 -6
- package/dist/runner/claude/session.js +7 -8
- package/dist/runner/codex/app-server/protocol-types.d.ts +4 -0
- package/dist/runner/codex/app-server/protocol-types.js +4 -0
- package/dist/runner/codex/app-server/runner.js +3 -0
- package/dist/runner/common/acp/base-acp-runner.js +7 -0
- package/dist/runner/common/acp/base-translator.d.ts +43 -0
- package/dist/runner/common/acp/base-translator.js +83 -18
- package/dist/runner/common/connection-based-runner.d.ts +18 -0
- package/dist/runner/common/connection-based-runner.js +31 -0
- package/dist/runner/common/jsonrpc/client.js +6 -1
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +20 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +27 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +29 -1
- package/dist/runner/common/jsonrpc/transport.js +79 -10
- package/dist/runner/common/spawning-runner.d.ts +49 -19
- package/dist/runner/common/spawning-runner.js +94 -54
- package/dist/runner/dsh/client.d.ts +1 -3
- package/dist/runner/dsh/client.js +0 -4
- package/dist/runner/dsh/types.d.ts +0 -36
- package/dist/runner/kimi/acp/runner.d.ts +18 -1
- package/dist/runner/kimi/acp/runner.js +100 -42
- package/dist/runner/kimi/acp/translator.d.ts +0 -1
- package/dist/runner/opencode/acp/translator.d.ts +22 -2
- package/dist/runner/opencode/acp/translator.js +60 -0
- package/dist/runner/pi/rpc/protocol-types.d.ts +4 -0
- package/dist/runner/pi/rpc/runner.js +3 -0
- package/dist/runner/resolve-agent-choices.js +0 -5
- package/package.json +2 -2
- package/dist/runner/common/process-stopper.d.ts +0 -7
- package/dist/runner/common/process-stopper.js +0 -8
- package/dist/runner/dsh/fake-dsh-server.d.ts +0 -66
- package/dist/runner/dsh/fake-dsh-server.js +0 -162
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* 生命周期:一个 workspace 一个长驻进程(lifetime='workspace')。每次
|
|
26
26
|
* run(message) = 写一条 user 消息 + 消费 stdout 事件直到本 turn 的 result;
|
|
27
27
|
* 进程在 turn 之间保持存活(stdin 不关闭),/stop / /new / /cd / 看门狗超时
|
|
28
|
-
* 时经
|
|
28
|
+
* 时经 Terminator 组杀,下条消息按 SessionStore 的 sessionId --resume。
|
|
29
29
|
*/
|
|
30
30
|
import { readFile } from 'node:fs/promises';
|
|
31
31
|
import { silentlyUnlink } from '../../common/fs.js';
|
|
@@ -43,7 +43,7 @@ const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
|
43
43
|
const COMPACTION_RESULT_SUBTYPES = new Set(['compact', 'compaction']);
|
|
44
44
|
/**
|
|
45
45
|
* ClaudeSession extends SpawningRunner 复用其 spawn/pid 文件/killOrphan/
|
|
46
|
-
*
|
|
46
|
+
* Terminator/SpawnHeartbeat/退出分发器机制(P1-1/P1-10/P1-11 契约保持),
|
|
47
47
|
* 但 run() 覆盖为「长驻 + 按 turn 消费」:进程跨 turn 存活,turn 结束以
|
|
48
48
|
* stream-json result 事件(非 compact)为界,而不是进程退出。
|
|
49
49
|
*/
|
|
@@ -97,6 +97,8 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
97
97
|
spawnHeartbeatMs: opts.spawnHeartbeatMs,
|
|
98
98
|
pidFilePrefix: 'claude',
|
|
99
99
|
logTag: 'claude-runner',
|
|
100
|
+
// win32 查协议停止通道用的 agent key(claude 的 channel = 控制通道/关 stdin)
|
|
101
|
+
agent: 'claude',
|
|
100
102
|
});
|
|
101
103
|
this.binary = 'claude';
|
|
102
104
|
this.permissionMode = opts.permissionMode ?? 'bypassPermissions';
|
|
@@ -166,12 +168,6 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
166
168
|
this.armIdleTimer();
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
|
-
/**
|
|
170
|
-
* 写一条用户消息到 stdin(--input-format stream-json 的 user 事件)。
|
|
171
|
-
*/
|
|
172
|
-
async sendUserMessage(message) {
|
|
173
|
-
await this.writeUserMessage(message);
|
|
174
|
-
}
|
|
175
171
|
/**
|
|
176
172
|
* 回写审批响应(允许/拒绝)。
|
|
177
173
|
*/
|
|
@@ -373,6 +369,9 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
373
369
|
if (generation === this.processGeneration) {
|
|
374
370
|
this.streamEnded = true;
|
|
375
371
|
this.currentProcess = null;
|
|
372
|
+
// 进程自行结束(崩溃/上游退出):注销协议停止通道,别把死 pid 的
|
|
373
|
+
// 条目留在注册表里等 pid 复用。
|
|
374
|
+
this.unregisterStopper();
|
|
376
375
|
silentlyUnlink(this.pidFilePath);
|
|
377
376
|
this.wakeWaiters();
|
|
378
377
|
}
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* field names use the wire format exactly: camelCase paths for methods
|
|
7
7
|
* (`item/agentMessage/delta`) and snake_case only where the server requires it
|
|
8
8
|
* (e.g. permission profile fields).
|
|
9
|
+
*
|
|
10
|
+
* 本文件是上游 schema 的完整镜像,不是"用到什么抄什么":少数导出类型当前没有
|
|
11
|
+
* consumer,是有意保留(接新通知/新方法时不必回头重抄 schema)。静态扫描再报
|
|
12
|
+
* "零引用导出类型"指的就是这一批,别据此删除。
|
|
9
13
|
*/
|
|
10
14
|
export type { JsonRpcRequest, JsonRpcNotification, JsonRpcSuccessResponse, JsonRpcErrorResponse, } from '../../common/jsonrpc/types.js';
|
|
11
15
|
export interface ClientInfo {
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* field names use the wire format exactly: camelCase paths for methods
|
|
7
7
|
* (`item/agentMessage/delta`) and snake_case only where the server requires it
|
|
8
8
|
* (e.g. permission profile fields).
|
|
9
|
+
*
|
|
10
|
+
* 本文件是上游 schema 的完整镜像,不是"用到什么抄什么":少数导出类型当前没有
|
|
11
|
+
* consumer,是有意保留(接新通知/新方法时不必回头重抄 schema)。静态扫描再报
|
|
12
|
+
* "零引用导出类型"指的就是这一批,别据此删除。
|
|
9
13
|
*/
|
|
10
14
|
// =============================================================================
|
|
11
15
|
// Notifications (server → client)
|
|
@@ -136,6 +136,9 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
136
136
|
},
|
|
137
137
|
};
|
|
138
138
|
this.connectionManager = new ConnectionManager(managerOpts);
|
|
139
|
+
// 协议停止通道(design §3.3):codex 的 cancel 是 `turn/interrupt` 请求,已由
|
|
140
|
+
// cancelCurrentTurn 实现;登记后 win32 优雅停止才有通道可用(否则一路树杀)。
|
|
141
|
+
this.connectionManager.stopper = ({ client }) => this.buildCooperativeStop(client);
|
|
139
142
|
}
|
|
140
143
|
get logTag() {
|
|
141
144
|
return 'codex-app-server-runner';
|
|
@@ -15,6 +15,10 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
|
|
|
15
15
|
constructor(opts, connectionManager) {
|
|
16
16
|
super(opts);
|
|
17
17
|
this.connectionManager = connectionManager;
|
|
18
|
+
// 协议停止通道(design §3.3):ACP 的 cancel 就是 `session/cancel`,已由
|
|
19
|
+
// cancelCurrentTurn 实现。这里只做登记接线——manager 建在 super() 实参里,
|
|
20
|
+
// 那时 this 不可用,所以只能等 super() 返回后再赋值。
|
|
21
|
+
connectionManager.stopper = ({ client }) => this.buildCooperativeStop(client);
|
|
18
22
|
}
|
|
19
23
|
// =========================================================================
|
|
20
24
|
// Shared plumbing
|
|
@@ -40,6 +44,9 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
|
|
|
40
44
|
this.promptSettled = false;
|
|
41
45
|
this.promptSent = false;
|
|
42
46
|
this.activeSessionId = null;
|
|
47
|
+
// 未答的审批随轮次一起作废:连接是按工作区长驻的,请求 id 会从 1 重新
|
|
48
|
+
// 计数,留着旧条目等于让下一轮的迟到的卡片点击回信到一条无关请求上。
|
|
49
|
+
this.pendingApprovals.clear();
|
|
43
50
|
}
|
|
44
51
|
async releaseConnection(cwd) {
|
|
45
52
|
await this.connectionManager.release(cwd);
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* handleRequestPermission — approval rendering (abstract)
|
|
16
16
|
* handleOtherServerRequest — extra reverse RPC (kimi: elicitation)
|
|
17
17
|
* extractToolResultContent — tool_result content (default: rawOutput)
|
|
18
|
+
* extractToolCallArgsText — streaming-args source (opencode: none)
|
|
19
|
+
* buildToolUseUpdate — tool_use patch from tool_call_update
|
|
18
20
|
* onUsageSample — extra usage bookkeeping (opencode: cost)
|
|
19
21
|
*/
|
|
20
22
|
import type { AgentEvent, ApprovalRequestedEvent, ResultEvent } from '../../types.js';
|
|
@@ -69,6 +71,13 @@ export declare abstract class BaseAcpTranslator {
|
|
|
69
71
|
* approval handler correlate by toolCallId and recover the real command.
|
|
70
72
|
*/
|
|
71
73
|
private toolCallArgsByCallId;
|
|
74
|
+
/**
|
|
75
|
+
* tool_use name recorded at tool_call time (kind mapping wins, else title).
|
|
76
|
+
* A later tool_call_update may carry only a title (opencode's completed
|
|
77
|
+
* update has neither kind nor rawInput); pairing it with the recorded name
|
|
78
|
+
* lets a tool_use patch render the original title as a subtitle.
|
|
79
|
+
*/
|
|
80
|
+
private toolCallNameByCallId;
|
|
72
81
|
/**
|
|
73
82
|
* Handle a notification from the ACP server and return translated events.
|
|
74
83
|
* Wire envelope: {sessionId, update: {sessionUpdate: '<kind>', ...}} — the
|
|
@@ -132,8 +141,42 @@ export declare abstract class BaseAcpTranslator {
|
|
|
132
141
|
* command (see toolCallArgsByCallId).
|
|
133
142
|
*/
|
|
134
143
|
protected getTrackedToolCallArgs(toolCallId: string): string | undefined;
|
|
144
|
+
/** tool_use name recorded at tool_call time (kind mapping wins, else title). */
|
|
145
|
+
protected getToolCallName(toolCallId: string): string | undefined;
|
|
146
|
+
/**
|
|
147
|
+
* kind → canonical tool name (信息保真 C3.1). Shared by the initial tool_call
|
|
148
|
+
* handler and the tool_call_update patch builder so both agree on the name.
|
|
149
|
+
*/
|
|
150
|
+
protected toolNameFromKind(kind: string | undefined): string | undefined;
|
|
151
|
+
/**
|
|
152
|
+
* Streaming-args text carried by a tool_call/_update notification.
|
|
153
|
+
*
|
|
154
|
+
* Default: the notification's content text — kimi's events-map.ts encodes the
|
|
155
|
+
* accumulated args there (toolCallDeltaToSessionUpdate: content.text =
|
|
156
|
+
* accumulator.args), and the approval handler relies on it.
|
|
157
|
+
*
|
|
158
|
+
* opencode overrides this to return undefined: its tool_call/_update content
|
|
159
|
+
* is the shell OUTPUT snapshot (decompiled acp service `WP`), never args
|
|
160
|
+
* (args ride rawInput in `XC`/`BP`). Treating that output as args would
|
|
161
|
+
* corrupt approval command recovery — the hazard this hook exists to close.
|
|
162
|
+
*/
|
|
163
|
+
protected extractToolCallArgsText(event: ToolCallEvent | ToolCallUpdateEvent): string | undefined;
|
|
164
|
+
/**
|
|
165
|
+
* tool_call_update → optional tool_use patch event. When an update carries
|
|
166
|
+
* richer identity than the initial tool_call did (opencode emits the
|
|
167
|
+
* tool_call while args are still streaming, then delivers the real title /
|
|
168
|
+
* rawInput in later updates), the returned `assistant`/`tool_use` block is
|
|
169
|
+
* merged into the existing tool block by the card reducer (matched by id)
|
|
170
|
+
* instead of creating a duplicate panel.
|
|
171
|
+
*
|
|
172
|
+
* Default: null (kimi self-produces its Bash tool_use from terminal/create
|
|
173
|
+
* with the real command, so it needs no patch). opencode overrides.
|
|
174
|
+
*/
|
|
175
|
+
protected buildToolUseUpdate(_event: ToolCallUpdateEvent): AgentEvent | null;
|
|
135
176
|
/** Record latest args text, bounded so a long session cannot grow it forever. */
|
|
136
177
|
private trackToolCallArgs;
|
|
178
|
+
/** Record tool_use name, bounded to keep a long session from growing it. */
|
|
179
|
+
private trackToolCallName;
|
|
137
180
|
/**
|
|
138
181
|
* tool_result content extraction. Default: rawOutput passthrough (kimi
|
|
139
182
|
* semantics). opencode overrides — its rawOutput is an OBJECT
|
|
@@ -65,6 +65,13 @@ export class BaseAcpTranslator {
|
|
|
65
65
|
* approval handler correlate by toolCallId and recover the real command.
|
|
66
66
|
*/
|
|
67
67
|
toolCallArgsByCallId = new Map();
|
|
68
|
+
/**
|
|
69
|
+
* tool_use name recorded at tool_call time (kind mapping wins, else title).
|
|
70
|
+
* A later tool_call_update may carry only a title (opencode's completed
|
|
71
|
+
* update has neither kind nor rawInput); pairing it with the recorded name
|
|
72
|
+
* lets a tool_use patch render the original title as a subtitle.
|
|
73
|
+
*/
|
|
74
|
+
toolCallNameByCallId = new Map();
|
|
68
75
|
/**
|
|
69
76
|
* Handle a notification from the ACP server and return translated events.
|
|
70
77
|
* Wire envelope: {sessionId, update: {sessionUpdate: '<kind>', ...}} — the
|
|
@@ -231,15 +238,16 @@ export class BaseAcpTranslator {
|
|
|
231
238
|
// Keep raw string as-is
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
|
-
const argsText =
|
|
241
|
+
const argsText = this.extractToolCallArgsText(event);
|
|
235
242
|
if (argsText !== undefined) {
|
|
236
243
|
this.trackToolCallArgs(event.toolCallId, argsText);
|
|
237
244
|
}
|
|
238
245
|
else if (event.rawInput !== undefined && event.rawInput !== null) {
|
|
239
246
|
this.trackToolCallArgs(event.toolCallId, stringifyUnknown(event.rawInput));
|
|
240
247
|
}
|
|
241
|
-
const mapped =
|
|
248
|
+
const mapped = this.toolNameFromKind(event.kind);
|
|
242
249
|
const name = mapped ?? event.title;
|
|
250
|
+
this.trackToolCallName(event.toolCallId, name);
|
|
243
251
|
const summary = mapped && event.title && event.title !== name ? event.title : undefined;
|
|
244
252
|
return [
|
|
245
253
|
{
|
|
@@ -265,6 +273,11 @@ export class BaseAcpTranslator {
|
|
|
265
273
|
handleToolCallUpdate(event, _sessionId) {
|
|
266
274
|
const isError = event.status === 'failed';
|
|
267
275
|
const content = this.extractToolResultContent(event);
|
|
276
|
+
// Patch the existing tool block with identity that was not available at the
|
|
277
|
+
// initial tool_call (opencode streams args after emitting tool_call). The
|
|
278
|
+
// reducer merges by id, so this never duplicates the panel. Built before
|
|
279
|
+
// the terminal cleanup below so it can still resolve the recorded name.
|
|
280
|
+
const useUpdate = this.buildToolUseUpdate(event);
|
|
268
281
|
// Terminal updates end the args-streaming phase — drop the tracked entry.
|
|
269
282
|
// In-progress delta text is cumulative (events-map.ts
|
|
270
283
|
// toolCallDeltaToSessionUpdate: content.text = accumulator.args), so the
|
|
@@ -273,7 +286,7 @@ export class BaseAcpTranslator {
|
|
|
273
286
|
this.toolCallArgsByCallId.delete(event.toolCallId);
|
|
274
287
|
}
|
|
275
288
|
else {
|
|
276
|
-
const argsText =
|
|
289
|
+
const argsText = this.extractToolCallArgsText(event);
|
|
277
290
|
if (argsText !== undefined) {
|
|
278
291
|
this.trackToolCallArgs(event.toolCallId, argsText);
|
|
279
292
|
}
|
|
@@ -281,22 +294,24 @@ export class BaseAcpTranslator {
|
|
|
281
294
|
this.trackToolCallArgs(event.toolCallId, stringifyUnknown(event.rawInput));
|
|
282
295
|
}
|
|
283
296
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
297
|
+
const events = [];
|
|
298
|
+
if (useUpdate)
|
|
299
|
+
events.push(useUpdate);
|
|
300
|
+
events.push({
|
|
301
|
+
type: 'user',
|
|
302
|
+
message: {
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: 'tool_result',
|
|
306
|
+
tool_use_id: event.toolCallId,
|
|
307
|
+
content,
|
|
308
|
+
is_error: isError,
|
|
309
|
+
},
|
|
310
|
+
],
|
|
298
311
|
},
|
|
299
|
-
|
|
312
|
+
timestamp: new Date().toISOString(),
|
|
313
|
+
});
|
|
314
|
+
return events;
|
|
300
315
|
}
|
|
301
316
|
/**
|
|
302
317
|
* Latest tracked streaming-args text for a tool call (undefined when never
|
|
@@ -307,6 +322,46 @@ export class BaseAcpTranslator {
|
|
|
307
322
|
getTrackedToolCallArgs(toolCallId) {
|
|
308
323
|
return this.toolCallArgsByCallId.get(toolCallId);
|
|
309
324
|
}
|
|
325
|
+
/** tool_use name recorded at tool_call time (kind mapping wins, else title). */
|
|
326
|
+
getToolCallName(toolCallId) {
|
|
327
|
+
return this.toolCallNameByCallId.get(toolCallId);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* kind → canonical tool name (信息保真 C3.1). Shared by the initial tool_call
|
|
331
|
+
* handler and the tool_call_update patch builder so both agree on the name.
|
|
332
|
+
*/
|
|
333
|
+
toolNameFromKind(kind) {
|
|
334
|
+
return kind ? KIND_TO_TOOL[kind] : undefined;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Streaming-args text carried by a tool_call/_update notification.
|
|
338
|
+
*
|
|
339
|
+
* Default: the notification's content text — kimi's events-map.ts encodes the
|
|
340
|
+
* accumulated args there (toolCallDeltaToSessionUpdate: content.text =
|
|
341
|
+
* accumulator.args), and the approval handler relies on it.
|
|
342
|
+
*
|
|
343
|
+
* opencode overrides this to return undefined: its tool_call/_update content
|
|
344
|
+
* is the shell OUTPUT snapshot (decompiled acp service `WP`), never args
|
|
345
|
+
* (args ride rawInput in `XC`/`BP`). Treating that output as args would
|
|
346
|
+
* corrupt approval command recovery — the hazard this hook exists to close.
|
|
347
|
+
*/
|
|
348
|
+
extractToolCallArgsText(event) {
|
|
349
|
+
return extractToolCallContentText(event.content);
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* tool_call_update → optional tool_use patch event. When an update carries
|
|
353
|
+
* richer identity than the initial tool_call did (opencode emits the
|
|
354
|
+
* tool_call while args are still streaming, then delivers the real title /
|
|
355
|
+
* rawInput in later updates), the returned `assistant`/`tool_use` block is
|
|
356
|
+
* merged into the existing tool block by the card reducer (matched by id)
|
|
357
|
+
* instead of creating a duplicate panel.
|
|
358
|
+
*
|
|
359
|
+
* Default: null (kimi self-produces its Bash tool_use from terminal/create
|
|
360
|
+
* with the real command, so it needs no patch). opencode overrides.
|
|
361
|
+
*/
|
|
362
|
+
buildToolUseUpdate(_event) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
310
365
|
/** Record latest args text, bounded so a long session cannot grow it forever. */
|
|
311
366
|
trackToolCallArgs(toolCallId, argsText) {
|
|
312
367
|
this.toolCallArgsByCallId.delete(toolCallId);
|
|
@@ -317,6 +372,16 @@ export class BaseAcpTranslator {
|
|
|
317
372
|
this.toolCallArgsByCallId.delete(oldest);
|
|
318
373
|
}
|
|
319
374
|
}
|
|
375
|
+
/** Record tool_use name, bounded to keep a long session from growing it. */
|
|
376
|
+
trackToolCallName(toolCallId, name) {
|
|
377
|
+
this.toolCallNameByCallId.delete(toolCallId);
|
|
378
|
+
this.toolCallNameByCallId.set(toolCallId, name);
|
|
379
|
+
if (this.toolCallNameByCallId.size > 100) {
|
|
380
|
+
const oldest = this.toolCallNameByCallId.keys().next().value;
|
|
381
|
+
if (oldest !== undefined)
|
|
382
|
+
this.toolCallNameByCallId.delete(oldest);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
320
385
|
/**
|
|
321
386
|
* tool_result content extraction. Default: rawOutput passthrough (kimi
|
|
322
387
|
* semantics). opencode overrides — its rawOutput is an OBJECT
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* updateApprovalMode, getStatusInfo) and provide the small abstract hooks below.
|
|
16
16
|
*/
|
|
17
17
|
import type { AgentKind, AgentSessionReader, AgentEvent, AgentRunner, AgentStatusInfo, SpawnOptions } from '../types.js';
|
|
18
|
+
import type { AgentStopper } from '../../platform/agent-stopper.js';
|
|
18
19
|
export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent> implements AgentRunner {
|
|
19
20
|
readonly kind: AgentKind;
|
|
20
21
|
readonly sessionReader: AgentSessionReader;
|
|
@@ -49,6 +50,23 @@ export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent
|
|
|
49
50
|
protected abstract cancelCurrentTurn(): Promise<void>;
|
|
50
51
|
/** Set up the turn: acquire + thread|session new/resume + prompt. */
|
|
51
52
|
protected abstract setupTurn(message: string, opts: SpawnOptions): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* 本连接的协议停止通道(design §3.3)。
|
|
55
|
+
*
|
|
56
|
+
* win32 上「优雅停止」没有可拦截的跨进程 SIGTERM,只能经 agent 自有协议通道
|
|
57
|
+
* 请求对方收摊。子类把这个方法的结果交给 ConnectionManager.stopper,由后者按
|
|
58
|
+
* `agent + pid` 登记到 AgentStopperRegistry,Terminator 停进程时按 pid 取回。
|
|
59
|
+
*
|
|
60
|
+
* 两件事,顺序不能反:
|
|
61
|
+
* 1. 若本连接就是当前在途 turn 的连接,先发协议取消(codex turn/interrupt /
|
|
62
|
+
* ACP session/cancel / pi abort)。**归属判定不能省**:runner 实例按 agent
|
|
63
|
+
* 维度共享(一个实例管多条 workspace 连接),无条件调用
|
|
64
|
+
* `cancelCurrentTurn()` 会用它自己的 currentClient/activeSessionId 去打断
|
|
65
|
+
* **另一个 workspace** 正在跑的 turn;
|
|
66
|
+
* 2. 关 stdin。stdio 型 agent server 在 stdin EOF 后自行退出——只发协议取消
|
|
67
|
+
* 不会让进程退出,优雅段会空转到 grace 超时后照样树杀,等于通道白注册。
|
|
68
|
+
*/
|
|
69
|
+
protected buildCooperativeStop(client: TClient): AgentStopper;
|
|
52
70
|
get isRunning(): boolean;
|
|
53
71
|
/** Agent self-describing status info for /status display. */
|
|
54
72
|
abstract getStatusInfo(): AgentStatusInfo;
|
|
@@ -27,6 +27,37 @@ export class ConnectionBasedRunner {
|
|
|
27
27
|
this.sessionReader = opts.sessionReader;
|
|
28
28
|
this.turnIdleTimeoutMs = opts.turnIdleTimeoutMs ?? defaultTurnIdleTimeoutMs;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* 本连接的协议停止通道(design §3.3)。
|
|
32
|
+
*
|
|
33
|
+
* win32 上「优雅停止」没有可拦截的跨进程 SIGTERM,只能经 agent 自有协议通道
|
|
34
|
+
* 请求对方收摊。子类把这个方法的结果交给 ConnectionManager.stopper,由后者按
|
|
35
|
+
* `agent + pid` 登记到 AgentStopperRegistry,Terminator 停进程时按 pid 取回。
|
|
36
|
+
*
|
|
37
|
+
* 两件事,顺序不能反:
|
|
38
|
+
* 1. 若本连接就是当前在途 turn 的连接,先发协议取消(codex turn/interrupt /
|
|
39
|
+
* ACP session/cancel / pi abort)。**归属判定不能省**:runner 实例按 agent
|
|
40
|
+
* 维度共享(一个实例管多条 workspace 连接),无条件调用
|
|
41
|
+
* `cancelCurrentTurn()` 会用它自己的 currentClient/activeSessionId 去打断
|
|
42
|
+
* **另一个 workspace** 正在跑的 turn;
|
|
43
|
+
* 2. 关 stdin。stdio 型 agent server 在 stdin EOF 后自行退出——只发协议取消
|
|
44
|
+
* 不会让进程退出,优雅段会空转到 grace 超时后照样树杀,等于通道白注册。
|
|
45
|
+
*/
|
|
46
|
+
buildCooperativeStop(client) {
|
|
47
|
+
return async (proc) => {
|
|
48
|
+
if (this.currentClient === client) {
|
|
49
|
+
try {
|
|
50
|
+
await this.cancelCurrentTurn();
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
getLogger().warn(`[${this.logTag}] cooperative stop: cancel failed: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (proc.stdin && !proc.stdin.destroyed) {
|
|
57
|
+
proc.stdin.end();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
30
61
|
get isRunning() {
|
|
31
62
|
return this._isRunning;
|
|
32
63
|
}
|
|
@@ -128,7 +128,12 @@ export class JsonRpcClient {
|
|
|
128
128
|
return new Promise((resolve, reject) => {
|
|
129
129
|
// Clamp to setTimeout's max: callers may pass Infinity for turn-scoped
|
|
130
130
|
// requests (session/prompt) — Infinity would fire immediately in Node.
|
|
131
|
-
|
|
131
|
+
// 配置里的 `requestTimeoutMs: 0` = 不超时(同族 `turnIdleTimeoutMinutes`、
|
|
132
|
+
// `claude.idleTtlMinutes` 都是「0 = 禁用」)。按 0 直接 setTimeout 会让
|
|
133
|
+
// 每个请求在响应到达前抢先 reject,等于整个 agent 不可用。
|
|
134
|
+
const budget = timeoutMs ??
|
|
135
|
+
(this.requestTimeoutMs === 0 ? Number.POSITIVE_INFINITY : this.requestTimeoutMs);
|
|
136
|
+
const effectiveTimeout = Math.min(budget, 2_147_483_647);
|
|
132
137
|
const timer = setTimeout(() => {
|
|
133
138
|
this.pendingRequests.delete(id);
|
|
134
139
|
reject(new RpcTimeoutError(`request timed out: ${method}`));
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { JsonlRpcTransport } from './transport.js';
|
|
17
17
|
import { JsonRpcClient } from './client.js';
|
|
18
|
+
import { AgentStopperRegistry, type AgentStopper } from '../../../platform/agent-stopper.js';
|
|
18
19
|
/** Minimal client surface the manager needs from any protocol client. */
|
|
19
20
|
export interface ConnectionClient {
|
|
20
21
|
ready: boolean;
|
|
@@ -75,6 +76,8 @@ export interface ConnectionManagerOptions<TClient extends ConnectionClient = Jso
|
|
|
75
76
|
}) => boolean;
|
|
76
77
|
/** Log tag prefix for operational log lines. */
|
|
77
78
|
logTag?: string;
|
|
79
|
+
/** 协议停止通道注册表注入(测试隔离);默认进程级单例,透传给 transport。 */
|
|
80
|
+
stoppers?: AgentStopperRegistry;
|
|
78
81
|
}
|
|
79
82
|
export declare class ConnectionManager<TClient extends ConnectionClient = JsonRpcClient> {
|
|
80
83
|
private slots;
|
|
@@ -87,8 +90,22 @@ export declare class ConnectionManager<TClient extends ConnectionClient = JsonRp
|
|
|
87
90
|
private readonly clientFactory;
|
|
88
91
|
private readonly shouldReuse;
|
|
89
92
|
private readonly logTag;
|
|
93
|
+
private readonly stoppers;
|
|
90
94
|
/** Callback when a connection is lost — cleared from slot map. */
|
|
91
95
|
onConnectionLost?: (workspace: string) => void;
|
|
96
|
+
/**
|
|
97
|
+
* 协议停止通道工厂(design §3.3):连接建立后按 pid 登记到 AgentStopperRegistry,
|
|
98
|
+
* 供 Terminator 在**优雅**停止时回调(win32 上没有可拦截的 SIGTERM,只能靠
|
|
99
|
+
* 协议通道让 agent 自己收摊)。返回 undefined = 该协议无通道。
|
|
100
|
+
*
|
|
101
|
+
* 做成「构造后赋值的字段」而不是构造选项:kimi/opencode 的 manager 建在
|
|
102
|
+
* `super(...)` 的实参里,那时 `this` 尚不可用,runner 无法把
|
|
103
|
+
* cancelCurrentTurn 塞进选项——只能等 super() 返回后赋值。
|
|
104
|
+
*/
|
|
105
|
+
stopper?: (ctx: {
|
|
106
|
+
pid: number;
|
|
107
|
+
client: TClient;
|
|
108
|
+
}) => AgentStopper | undefined;
|
|
92
109
|
constructor(opts: ConnectionManagerOptions<TClient>);
|
|
93
110
|
/**
|
|
94
111
|
* Acquire a connection for the given workspace.
|
|
@@ -117,6 +134,9 @@ export declare class ConnectionManager<TClient extends ConnectionClient = JsonRp
|
|
|
117
134
|
notifyActivity(workspace: string): void;
|
|
118
135
|
/**
|
|
119
136
|
* Notify that the workspace is idle — arm the idle timer.
|
|
137
|
+
*
|
|
138
|
+
* `idleTtlMs: 0` = 不回收(与 claude 会话层的 `armIdleTimer` 同口径;按 0
|
|
139
|
+
* setTimeout 会秒删刚建好的连接,等于每条消息重起一次进程)。
|
|
120
140
|
*/
|
|
121
141
|
notifyIdle(workspace: string): void;
|
|
122
142
|
private createClient;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { JsonlRpcTransport } from './transport.js';
|
|
17
17
|
import { JsonRpcClient } from './client.js';
|
|
18
18
|
import { getLogger } from '../../../logger/index.js';
|
|
19
|
+
import { agentStopperRegistry, } from '../../../platform/agent-stopper.js';
|
|
19
20
|
const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
20
21
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000; // 60 seconds
|
|
21
22
|
export class ConnectionManager {
|
|
@@ -29,8 +30,19 @@ export class ConnectionManager {
|
|
|
29
30
|
clientFactory;
|
|
30
31
|
shouldReuse;
|
|
31
32
|
logTag;
|
|
33
|
+
stoppers;
|
|
32
34
|
/** Callback when a connection is lost — cleared from slot map. */
|
|
33
35
|
onConnectionLost;
|
|
36
|
+
/**
|
|
37
|
+
* 协议停止通道工厂(design §3.3):连接建立后按 pid 登记到 AgentStopperRegistry,
|
|
38
|
+
* 供 Terminator 在**优雅**停止时回调(win32 上没有可拦截的 SIGTERM,只能靠
|
|
39
|
+
* 协议通道让 agent 自己收摊)。返回 undefined = 该协议无通道。
|
|
40
|
+
*
|
|
41
|
+
* 做成「构造后赋值的字段」而不是构造选项:kimi/opencode 的 manager 建在
|
|
42
|
+
* `super(...)` 的实参里,那时 `this` 尚不可用,runner 无法把
|
|
43
|
+
* cancelCurrentTurn 塞进选项——只能等 super() 返回后赋值。
|
|
44
|
+
*/
|
|
45
|
+
stopper;
|
|
34
46
|
constructor(opts) {
|
|
35
47
|
this.binary = opts.binary;
|
|
36
48
|
this.buildArgs = opts.buildArgs ?? (() => opts.args ?? []);
|
|
@@ -47,6 +59,7 @@ export class ConnectionManager {
|
|
|
47
59
|
}, ctx.requestTimeoutMs, this.initializeParams));
|
|
48
60
|
this.shouldReuse = opts.shouldReuse ?? (({ client }) => client.ready && client.healthy);
|
|
49
61
|
this.logTag = opts.logTag ?? 'jsonrpc-connection-manager';
|
|
62
|
+
this.stoppers = opts.stoppers ?? agentStopperRegistry;
|
|
50
63
|
}
|
|
51
64
|
/**
|
|
52
65
|
* Acquire a connection for the given workspace.
|
|
@@ -142,8 +155,13 @@ export class ConnectionManager {
|
|
|
142
155
|
}
|
|
143
156
|
/**
|
|
144
157
|
* Notify that the workspace is idle — arm the idle timer.
|
|
158
|
+
*
|
|
159
|
+
* `idleTtlMs: 0` = 不回收(与 claude 会话层的 `armIdleTimer` 同口径;按 0
|
|
160
|
+
* setTimeout 会秒删刚建好的连接,等于每条消息重起一次进程)。
|
|
145
161
|
*/
|
|
146
162
|
notifyIdle(workspace) {
|
|
163
|
+
if (this.idleTtlMs <= 0)
|
|
164
|
+
return;
|
|
147
165
|
const slot = this.slots.get(workspace);
|
|
148
166
|
if (slot) {
|
|
149
167
|
this.clearIdleTimer(slot);
|
|
@@ -157,13 +175,21 @@ export class ConnectionManager {
|
|
|
157
175
|
// Internal
|
|
158
176
|
// =========================================================================
|
|
159
177
|
async createClient(workspace, req) {
|
|
178
|
+
let client = null;
|
|
160
179
|
const transport = new JsonlRpcTransport({
|
|
161
180
|
binary: this.binary,
|
|
162
181
|
args: this.buildArgs(req),
|
|
163
182
|
cwd: workspace,
|
|
164
183
|
env: this.env,
|
|
184
|
+
stoppers: this.stoppers,
|
|
185
|
+
// 通道工厂要拿到**连接本身**:协议取消(codex turn/interrupt、ACP
|
|
186
|
+
// session/cancel)必须发在这条连接上,不能借 runner 的共享状态——那会
|
|
187
|
+
// 打断另一 workspace 正在跑的 turn。工厂是在 transport.start() 内侧调用
|
|
188
|
+
// 的,那时 client 已由下面的 clientFactory 赋好。
|
|
189
|
+
stopper: this.stopper
|
|
190
|
+
? (pid) => this.stopper?.({ pid, client: client })
|
|
191
|
+
: undefined,
|
|
165
192
|
});
|
|
166
|
-
let client = null;
|
|
167
193
|
client = this.clientFactory({
|
|
168
194
|
transport,
|
|
169
195
|
requestTimeoutMs: this.requestTimeoutMs,
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* each line on stdout is a JSON message from the server; each `write()`
|
|
7
7
|
* sends a JSON line to stdin.
|
|
8
8
|
*/
|
|
9
|
+
import { type Terminator } from '../../../platform/terminator.js';
|
|
10
|
+
import { AgentStopperRegistry, type AgentStopper } from '../../../platform/agent-stopper.js';
|
|
9
11
|
export interface TransportEvents {
|
|
10
12
|
onMessage(msg: unknown): void;
|
|
11
13
|
onClose(reason: string): void;
|
|
@@ -14,11 +16,17 @@ export declare class JsonlRpcTransport {
|
|
|
14
16
|
private proc;
|
|
15
17
|
private _closed;
|
|
16
18
|
private events;
|
|
17
|
-
private
|
|
19
|
+
private terminator;
|
|
18
20
|
private readonly binary;
|
|
19
21
|
private readonly args;
|
|
20
22
|
private readonly cwd;
|
|
21
23
|
private readonly env;
|
|
24
|
+
/** 协议停止通道查找键(与 Terminator 用的 agent key 同一来源,见构造函数)。 */
|
|
25
|
+
private readonly agent;
|
|
26
|
+
private readonly stoppers;
|
|
27
|
+
private readonly stopperFactory?;
|
|
28
|
+
/** 已登记的通道(agent+pid 键);null = 本连接没有可用通道或已注销。 */
|
|
29
|
+
private registration;
|
|
22
30
|
/** JSON lines buffered while stdin is under backpressure (bounded by callers). */
|
|
23
31
|
private writeQueue;
|
|
24
32
|
private flushing;
|
|
@@ -29,7 +37,20 @@ export declare class JsonlRpcTransport {
|
|
|
29
37
|
args: string[];
|
|
30
38
|
cwd: string;
|
|
31
39
|
env?: Record<string, string | undefined>;
|
|
40
|
+
/** win32 查协议停止通道用的 agent key;缺省取 binary 名('kimi'/'codex'/…) */
|
|
41
|
+
agent?: string;
|
|
42
|
+
/** 终止器注入(测试用);默认按平台建实现 */
|
|
43
|
+
terminator?: Terminator;
|
|
44
|
+
/**
|
|
45
|
+
* 协议停止通道工厂(design §3.3):spawn 成功后按 pid 建通道并登记到
|
|
46
|
+
* AgentStopperRegistry,进程退出/关闭时注销。返回 undefined = 无通道。
|
|
47
|
+
*/
|
|
48
|
+
stopper?: (pid: number) => AgentStopper | undefined;
|
|
49
|
+
/** 协议通道注册表注入(测试隔离);默认进程级单例 */
|
|
50
|
+
stoppers?: AgentStopperRegistry;
|
|
32
51
|
});
|
|
52
|
+
/** 子进程 pid(spawn 前/退出后为 undefined);供测试与连接层按 pid 定位。 */
|
|
53
|
+
get pid(): number | undefined;
|
|
33
54
|
get closed(): boolean;
|
|
34
55
|
/**
|
|
35
56
|
* Spawn the child process and start reading its stdout.
|
|
@@ -48,5 +69,12 @@ export declare class JsonlRpcTransport {
|
|
|
48
69
|
* Idempotent — safe to call multiple times.
|
|
49
70
|
*/
|
|
50
71
|
close(): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* 按 pid 登记协议停止通道(工厂返回 undefined = 该 agent 无通道)。
|
|
74
|
+
* pid 是归属键:同一 agent 的多条长驻连接各有自己的通道,不能互相顶替。
|
|
75
|
+
*/
|
|
76
|
+
private registerStopper;
|
|
77
|
+
/** 注销自己登记的那条通道(幂等;带身份校验,防误删同 pid 上的新通道)。 */
|
|
78
|
+
private unregisterStopper;
|
|
51
79
|
private handleClose;
|
|
52
80
|
}
|