lark-remote 0.1.8 → 0.1.10
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 +38 -0
- package/README.en.md +4 -4
- package/README.md +4 -4
- package/dist/bridge/approval-coordinator.d.ts +18 -0
- package/dist/bridge/approval-coordinator.js +64 -3
- package/dist/bridge/index.d.ts +17 -0
- package/dist/bridge/index.js +35 -2
- package/dist/card/approval-render.js +105 -8
- package/dist/index.js +17 -3
- package/dist/order/alias-resolve.d.ts +6 -2
- package/dist/order/index.d.ts +17 -2
- package/dist/order/index.js +57 -0
- package/dist/router/index.d.ts +35 -3
- package/dist/router/index.js +529 -85
- package/dist/runner/claude/runner.js +22 -0
- package/dist/runner/claude/session.d.ts +26 -0
- package/dist/runner/claude/session.js +94 -13
- package/dist/runner/common/connection-based-runner.js +10 -5
- package/dist/runner/dsh/runner.d.ts +7 -0
- package/dist/runner/dsh/runner.js +125 -74
- package/dist/runner/kimi/acp/runner.js +15 -0
- package/dist/runner/pi/rpc/runner.js +6 -1
- package/dist/runner/registry.d.ts +3 -2
- package/dist/runner/registry.js +3 -2
- package/dist/runner/types.d.ts +33 -2
- package/dist/session/common/pagination.d.ts +1 -1
- package/dist/session/common/pagination.js +1 -1
- package/dist/session/dsh/sessions.d.ts +6 -1
- package/dist/session/dsh/sessions.js +20 -5
- package/dist/workspace/index.d.ts +8 -0
- package/dist/workspace/index.js +20 -1
- package/package.json +1 -1
- package/dist/order/alias-store.d.ts +0 -38
- package/dist/order/alias-store.js +0 -106
|
@@ -102,6 +102,28 @@ export class ClaudeRunner {
|
|
|
102
102
|
this.session.setAutoApprove(true);
|
|
103
103
|
await this.session.respondPermission(requestId, { behavior: 'allow' });
|
|
104
104
|
return;
|
|
105
|
+
case 'accept_with_feedback': {
|
|
106
|
+
// 计划审批「批准并采纳修改」:allow + updatedInput.plan(改写过的新计划),
|
|
107
|
+
// 触发 Claude 侧 planWasEdited → tool_result 回显 edited plan 并写回文件。
|
|
108
|
+
const plan = response.plan;
|
|
109
|
+
if (!plan || !plan.trim()) {
|
|
110
|
+
throw new Error('approval accept_with_feedback missing plan payload');
|
|
111
|
+
}
|
|
112
|
+
await this.session.respondPermission(requestId, {
|
|
113
|
+
behavior: 'allow',
|
|
114
|
+
updatedInput: { plan },
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
case 'decline_with_feedback': {
|
|
119
|
+
// 计划审批「拒绝并附意见」:deny + message,Claude 留在 plan 模式修订。
|
|
120
|
+
const message = response.message?.trim();
|
|
121
|
+
if (!message) {
|
|
122
|
+
throw new Error('approval decline_with_feedback missing message payload');
|
|
123
|
+
}
|
|
124
|
+
await this.session.respondPermission(requestId, { behavior: 'deny', message });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
105
127
|
case 'decline':
|
|
106
128
|
await this.session.respondPermission(requestId, { behavior: 'deny' });
|
|
107
129
|
return;
|
|
@@ -46,6 +46,11 @@ export interface ClaudeSessionOptions {
|
|
|
46
46
|
export interface PermissionResult {
|
|
47
47
|
behavior: 'allow' | 'deny';
|
|
48
48
|
message?: string;
|
|
49
|
+
/**
|
|
50
|
+
* allow 时显式覆盖 updatedInput(默认回写 pendingToolInputs 原始 input)。
|
|
51
|
+
* 计划审批「批准并采纳修改意见」用它携带改写的 plan。
|
|
52
|
+
*/
|
|
53
|
+
updatedInput?: unknown;
|
|
49
54
|
}
|
|
50
55
|
/**
|
|
51
56
|
* ClaudeSession extends SpawningRunner 复用其 spawn/pid 文件/killOrphan/
|
|
@@ -74,6 +79,12 @@ export declare class ClaudeSession extends SpawningRunner {
|
|
|
74
79
|
private waitResolve;
|
|
75
80
|
/** 待审批的 control_request 原始 input(回写 updatedInput 需要)。 */
|
|
76
81
|
private pendingToolInputs;
|
|
82
|
+
/**
|
|
83
|
+
* 本会话内跟踪到的计划文件路径:Write/Edit 工具把文件写到 `plans` 目录
|
|
84
|
+
* 时记录(ExitPlanMode 的 plan 内容不总是经 input 注入,文件是可靠兜底)。
|
|
85
|
+
* 会话进程生命周期内有效(长驻进程 = 一个会话)。
|
|
86
|
+
*/
|
|
87
|
+
private planFilePath;
|
|
77
88
|
/** 允许所有:后续 control_request 自动 allow(本会话进程生命周期内有效)。 */
|
|
78
89
|
private autoApprove;
|
|
79
90
|
/** 串行化 stdin 写入(用户消息与审批响应交错到达)。 */
|
|
@@ -160,6 +171,21 @@ export declare class ClaudeSession extends SpawningRunner {
|
|
|
160
171
|
*/
|
|
161
172
|
private writeStdin;
|
|
162
173
|
private translateEvent;
|
|
174
|
+
/**
|
|
175
|
+
* 跟踪会话内 Write/Edit 工具写到 `plans` 目录的文件路径:CLI 对
|
|
176
|
+
* ExitPlanMode 的 plan 注入不稳定(input.plan 时有时无),但模型被要求
|
|
177
|
+
* 先把计划写入计划文件,路径可经工具调用可靠捕获(含模型写错目录的
|
|
178
|
+
* 退化场景——渲染层可展示实际落盘位置)。
|
|
179
|
+
*/
|
|
180
|
+
private trackPlanFilePath;
|
|
181
|
+
/**
|
|
182
|
+
* ExitPlanMode 计划解析,优先级:
|
|
183
|
+
* 1. control_request input.plan(CLI normalizeToolInput 从磁盘注入,非稳定);
|
|
184
|
+
* 2. input.planFilePath 指向的文件;
|
|
185
|
+
* 3. 会话内 Write/Edit 跟踪到的计划文件。
|
|
186
|
+
* 全部落空时返回空对象,渲染层展示「未获取到计划文件内容」兜底。
|
|
187
|
+
*/
|
|
188
|
+
private resolvePlanContent;
|
|
163
189
|
private translateControlRequest;
|
|
164
190
|
/** 统一方案:所有 runner 自己生成 timestamp(缺失时补)。 */
|
|
165
191
|
private withTimestamp;
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* 进程在 turn 之间保持存活(stdin 不关闭),/stop / /new / /cd / 看门狗超时
|
|
28
28
|
* 时经 ProcessStopper 组杀,下条消息按 SessionStore 的 sessionId --resume。
|
|
29
29
|
*/
|
|
30
|
+
import { readFile } from 'node:fs/promises';
|
|
30
31
|
import { silentlyUnlink } from '../../common/fs.js';
|
|
31
32
|
import { getLogger } from '../../logger/index.js';
|
|
32
33
|
import { SpawningRunner } from '../common/spawning-runner.js';
|
|
@@ -35,6 +36,8 @@ import { authErrorEvent, syntheticInitEvent } from '../common/runner-utils.js';
|
|
|
35
36
|
import { makeQuestionApprovalEvent } from '../question-common.js';
|
|
36
37
|
/** 拒绝权限时的默认提示(进入 claude 上下文,中文用户可读)。 */
|
|
37
38
|
const DENY_MESSAGE = '用户拒绝了此工具调用,请停止并等待用户指令。';
|
|
39
|
+
/** ExitPlanMode 计划文件读取上限(超出截断,防超大计划拖垮事件翻译)。 */
|
|
40
|
+
const MAX_PLAN_CHARS = 60_000;
|
|
38
41
|
/** 会话级空闲回收默认 TTL(对齐 codex appServer.idleTtlMs 默认 30 分钟)。 */
|
|
39
42
|
const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
40
43
|
/** result 事件里表示「turn 中途压缩」的 subtype(不是 turn 结束)。 */
|
|
@@ -66,6 +69,12 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
66
69
|
waitResolve = null;
|
|
67
70
|
/** 待审批的 control_request 原始 input(回写 updatedInput 需要)。 */
|
|
68
71
|
pendingToolInputs = new Map();
|
|
72
|
+
/**
|
|
73
|
+
* 本会话内跟踪到的计划文件路径:Write/Edit 工具把文件写到 `plans` 目录
|
|
74
|
+
* 时记录(ExitPlanMode 的 plan 内容不总是经 input 注入,文件是可靠兜底)。
|
|
75
|
+
* 会话进程生命周期内有效(长驻进程 = 一个会话)。
|
|
76
|
+
*/
|
|
77
|
+
planFilePath = '';
|
|
69
78
|
/** 允许所有:后续 control_request 自动 allow(本会话进程生命周期内有效)。 */
|
|
70
79
|
autoApprove = false;
|
|
71
80
|
/** 串行化 stdin 写入(用户消息与审批响应交错到达)。 */
|
|
@@ -182,7 +191,9 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
182
191
|
if (result.behavior === 'allow') {
|
|
183
192
|
await this.writeControlResponse(requestId, {
|
|
184
193
|
behavior: 'allow',
|
|
185
|
-
updatedInput
|
|
194
|
+
// 显式 updatedInput 优先(计划审批「批准并采纳修改」携带改写 plan);
|
|
195
|
+
// 缺省回写原始 input,保证其他工具审批行为不变。
|
|
196
|
+
updatedInput: result.updatedInput !== undefined ? result.updatedInput : (input ?? {}),
|
|
186
197
|
});
|
|
187
198
|
}
|
|
188
199
|
else {
|
|
@@ -301,6 +312,10 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
301
312
|
this.cwd = opts.cwd;
|
|
302
313
|
this.autoApprove = false;
|
|
303
314
|
this.pendingToolInputs.clear();
|
|
315
|
+
// 每次 run 独立:上次 run 会话内跟踪到的计划文件不得泄漏到本次
|
|
316
|
+
// (否则本次 ExitPlanMode 无 plan/planFilePath 时 resolvePlanContent
|
|
317
|
+
// 会误读上一次的计划文件当当前计划)。
|
|
318
|
+
this.planFilePath = '';
|
|
304
319
|
let proc;
|
|
305
320
|
try {
|
|
306
321
|
proc = await this.spawnChild(opts);
|
|
@@ -344,7 +359,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
344
359
|
for await (const raw of stream) {
|
|
345
360
|
if (generation !== this.processGeneration)
|
|
346
361
|
return; // 旧进程残留事件丢弃
|
|
347
|
-
const events = this.translateEvent(raw);
|
|
362
|
+
const events = await this.translateEvent(raw);
|
|
348
363
|
if (events.length > 0) {
|
|
349
364
|
this.eventQueue.push(...events);
|
|
350
365
|
this.wakeWaiters();
|
|
@@ -530,7 +545,7 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
530
545
|
// =========================================================================
|
|
531
546
|
// Internal: event translation
|
|
532
547
|
// =========================================================================
|
|
533
|
-
translateEvent(raw) {
|
|
548
|
+
async translateEvent(raw) {
|
|
534
549
|
const event = raw;
|
|
535
550
|
const type = event.type;
|
|
536
551
|
// review P3-3:--resume 会先重放上一轮历史事件(旧 result 之外还可能有
|
|
@@ -538,6 +553,10 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
538
553
|
// 内容混进当前卡片(result 分支的 sawInit 守卫升级为全类型守卫)。
|
|
539
554
|
if (type !== 'system' && !this.sawInit)
|
|
540
555
|
return [];
|
|
556
|
+
// 跟踪会话内计划文件写入(ExitPlanMode 审批卡的 plan 文件兜底)。
|
|
557
|
+
if (type === 'assistant') {
|
|
558
|
+
this.trackPlanFilePath(event);
|
|
559
|
+
}
|
|
541
560
|
if (type === 'control_request') {
|
|
542
561
|
return this.translateControlRequest(event);
|
|
543
562
|
}
|
|
@@ -588,7 +607,63 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
588
607
|
// 下游 reducer 忽略,保留诊断价值。
|
|
589
608
|
return [this.withTimestamp(event)];
|
|
590
609
|
}
|
|
591
|
-
|
|
610
|
+
/**
|
|
611
|
+
* 跟踪会话内 Write/Edit 工具写到 `plans` 目录的文件路径:CLI 对
|
|
612
|
+
* ExitPlanMode 的 plan 注入不稳定(input.plan 时有时无),但模型被要求
|
|
613
|
+
* 先把计划写入计划文件,路径可经工具调用可靠捕获(含模型写错目录的
|
|
614
|
+
* 退化场景——渲染层可展示实际落盘位置)。
|
|
615
|
+
*/
|
|
616
|
+
trackPlanFilePath(raw) {
|
|
617
|
+
const content = raw.message?.content;
|
|
618
|
+
if (!Array.isArray(content))
|
|
619
|
+
return;
|
|
620
|
+
for (const block of content) {
|
|
621
|
+
const b = block;
|
|
622
|
+
if (b?.type !== 'tool_use')
|
|
623
|
+
continue;
|
|
624
|
+
if (b.name !== 'Write' && b.name !== 'Edit')
|
|
625
|
+
continue;
|
|
626
|
+
const fp = b.input?.file_path;
|
|
627
|
+
if (typeof fp === 'string' && fp.includes('/plans/')) {
|
|
628
|
+
this.planFilePath = fp;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* ExitPlanMode 计划解析,优先级:
|
|
634
|
+
* 1. control_request input.plan(CLI normalizeToolInput 从磁盘注入,非稳定);
|
|
635
|
+
* 2. input.planFilePath 指向的文件;
|
|
636
|
+
* 3. 会话内 Write/Edit 跟踪到的计划文件。
|
|
637
|
+
* 全部落空时返回空对象,渲染层展示「未获取到计划文件内容」兜底。
|
|
638
|
+
*/
|
|
639
|
+
async resolvePlanContent(input, trackedPath) {
|
|
640
|
+
const direct = typeof input.plan === 'string' && input.plan.trim() ? input.plan : undefined;
|
|
641
|
+
if (direct !== undefined) {
|
|
642
|
+
// 计划已随 input 注入时仍携带文件路径(input.planFilePath 或会话内跟踪),
|
|
643
|
+
// 供卡片展示「计划落盘位置」。
|
|
644
|
+
const filePath = typeof input.planFilePath === 'string' && input.planFilePath
|
|
645
|
+
? input.planFilePath
|
|
646
|
+
: trackedPath;
|
|
647
|
+
return filePath ? { plan: direct, planFilePath: filePath } : { plan: direct };
|
|
648
|
+
}
|
|
649
|
+
const candidate = typeof input.planFilePath === 'string' && input.planFilePath
|
|
650
|
+
? input.planFilePath
|
|
651
|
+
: trackedPath;
|
|
652
|
+
if (!candidate)
|
|
653
|
+
return {};
|
|
654
|
+
try {
|
|
655
|
+
const content = await readFile(candidate, 'utf8');
|
|
656
|
+
const trimmed = content.trim();
|
|
657
|
+
if (trimmed) {
|
|
658
|
+
return { plan: trimmed.slice(0, MAX_PLAN_CHARS), planFilePath: candidate };
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
// 计划文件缺失/不可读:fall through,渲染层兜底。
|
|
663
|
+
}
|
|
664
|
+
return {};
|
|
665
|
+
}
|
|
666
|
+
async translateControlRequest(raw) {
|
|
592
667
|
const requestId = raw.request_id;
|
|
593
668
|
if (requestId === null || requestId === undefined) {
|
|
594
669
|
getLogger().warn(`[${this.logTag}] control_request missing request_id, dropping`);
|
|
@@ -642,21 +717,27 @@ export class ClaudeSession extends SpawningRunner {
|
|
|
642
717
|
}
|
|
643
718
|
return [makeQuestionApprovalEvent(requestId, questions, this.sessionId)];
|
|
644
719
|
}
|
|
645
|
-
// ExitPlanMode
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
// 标题/工具名 + reason 表达审批内容。
|
|
720
|
+
// ExitPlanMode:计划已写入计划文件(或经 input.plan 注入)。没有 command
|
|
721
|
+
// 语义,塞进 command 槽位会显示无意义 `{}` → 用独立 tool kind,把计划
|
|
722
|
+
// 全文 + 文件路径带给渲染层,决策对齐 TUI 四种交互。
|
|
649
723
|
if (toolName === 'ExitPlanMode') {
|
|
650
|
-
const
|
|
724
|
+
const resolved = await this.resolvePlanContent(input, this.planFilePath);
|
|
651
725
|
const view = {
|
|
652
726
|
requestId,
|
|
653
727
|
kind: 'tool',
|
|
654
728
|
toolName,
|
|
655
729
|
reason: typeof request.description === 'string' ? request.description : undefined,
|
|
656
|
-
|
|
657
|
-
...(
|
|
658
|
-
//
|
|
659
|
-
|
|
730
|
+
...(resolved.plan !== undefined ? { plan: resolved.plan } : {}),
|
|
731
|
+
...(resolved.planFilePath !== undefined ? { planFilePath: resolved.planFilePath } : {}),
|
|
732
|
+
// 对齐 TUI:批准并执行 / 批准并自动放行(acceptAll)/ 拒绝并附意见 /
|
|
733
|
+
// 批准并采纳修改 / 拒绝。
|
|
734
|
+
availableDecisions: [
|
|
735
|
+
'accept',
|
|
736
|
+
'acceptAll',
|
|
737
|
+
'declineWithFeedback',
|
|
738
|
+
'acceptWithFeedback',
|
|
739
|
+
'decline',
|
|
740
|
+
],
|
|
660
741
|
};
|
|
661
742
|
getLogger().info(`[${this.logTag}] permission request requestId=${requestId} tool=${toolName}`);
|
|
662
743
|
return [
|
|
@@ -104,18 +104,23 @@ export class ConnectionBasedRunner {
|
|
|
104
104
|
* unblock; otherwise cancel now via cancelCurrentTurn().
|
|
105
105
|
*/
|
|
106
106
|
async stop(_opts) {
|
|
107
|
-
if (!this._isRunning
|
|
107
|
+
if (!this._isRunning)
|
|
108
108
|
return;
|
|
109
|
+
// CC-01: 启动阶段(client/session 未建立)也必须记录停止意图,否则 /stop 被吞、
|
|
110
|
+
// turn 照常执行。executeTurn 在 setup 完成后检查 stopRequested 补发 cancel。
|
|
111
|
+
this.stopRequested = true;
|
|
112
|
+
this.forceFinish = true;
|
|
113
|
+
this.wakeWaiters();
|
|
114
|
+
if (!this.currentClient || !this.currentSessionId()) {
|
|
115
|
+
getLogger().warn(`[${this.logTag}] stop during setup (client/session not ready); will cancel after setup`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
109
118
|
if (this.shouldDeferStop()) {
|
|
110
119
|
// turn/start 或 prompt 尚未响应:先标记,等 run() 完成后再补发 cancel。
|
|
111
|
-
this.stopRequested = true;
|
|
112
|
-
this.forceFinish = true;
|
|
113
|
-
this.wakeWaiters();
|
|
114
120
|
return;
|
|
115
121
|
}
|
|
116
122
|
// turn/start 或 prompt 已 resolved:立即 cancel;同时置 stopRequested 抑制
|
|
117
123
|
// 尚未结算的 prompt 结果翻译(opencode 语义),对已结算方无害。
|
|
118
|
-
this.stopRequested = true;
|
|
119
124
|
try {
|
|
120
125
|
await this.cancelCurrentTurn();
|
|
121
126
|
}
|
|
@@ -44,6 +44,13 @@ export declare class DshRunner implements AgentRunner {
|
|
|
44
44
|
private activeAbort;
|
|
45
45
|
private activeSessionId;
|
|
46
46
|
private running;
|
|
47
|
+
/**
|
|
48
|
+
* 已对齐模型的 session 集合:同一 session 二次 run 不再重发 selectModel
|
|
49
|
+
* (session.selectModel 会写服务端全局默认,每次 run 都写是写放大)。
|
|
50
|
+
* configuredModel 在实例生命周期内不变(/config 改模型会 clearRunners
|
|
51
|
+
* 重建实例 + 换新 sessionId),故「每 session 对齐一次」足以覆盖配置变化。
|
|
52
|
+
*/
|
|
53
|
+
private readonly alignedSessions;
|
|
47
54
|
constructor(opts: DshRunnerOptions);
|
|
48
55
|
get isRunning(): boolean;
|
|
49
56
|
getStatusInfo(): AgentStatusInfo;
|
|
@@ -28,6 +28,13 @@ export class DshRunner {
|
|
|
28
28
|
activeAbort = null;
|
|
29
29
|
activeSessionId = null;
|
|
30
30
|
running = false;
|
|
31
|
+
/**
|
|
32
|
+
* 已对齐模型的 session 集合:同一 session 二次 run 不再重发 selectModel
|
|
33
|
+
* (session.selectModel 会写服务端全局默认,每次 run 都写是写放大)。
|
|
34
|
+
* configuredModel 在实例生命周期内不变(/config 改模型会 clearRunners
|
|
35
|
+
* 重建实例 + 换新 sessionId),故「每 session 对齐一次」足以覆盖配置变化。
|
|
36
|
+
*/
|
|
37
|
+
alignedSessions = new Set();
|
|
31
38
|
constructor(opts) {
|
|
32
39
|
this.sessionReader = opts.sessionReader;
|
|
33
40
|
this.configuredModel = opts.model;
|
|
@@ -51,99 +58,143 @@ export class DshRunner {
|
|
|
51
58
|
};
|
|
52
59
|
}
|
|
53
60
|
async *run(message, opts) {
|
|
61
|
+
if (this.running) {
|
|
62
|
+
throw new Error('DshRunner is already running');
|
|
63
|
+
}
|
|
54
64
|
const abort = new AbortController();
|
|
55
65
|
this.activeAbort = abort;
|
|
56
66
|
this.running = true;
|
|
57
67
|
let sessionId = opts.sessionId;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// 模型/推理强度变更 = 保留 session,每次 run 前对齐(§4.3):
|
|
67
|
-
// 该调用同时会写服务端全局默认;失败仅告警不阻断 run(模型校验由 DSH 负责)。
|
|
68
|
-
if (this.configuredModel) {
|
|
69
|
-
try {
|
|
70
|
-
await this.client.selectModel({
|
|
71
|
-
sessionId,
|
|
72
|
-
provider: DSH_MODEL_PROVIDER,
|
|
73
|
-
model: this.configuredModel,
|
|
74
|
-
...(this.reasoningEffort ? { reasoningEffort: this.reasoningEffort } : {}),
|
|
68
|
+
let terminalEmitted = false;
|
|
69
|
+
try {
|
|
70
|
+
// CC-03: session.create 必须包进同一 try/finally,否则创建失败(服务端不可达/报错)
|
|
71
|
+
// 会在进入旧 try 之前抛异常,finally 不执行 → running 恒 true、activeAbort 残留。
|
|
72
|
+
if (!sessionId) {
|
|
73
|
+
const created = await this.client.createSession({
|
|
74
|
+
cwd: opts.cwd,
|
|
75
|
+
...(this.agentPreset ? { agentPreset: this.agentPreset } : {}),
|
|
75
76
|
});
|
|
77
|
+
sessionId = created.sessionId;
|
|
76
78
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
this.activeSessionId = sessionId;
|
|
80
|
+
// Startup abort:createSession 挂起期间 stop() 已 abort(此时 sessionId 尚
|
|
81
|
+
// 未知,stop() 无法 cancel)。createSession 返回后立即检查——已 abort 则
|
|
82
|
+
// cancel 这个刚建的 session 并中断,不让启动流程继续(selectModel/prompt)。
|
|
83
|
+
// 必须 yield interrupted result(不能裸 return):调用方依赖 result 收尾
|
|
84
|
+
// run 卡,否则 for-await 自然结束后无终态、卡片停留「运行中」。
|
|
85
|
+
if (abort.signal.aborted) {
|
|
86
|
+
try {
|
|
87
|
+
await this.client.unary('session.cancel', { sessionId });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* 取消失败仅告警,session 已 abort 无需继续 */
|
|
91
|
+
}
|
|
92
|
+
getLogger().info(`[dsh] run aborted during startup, cancelled session ${sessionId}`);
|
|
93
|
+
terminalEmitted = true;
|
|
94
|
+
yield {
|
|
95
|
+
type: 'result',
|
|
96
|
+
subtype: 'interrupted',
|
|
97
|
+
session_id: sessionId,
|
|
98
|
+
errorMessage: 'run stopped',
|
|
99
|
+
};
|
|
100
|
+
return;
|
|
79
101
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
// 模型/推理强度对齐(§4.3):会话首次 run 时 selectModel 一次
|
|
103
|
+
// (该调用同时写服务端全局默认,故不重复写);同一 session 后续 run
|
|
104
|
+
// 跳过。失败仅告警不阻断 run(模型校验由 DSH 负责)。
|
|
105
|
+
if (this.configuredModel && !this.alignedSessions.has(sessionId)) {
|
|
106
|
+
try {
|
|
107
|
+
await this.client.selectModel({
|
|
108
|
+
sessionId,
|
|
109
|
+
provider: DSH_MODEL_PROVIDER,
|
|
110
|
+
model: this.configuredModel,
|
|
111
|
+
...(this.reasoningEffort ? { reasoningEffort: this.reasoningEffort } : {}),
|
|
112
|
+
});
|
|
113
|
+
this.alignedSessions.add(sessionId);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
getLogger().warn(`[dsh] selectModel failed for session ${sessionId} (run continues): ${err.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
yield {
|
|
120
|
+
type: 'system',
|
|
121
|
+
subtype: 'init',
|
|
122
|
+
session_id: sessionId,
|
|
123
|
+
cwd: opts.cwd,
|
|
124
|
+
model: this.configuredModel ?? 'DSH',
|
|
125
|
+
};
|
|
100
126
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
// Snapshot the pre-prompt high-water mark before mutating the session.
|
|
128
|
+
// DSH session.history returns the whole session, so starting from seq 0
|
|
129
|
+
// would replay the first completed turn on every follow-up prompt.
|
|
130
|
+
const prePromptSeq = await this.client.latestEventSeq(sessionId);
|
|
131
|
+
await this.client.unary('session.prompt', {
|
|
132
|
+
sessionId,
|
|
133
|
+
mode: 'queue',
|
|
134
|
+
content: [{ type: 'text', text: message }],
|
|
135
|
+
});
|
|
136
|
+
const translator = new DshTranslator();
|
|
137
|
+
try {
|
|
138
|
+
for await (const item of this.client.sessionEvents(sessionId, opts.cwd, abort.signal, prePromptSeq)) {
|
|
139
|
+
if (abort.signal.aborted)
|
|
140
|
+
break;
|
|
141
|
+
if (item.kind === 'approval') {
|
|
142
|
+
getLogger().warn(`[dsh] approval requested approvalId=${item.approvalId} tool=${item.toolName} — resolve in DSH Web UI`);
|
|
143
|
+
yield {
|
|
144
|
+
type: 'assistant',
|
|
145
|
+
message: {
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: 'text',
|
|
149
|
+
text: `⚠️ 需要授权(${item.toolName}):请在 DSH Web UI 处理 ${this.client.baseUrl}`,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
for (const ev of translator.eventToAgentEvents(item.event, sessionId)) {
|
|
157
|
+
yield ev;
|
|
158
|
+
if (ev.type === 'result') {
|
|
159
|
+
terminalEmitted = true;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
124
162
|
}
|
|
125
163
|
}
|
|
126
164
|
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
if (abort.signal.aborted)
|
|
167
|
+
throw err;
|
|
168
|
+
getLogger().error(`[dsh] run consume error: ${err.message}`);
|
|
169
|
+
yield {
|
|
170
|
+
type: 'result',
|
|
171
|
+
subtype: 'error',
|
|
172
|
+
session_id: sessionId,
|
|
173
|
+
errorMessage: `DSH stream error: ${err.message}`,
|
|
174
|
+
};
|
|
175
|
+
terminalEmitted = true;
|
|
176
|
+
}
|
|
127
177
|
}
|
|
128
178
|
catch (err) {
|
|
129
|
-
if (abort.signal.aborted)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
terminalEmitted = true;
|
|
179
|
+
if (!abort.signal.aborted) {
|
|
180
|
+
yield {
|
|
181
|
+
type: 'result',
|
|
182
|
+
subtype: 'error',
|
|
183
|
+
session_id: sessionId,
|
|
184
|
+
errorMessage: `DSH run failed: ${err.message}`,
|
|
185
|
+
};
|
|
186
|
+
terminalEmitted = true;
|
|
187
|
+
}
|
|
139
188
|
}
|
|
140
189
|
}
|
|
141
190
|
catch (err) {
|
|
191
|
+
// CC-03: createSession 等启动期失败也产出 error result(而非抛异常),
|
|
192
|
+
// 由本 catch 统一转 error result;state 清理在 finally。
|
|
142
193
|
if (!abort.signal.aborted) {
|
|
143
194
|
yield {
|
|
144
195
|
type: 'result',
|
|
145
196
|
subtype: 'error',
|
|
146
|
-
session_id: sessionId,
|
|
197
|
+
session_id: sessionId ?? '',
|
|
147
198
|
errorMessage: `DSH run failed: ${err.message}`,
|
|
148
199
|
};
|
|
149
200
|
terminalEmitted = true;
|
|
@@ -158,7 +209,7 @@ export class DshRunner {
|
|
|
158
209
|
yield {
|
|
159
210
|
type: 'result',
|
|
160
211
|
subtype: abort.signal.aborted ? 'interrupted' : 'error',
|
|
161
|
-
session_id: sessionId,
|
|
212
|
+
session_id: sessionId ?? '',
|
|
162
213
|
errorMessage: abort.signal.aborted ? 'run stopped' : 'turn ended without a terminal event',
|
|
163
214
|
};
|
|
164
215
|
}
|
|
@@ -405,6 +405,21 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
405
405
|
sessionId = newResult.sessionId;
|
|
406
406
|
}
|
|
407
407
|
this.activeSessionId = sessionId;
|
|
408
|
+
// CC-07: 下发配置的模型(provider/model)。session/new|resume 只带 cwd/mcpServers,
|
|
409
|
+
// 不带 model;不主动下发则实际跑 kimi 服务端默认模型(旧 KimiRunner 通过 -m 传模型,
|
|
410
|
+
// 迁移到纯 ACP 后丢失)。仿 opencode 用 session/set_config_option。失败仅告警不阻断。
|
|
411
|
+
if (this.model) {
|
|
412
|
+
try {
|
|
413
|
+
await client.request('session/set_config_option', {
|
|
414
|
+
sessionId,
|
|
415
|
+
configId: 'model',
|
|
416
|
+
value: this.model,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
getLogger().warn(`[${this.logTag}] set_config_option model failed (non-fatal): ${err.message}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
408
423
|
// Set permission mode (§5.1): EVERY fresh ACP session starts in
|
|
409
424
|
// 'default' (= manual approvals, acp-server modes.ts DEFAULT_MODE_ID),
|
|
410
425
|
// so the mode must be sent unconditionally — skipping yolo leaves the
|
|
@@ -134,7 +134,12 @@ export class PiRpcRunner extends ConnectionBasedRunner {
|
|
|
134
134
|
this.currentTurnId = turnId;
|
|
135
135
|
this.pushEvents([translator.produceTurnStarted(sessionId, turnId)]);
|
|
136
136
|
this.promptSettled = false;
|
|
137
|
-
await client.request({ type: 'prompt', message });
|
|
137
|
+
const response = await client.request({ type: 'prompt', message });
|
|
138
|
+
if (!response.success) {
|
|
139
|
+
// CC-08: pi 返回 success:false(无效模型/provider 错误/忙)时立即失败,
|
|
140
|
+
// 不能当成成功 ACK 继续等 agent_settled(否则最长 turn idle timeout)。
|
|
141
|
+
throw new Error(`pi prompt failed: ${response.error ?? 'unknown error'}`);
|
|
142
|
+
}
|
|
138
143
|
this.promptSettled = true;
|
|
139
144
|
}
|
|
140
145
|
/**
|
|
@@ -5,8 +5,9 @@ import type { AgentKind, Runner } from './index.js';
|
|
|
5
5
|
* up `config.defaultAgent` here to pick the concrete runner.
|
|
6
6
|
*
|
|
7
7
|
* Singleton caching: the factory MAY cache its instance internally. Most
|
|
8
|
-
* current agents (claude, opencode, pi, kimi) are spawn-per-message; codex is
|
|
9
|
-
* workspace-lifetime (app-server)
|
|
8
|
+
* current spawn-type agents (claude, opencode, pi, kimi) are spawn-per-message; codex is
|
|
9
|
+
* workspace-lifetime (app-server); dsh is HTTP+WS direct connection (no local
|
|
10
|
+
* subprocess). Each factory returns a runner per call.
|
|
10
11
|
*
|
|
11
12
|
* Dynamic config reload: factory can read latest config via registry's
|
|
12
13
|
* configContainer, enabling runtime config changes to take effect.
|
package/dist/runner/registry.js
CHANGED
|
@@ -9,8 +9,9 @@ let globalRegistry;
|
|
|
9
9
|
* up `config.defaultAgent` here to pick the concrete runner.
|
|
10
10
|
*
|
|
11
11
|
* Singleton caching: the factory MAY cache its instance internally. Most
|
|
12
|
-
* current agents (claude, opencode, pi, kimi) are spawn-per-message; codex is
|
|
13
|
-
* workspace-lifetime (app-server)
|
|
12
|
+
* current spawn-type agents (claude, opencode, pi, kimi) are spawn-per-message; codex is
|
|
13
|
+
* workspace-lifetime (app-server); dsh is HTTP+WS direct connection (no local
|
|
14
|
+
* subprocess). Each factory returns a runner per call.
|
|
14
15
|
*
|
|
15
16
|
* Dynamic config reload: factory can read latest config via registry's
|
|
16
17
|
* configContainer, enabling runtime config changes to take effect.
|