lark-remote 0.5.19 → 0.5.21
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 +29 -0
- package/dist/bridge/approval-actions.d.ts +1 -1
- package/dist/bridge/approval-actions.js +1 -1
- package/dist/bridge/bash-run.d.ts +71 -0
- package/dist/bridge/bash-run.js +217 -0
- package/dist/bridge/compact.d.ts +155 -0
- package/dist/bridge/compact.js +450 -0
- package/dist/bridge/final-usage.js +4 -4
- package/dist/bridge/inbound-media.js +72 -46
- package/dist/bridge/index.d.ts +106 -153
- package/dist/bridge/index.js +400 -1000
- package/dist/bridge/job-watch.d.ts +68 -0
- package/dist/bridge/job-watch.js +146 -0
- package/dist/bridge/queue-manager.d.ts +3 -3
- package/dist/bridge/queue-manager.js +10 -29
- package/dist/card/approval-render.d.ts +2 -2
- package/dist/card/approval-render.js +141 -130
- package/dist/card/card-budget.js +123 -98
- package/dist/card/card-helpers.d.ts +3 -3
- package/dist/card/card-helpers.js +6 -6
- package/dist/card/final-answer-file.js +1 -1
- package/dist/card/run-renderer.d.ts +3 -3
- package/dist/card/run-renderer.js +114 -159
- package/dist/card/run-state.d.ts +1 -15
- package/dist/card/run-state.js +358 -292
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +21 -17
- package/dist/card/usage-format.d.ts +22 -7
- package/dist/card/usage-format.js +119 -100
- package/dist/cli.js +15 -3
- package/dist/clone.js +2 -2
- package/dist/common/text-width.js +36 -24
- package/dist/common/tool-summary.js +132 -90
- package/dist/config/codex-config.js +130 -106
- package/dist/config/index.d.ts +16 -2
- package/dist/config/index.js +23 -8
- package/dist/connector/index.js +6 -6
- package/dist/error-classification.js +3 -2
- package/dist/inbound/placeholder.d.ts +1 -1
- package/dist/inbound/placeholder.js +1 -1
- package/dist/index.js +387 -281
- package/dist/logger/index.js +6 -6
- package/dist/message-handlers.d.ts +0 -5
- package/dist/message-handlers.js +254 -237
- package/dist/model-catalog.d.ts +2 -2
- package/dist/model-catalog.js +3 -8
- package/dist/platform/identity.js +1 -1
- package/dist/platform/path.d.ts +1 -1
- package/dist/platform/path.js +1 -1
- package/dist/platform/sleep-blocker.js +1 -1
- package/dist/platform/terminator-win32.js +2 -1
- package/dist/router/commands/ls.d.ts +3 -0
- package/dist/router/commands/ls.js +256 -175
- package/dist/router/commands/order.js +184 -159
- package/dist/router/commands/resume.js +190 -152
- package/dist/router/commands/ws.js +219 -174
- package/dist/router/config/dsh.d.ts +4 -8
- package/dist/router/config/dsh.js +3 -10
- package/dist/router/config/kimi.js +1 -1
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/config-diff.js +1 -1
- package/dist/router/index.d.ts +56 -11
- package/dist/router/index.js +401 -391
- package/dist/runner/bash/runner.js +2 -4
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +6 -4
- package/dist/runner/codex/app-server/runner.d.ts +9 -2
- package/dist/runner/codex/app-server/runner.js +30 -25
- package/dist/runner/codex/app-server/translator.d.ts +2 -5
- package/dist/runner/codex/app-server/translator.js +291 -238
- package/dist/runner/common/acp/acp-approval.d.ts +1 -1
- package/dist/runner/common/acp/acp-approval.js +1 -1
- package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
- package/dist/runner/common/acp/base-acp-runner.js +26 -65
- package/dist/runner/common/acp/base-translator.d.ts +12 -2
- package/dist/runner/common/acp/base-translator.js +50 -26
- package/dist/runner/common/acp/protocol-types.d.ts +1 -1
- package/dist/runner/common/acp/protocol-types.js +2 -2
- package/dist/runner/common/connection-based-runner.d.ts +32 -1
- package/dist/runner/common/connection-based-runner.js +32 -2
- package/dist/runner/common/jsonl-stream.js +154 -187
- package/dist/runner/common/jsonrpc/client.d.ts +29 -0
- package/dist/runner/common/jsonrpc/client.js +43 -12
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
- package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
- package/dist/runner/common/spawning-runner.d.ts +6 -1
- package/dist/runner/common/spawning-runner.js +9 -4
- package/dist/runner/common/turn-started.d.ts +8 -0
- package/dist/runner/common/turn-started.js +11 -0
- package/dist/runner/dsh/approval.js +84 -51
- package/dist/runner/dsh/catalog-probe.d.ts +17 -0
- package/dist/runner/dsh/catalog-probe.js +10 -0
- package/dist/runner/dsh/client.d.ts +7 -11
- package/dist/runner/dsh/client.js +356 -289
- package/dist/runner/dsh/exit-cancel.d.ts +2 -2
- package/dist/runner/dsh/exit-cancel.js +2 -2
- package/dist/runner/dsh/inflight.d.ts +1 -1
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.js +28 -8
- package/dist/runner/dsh/runner.d.ts +44 -4
- package/dist/runner/dsh/runner.js +296 -199
- package/dist/runner/dsh/translator.d.ts +1 -31
- package/dist/runner/dsh/translator.js +180 -178
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +1 -2
- package/dist/runner/kimi/acp/runner.js +82 -46
- package/dist/runner/kimi/acp/translator.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +3 -4
- package/dist/runner/opencode/acp/error-monitor.js +5 -5
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/pi/rpc/client.js +4 -13
- package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
- package/dist/runner/pi/rpc/protocol-types.js +1 -1
- package/dist/runner/pi/rpc/runner.d.ts +3 -0
- package/dist/runner/pi/rpc/runner.js +27 -42
- package/dist/runner/pi/rpc/translator.js +61 -60
- package/dist/runner/types.d.ts +2 -2
- package/dist/runner/types.js +1 -1
- package/dist/session/claude/sessions.d.ts +45 -0
- package/dist/session/claude/sessions.js +160 -122
- package/dist/session/codex/rollout-reader.js +181 -182
- package/dist/session/common/throw-session-list-error.d.ts +10 -0
- package/dist/session/common/throw-session-list-error.js +16 -0
- package/dist/session/dsh/sessions.js +291 -235
- package/dist/session/kimi/sessions.d.ts +40 -8
- package/dist/session/kimi/sessions.js +373 -301
- package/dist/session/opencode/sessions.d.ts +142 -1
- package/dist/session/opencode/sessions.js +378 -321
- package/dist/session/pi/sessions.js +4 -5
- package/dist/session/session-store.d.ts +9 -1
- package/dist/session/session-store.js +99 -99
- package/dist/update/version-check.js +2 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.js +1 -1
- package/package.json +1 -1
|
@@ -192,6 +192,88 @@ function interruptedResult(sessionId) {
|
|
|
192
192
|
errorMessage: 'run stopped',
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
|
+
/** goal/change:权威变更,不等投影直接按事件更新(create/edit/pause/...)。 */
|
|
196
|
+
function goalChangeEvents(_runner, event, ctx) {
|
|
197
|
+
const parsed = goalFromChange(event.data);
|
|
198
|
+
if (parsed !== undefined && !sameGoal(parsed, ctx.goal.last)) {
|
|
199
|
+
ctx.goal.last = parsed;
|
|
200
|
+
return [goalStateEvent(parsed, true)];
|
|
201
|
+
}
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
/** approval/decided:审批回流,清审批卡。 */
|
|
205
|
+
function approvalDecidedEvents(runner, event) {
|
|
206
|
+
const data = event.data;
|
|
207
|
+
if (!data.id)
|
|
208
|
+
return [];
|
|
209
|
+
runner.approvalTracker.onDecided(data.id);
|
|
210
|
+
return [runner.approvalResolved(data.id)];
|
|
211
|
+
}
|
|
212
|
+
/** 终态分支查表:命中即返回(不再落到 translator)。 */
|
|
213
|
+
const SESSION_EVENT_TERMINAL_HANDLERS = new Map([
|
|
214
|
+
['goal/change', goalChangeEvents],
|
|
215
|
+
['approval/decided', approvalDecidedEvents],
|
|
216
|
+
]);
|
|
217
|
+
/** tool/call:先记账(审批卡命令展示用),随后仍落到 translator 出工具行。 */
|
|
218
|
+
function recordToolCall(toolCalls, event) {
|
|
219
|
+
const data = event.data;
|
|
220
|
+
if (data.callId && data.name) {
|
|
221
|
+
toolCalls.set(data.callId, {
|
|
222
|
+
name: data.name,
|
|
223
|
+
arguments: data.arguments ?? '',
|
|
224
|
+
...(data.turn === undefined ? {} : { turn: data.turn }),
|
|
225
|
+
});
|
|
226
|
+
if (toolCalls.size > 100) {
|
|
227
|
+
const oldest = toolCalls.keys().next().value;
|
|
228
|
+
if (oldest !== undefined)
|
|
229
|
+
toolCalls.delete(oldest);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** result 收尾:后台任务名单 + goal 收尾复核;返回要插到 result 前的事件。 */
|
|
234
|
+
async function resultFinalizeEvents(runner, ctx) {
|
|
235
|
+
const events = [];
|
|
236
|
+
for (const jobsEvent of await runner.collectBackgroundJobs(ctx.sessionId, ctx.abort.signal)) {
|
|
237
|
+
events.push(jobsEvent);
|
|
238
|
+
}
|
|
239
|
+
// 收尾复核:这一轮结束时 goal 是否仍 active,决定卡片能不能收尾。
|
|
240
|
+
// 必须现读宿主,不能沿用过程中的最后一次观测——goal 可能在本轮
|
|
241
|
+
// 最后一个 step 才被 complete,也可能刚被 driver 置为 blocked。
|
|
242
|
+
const finalGoal = await runner.client.goalState(ctx.sessionId, {
|
|
243
|
+
signal: ctx.signal,
|
|
244
|
+
timeoutMs: GOAL_PROBE_TIMEOUT_MS,
|
|
245
|
+
});
|
|
246
|
+
if (!finalGoal.known) {
|
|
247
|
+
// 收尾读不到:seed 已报过 unknown 就不重复发,否则把状态降级
|
|
248
|
+
// 交给卡片(卡片据此不再宣布完成)。
|
|
249
|
+
if (ctx.goal.known) {
|
|
250
|
+
ctx.goal.known = false;
|
|
251
|
+
events.push(goalStateEvent(null, false));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else if (!ctx.goal.known || !sameGoal(finalGoal.goal, ctx.goal.last)) {
|
|
255
|
+
ctx.goal.known = true;
|
|
256
|
+
ctx.goal.last = finalGoal.goal;
|
|
257
|
+
events.push(goalStateEvent(finalGoal.goal, true));
|
|
258
|
+
}
|
|
259
|
+
return events;
|
|
260
|
+
}
|
|
261
|
+
/** translator 循环:result 事件插入收尾事件并提前返回。 */
|
|
262
|
+
async function collectAgentEvents(runner, event, ctx) {
|
|
263
|
+
const events = [];
|
|
264
|
+
for (const ev of ctx.translator.eventToAgentEvents(event, ctx.sessionId)) {
|
|
265
|
+
if (ev.type === 'result') {
|
|
266
|
+
if (!ctx.abort.signal.aborted) {
|
|
267
|
+
for (const finalized of await resultFinalizeEvents(runner, ctx))
|
|
268
|
+
events.push(finalized);
|
|
269
|
+
}
|
|
270
|
+
events.push(attachContextLimit(ev, ctx.live.contextLimit));
|
|
271
|
+
return events;
|
|
272
|
+
}
|
|
273
|
+
events.push(ev);
|
|
274
|
+
}
|
|
275
|
+
return events;
|
|
276
|
+
}
|
|
195
277
|
export class DshRunner {
|
|
196
278
|
kind = 'dsh';
|
|
197
279
|
/**
|
|
@@ -432,41 +514,6 @@ export class DshRunner {
|
|
|
432
514
|
yield interruptedResult(sessionId);
|
|
433
515
|
return;
|
|
434
516
|
}
|
|
435
|
-
// provider/模型/推理强度对齐:会话首次 run 时 selectModel 一次
|
|
436
|
-
// (该调用同时写服务端全局默认,故不重复写);同一 session 后续 run
|
|
437
|
-
// 跳过。显式配置 provider 时失败必须 fail closed:静默回退到其他
|
|
438
|
-
// provider 会让用户以为切换成功,实际模型与账单都错。
|
|
439
|
-
if ((this.configuredModel || this.configuredProvider) &&
|
|
440
|
-
!this.alignedSessions.has(sessionId)) {
|
|
441
|
-
try {
|
|
442
|
-
const selection = await this.resolveModelSelection(abort.signal);
|
|
443
|
-
await this.client.selectModel({ sessionId, ...selection }, { signal: abort.signal });
|
|
444
|
-
this.alignedSessions.add(sessionId);
|
|
445
|
-
}
|
|
446
|
-
catch (err) {
|
|
447
|
-
if (abort.signal.aborted) {
|
|
448
|
-
getLogger().info(`[dsh] run stopped during selectModel session=${sessionId}`);
|
|
449
|
-
terminalEmitted = true;
|
|
450
|
-
yield interruptedResult(sessionId);
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
if (this.configuredProvider) {
|
|
454
|
-
throw new Error(`Provider "${this.configuredProvider}" 的模型对齐失败:${err.message}。` +
|
|
455
|
-
'请在 DSH Web UI → 设置 → Models 检查该 Provider 的凭据与模型目录', { cause: err });
|
|
456
|
-
}
|
|
457
|
-
getLogger().warn(`[dsh] selectModel failed for session ${sessionId} (run continues): ${err.message}`);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
// 权限预设对齐:与模型对齐同语义,同一 session 只执行一次
|
|
461
|
-
// `/permission <preset>`。失败必须 fail closed——用户明确选了安全策略,
|
|
462
|
-
// 不能在错误的权限下继续跑 prompt。
|
|
463
|
-
await this.alignPermissionPreset(sessionId, abort.signal);
|
|
464
|
-
// Abort 可能在上述 unary 完成与检查之间到达:不进入 prompt/turn。
|
|
465
|
-
if (abort.signal.aborted) {
|
|
466
|
-
terminalEmitted = true;
|
|
467
|
-
yield interruptedResult(sessionId);
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
517
|
yield {
|
|
471
518
|
type: 'system',
|
|
472
519
|
subtype: 'init',
|
|
@@ -474,24 +521,18 @@ export class DshRunner {
|
|
|
474
521
|
cwd: opts.cwd,
|
|
475
522
|
model: this.configuredModel ?? 'DSH',
|
|
476
523
|
};
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
// 只有确实存在 goal 才产事件:没有 goal 的会话保持事件序列不变(少一次
|
|
487
|
-
// 渲染,也不让"读不到"变成一张多出来的卡片事件)。
|
|
488
|
-
if (seedGoal.goal)
|
|
489
|
-
yield goalStateEvent(seedGoal.goal, true);
|
|
524
|
+
// 准备阶段(模型对齐 + 权限预设对齐 + goal seed + abort 检查)抽到
|
|
525
|
+
// prepareTurn:对齐失败 fail closed(显式 provider 时 throw,由外层 catch
|
|
526
|
+
// 转 error result);被 stop 时返回 interrupted 形态,这里统一 yield
|
|
527
|
+
// interrupted result 收尾。
|
|
528
|
+
const prepared = await this.prepareTurn(sessionId, abort);
|
|
529
|
+
if (!prepared.ok) {
|
|
530
|
+
terminalEmitted = true;
|
|
531
|
+
yield interruptedResult(sessionId);
|
|
532
|
+
return;
|
|
490
533
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
// 「没有 goal」写成「已完成」——正是 GoalStateEvent 契约禁止的推断。
|
|
494
|
-
yield goalStateEvent(null, false);
|
|
534
|
+
for (const goalEvent of prepared.goalEvents) {
|
|
535
|
+
yield goalEvent;
|
|
495
536
|
}
|
|
496
537
|
try {
|
|
497
538
|
const submitted = await this.submitPrompt(sessionId, opts.cwd, message, abort.signal);
|
|
@@ -504,7 +545,7 @@ export class DshRunner {
|
|
|
504
545
|
return;
|
|
505
546
|
}
|
|
506
547
|
try {
|
|
507
|
-
for await (const ev of this.runTurn(sessionId, opts.cwd, submitted.prePromptSeq, promptRequestId, abort, { last: lastGoal, known:
|
|
548
|
+
for await (const ev of this.runTurn(sessionId, opts.cwd, submitted.prePromptSeq, promptRequestId, abort, { last: prepared.lastGoal, known: prepared.goalKnown })) {
|
|
508
549
|
yield ev;
|
|
509
550
|
if (ev.type === 'result') {
|
|
510
551
|
terminalEmitted = true;
|
|
@@ -573,10 +614,80 @@ export class DshRunner {
|
|
|
573
614
|
};
|
|
574
615
|
}
|
|
575
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* 准备一个 turn(run() 的准备阶段):模型对齐 + 权限预设对齐 + goal seed 探测。
|
|
619
|
+
*
|
|
620
|
+
* 红线(逐条保留):
|
|
621
|
+
* - 对齐失败 fail closed:显式配置 provider 时 selectModel 失败必须 throw
|
|
622
|
+
* (静默回退到其他 provider 会让用户以为切换成功,实际模型与账单都错);
|
|
623
|
+
* 权限预设失败由 alignPermissionPreset throw(用户明确选了安全策略,不能在
|
|
624
|
+
* 错误的权限下继续跑 prompt)。
|
|
625
|
+
* - 准备期间被 stop(abort)→ 返回 { ok:false },由调用方 yield interrupted
|
|
626
|
+
* result 收尾。
|
|
627
|
+
* - goal seed 只探测不推断:读不到按 unknown 上报,不冒充「没有 goal」。
|
|
628
|
+
*/
|
|
629
|
+
async prepareTurn(sessionId, abort) {
|
|
630
|
+
// provider/模型/推理强度对齐:会话首次 run 时 selectModel 一次
|
|
631
|
+
// (该调用同时写服务端全局默认,故不重复写);同一 session 后续 run
|
|
632
|
+
// 跳过。显式配置 provider 时失败必须 fail closed:静默回退到其他
|
|
633
|
+
// provider 会让用户以为切换成功,实际模型与账单都错。
|
|
634
|
+
if ((this.configuredModel || this.configuredProvider) && !this.alignedSessions.has(sessionId)) {
|
|
635
|
+
try {
|
|
636
|
+
const selection = await this.resolveModelSelection(abort.signal);
|
|
637
|
+
await this.client.selectModel({ sessionId, ...selection }, { signal: abort.signal });
|
|
638
|
+
this.alignedSessions.add(sessionId);
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
if (abort.signal.aborted) {
|
|
642
|
+
getLogger().info(`[dsh] run stopped during selectModel session=${sessionId}`);
|
|
643
|
+
return { ok: false };
|
|
644
|
+
}
|
|
645
|
+
if (this.configuredProvider) {
|
|
646
|
+
throw new Error(`Provider "${this.configuredProvider}" 的模型对齐失败:${err.message}。` +
|
|
647
|
+
'请在 DSH Web UI → 设置 → Models 检查该 Provider 的凭据与模型目录', { cause: err });
|
|
648
|
+
}
|
|
649
|
+
getLogger().warn(`[dsh] selectModel failed for session ${sessionId} (run continues): ${err.message}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// 权限预设对齐:与模型对齐同语义,同一 session 只执行一次
|
|
653
|
+
// `/permission <preset>`。失败必须 fail closed——用户明确选了安全策略,
|
|
654
|
+
// 不能在错误的权限下继续跑 prompt。
|
|
655
|
+
await this.alignPermissionPreset(sessionId, abort.signal);
|
|
656
|
+
// Abort 可能在上述 unary 完成与检查之间到达:不进入 prompt/turn。
|
|
657
|
+
if (abort.signal.aborted) {
|
|
658
|
+
return { ok: false };
|
|
659
|
+
}
|
|
660
|
+
// Goal seed:卡片从第一帧就知道这个会话有没有长期目标。放在 init 之后、
|
|
661
|
+
// prompt 之前——用户发消息时若是 queued,卡片会长时间停在「运行中」,
|
|
662
|
+
// 这期间"目标是什么、第几轮"是唯一能解释"为什么还不停"的信息。
|
|
663
|
+
const seedGoal = await this.client.goalState(sessionId, {
|
|
664
|
+
signal: abort.signal,
|
|
665
|
+
timeoutMs: GOAL_PROBE_TIMEOUT_MS,
|
|
666
|
+
});
|
|
667
|
+
const lastGoal = seedGoal.known ? seedGoal.goal : undefined;
|
|
668
|
+
const goalEvents = [];
|
|
669
|
+
if (seedGoal.known) {
|
|
670
|
+
// 只有确实存在 goal 才产事件:没有 goal 的会话保持事件序列不变(少一次
|
|
671
|
+
// 渲染,也不让"读不到"变成一张多出来的卡片事件)。
|
|
672
|
+
if (seedGoal.goal)
|
|
673
|
+
goalEvents.push(goalStateEvent(seedGoal.goal, true));
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
// 读不到 ≠ 没有目标。必须显式把 unknown 交给卡片,否则收尾只能按
|
|
677
|
+
// 「没有 goal」写成「已完成」——正是 GoalStateEvent 契约禁止的推断。
|
|
678
|
+
goalEvents.push(goalStateEvent(null, false));
|
|
679
|
+
}
|
|
680
|
+
return { ok: true, lastGoal, goalKnown: seedGoal.known, goalEvents };
|
|
681
|
+
}
|
|
576
682
|
/**
|
|
577
683
|
* Consume one turn from both transports. `session/follow` owns transcript
|
|
578
684
|
* events; `$events` owns the answerable approval waterfall. The merged
|
|
579
685
|
* stream keeps either transport from starving the other.
|
|
686
|
+
*
|
|
687
|
+
* 事件源分派:projection(goal + 状态条)→ handleProjectionItem;
|
|
688
|
+
* $events waterfall(审批/提问)→ handleApprovalWaterfallItem;
|
|
689
|
+
* assistant-stream 直转发(translator 持有本 turn 的 usage 累计状态);
|
|
690
|
+
* session/follow 事件 → handleSessionEvent(含 result 收尾的 jobs + goal 复核)。
|
|
580
691
|
*/
|
|
581
692
|
async *runTurn(sessionId, cwd, prePromptSeq, promptRequestId, abort, goal) {
|
|
582
693
|
const streamAbort = new AbortController();
|
|
@@ -600,165 +711,41 @@ export class DshRunner {
|
|
|
600
711
|
for await (const item of mergeAsync(sources)) {
|
|
601
712
|
if (abort.signal.aborted)
|
|
602
713
|
return;
|
|
714
|
+
// 事件源 1:Host-wide 投影(goal + 状态条)。
|
|
603
715
|
if (item.kind === 'projection') {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (item.sessionId !== sessionId)
|
|
607
|
-
continue;
|
|
608
|
-
// goal 是会话级长期目标,单独成事件:卡片要在 agent 创建目标的当下
|
|
609
|
-
// 就显示它(否则用户只会在一小时后看到「完成」才发现有个 40 轮任务)。
|
|
610
|
-
if (item.key === 'goal') {
|
|
611
|
-
const parsed = goalFromProjection(item.value);
|
|
612
|
-
if (parsed !== undefined && !sameGoal(parsed, goal.last)) {
|
|
613
|
-
goal.last = parsed;
|
|
614
|
-
yield goalStateEvent(parsed, true);
|
|
615
|
-
}
|
|
616
|
-
continue;
|
|
617
|
-
}
|
|
618
|
-
// 只在消费字段真的变了时才产出事件:投影帧本身按 key 去重推送,
|
|
619
|
-
// 这里再挡掉"值没变"的重复帧。不需要时间节流——卡片更新本来就有
|
|
620
|
-
// 100ms 合批窗口,与正文 delta 共用同一条节流路径。
|
|
621
|
-
if (!applyProjection(live, item.key, item.value))
|
|
622
|
-
continue;
|
|
623
|
-
yield statusEventOf(live);
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
if (item.kind === 'ready') {
|
|
627
|
-
this.approvalChannelAvailable = true;
|
|
628
|
-
continue;
|
|
629
|
-
}
|
|
630
|
-
if (item.kind === 'unavailable') {
|
|
631
|
-
this.approvalChannelAvailable = false;
|
|
632
|
-
getLogger().warn(`[dsh] approval channel unavailable: ${item.message}`);
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
if (item.kind === 'request') {
|
|
636
|
-
this.approvalTracker.onWaterfall({
|
|
637
|
-
eventId: item.eventId,
|
|
638
|
-
clientId: item.clientId,
|
|
639
|
-
...(item.callId ? { callId: item.callId } : {}),
|
|
640
|
-
toolName: item.toolName,
|
|
641
|
-
...(item.reason ? { reason: item.reason } : {}),
|
|
642
|
-
});
|
|
643
|
-
continue;
|
|
644
|
-
}
|
|
645
|
-
if (item.kind === 'question') {
|
|
646
|
-
// 同一 waterfall 在重连后会带新的 clientId 重投:覆盖写回,卡片的
|
|
647
|
-
// requestId(eventId)不变,coordinator 按重投语义刷新视图。
|
|
648
|
-
this.pendingQuestions.set(item.eventId, {
|
|
649
|
-
clientId: item.clientId,
|
|
650
|
-
questions: item.questions,
|
|
651
|
-
});
|
|
652
|
-
getLogger().info(`[dsh] user question surfaced eventId=${item.eventId} questions=${item.questions.length}`);
|
|
653
|
-
yield this.buildQuestionRequested(sessionId, item.eventId, item.questions);
|
|
654
|
-
continue;
|
|
655
|
-
}
|
|
656
|
-
if (item.kind === 'cancelled') {
|
|
657
|
-
// 提问与审批共用 cancel 帧:提问在别处(DSH Web UI)被回答或超时后
|
|
658
|
-
// 也要清掉飞书卡片,否则上面还留着可点但已无 responder 的问题区。
|
|
659
|
-
if (this.pendingQuestions.delete(item.eventId)) {
|
|
660
|
-
getLogger().info(`[dsh] user question resolved elsewhere eventId=${item.eventId}`);
|
|
661
|
-
yield this.approvalResolved(item.eventId);
|
|
662
|
-
continue;
|
|
716
|
+
for (const ev of this.handleProjectionItem(item, sessionId, goal, live)) {
|
|
717
|
+
yield ev;
|
|
663
718
|
}
|
|
664
|
-
const binding = this.approvalTracker.onCancelled(item.eventId);
|
|
665
|
-
if (binding)
|
|
666
|
-
yield this.approvalResolved(binding.audit.approvalId);
|
|
667
719
|
continue;
|
|
668
720
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
getLogger().warn(`[dsh] approval requested approvalId=${item.approvalId} tool=${item.toolName}; live channel unavailable — falling back to Web UI`);
|
|
679
|
-
yield this.approvalFallbackText(item.toolName);
|
|
680
|
-
continue;
|
|
721
|
+
// 事件源 2:$events 审批/提问 waterfall。
|
|
722
|
+
if (item.kind === 'ready' ||
|
|
723
|
+
item.kind === 'unavailable' ||
|
|
724
|
+
item.kind === 'request' ||
|
|
725
|
+
item.kind === 'question' ||
|
|
726
|
+
item.kind === 'cancelled' ||
|
|
727
|
+
item.kind === 'approval') {
|
|
728
|
+
for (const ev of this.handleApprovalWaterfallItem(item, sessionId, cwd, emittedApprovals)) {
|
|
729
|
+
yield ev;
|
|
681
730
|
}
|
|
682
|
-
if (emittedApprovals.has(item.approvalId))
|
|
683
|
-
continue;
|
|
684
|
-
emittedApprovals.add(item.approvalId);
|
|
685
|
-
yield this.buildApprovalRequested(sessionId, audit, binding, cwd);
|
|
686
731
|
continue;
|
|
687
732
|
}
|
|
733
|
+
// 事件源 3a:assistant 流式帧(translator 持有本 turn 的 usage 累计状态,
|
|
734
|
+
// 必须与 3b 共用同一实例)。
|
|
688
735
|
if (item.kind === 'assistant-stream') {
|
|
689
736
|
for (const ev of translator.streamFrameToAgentEvents(item.frame, sessionId)) {
|
|
690
737
|
yield ev;
|
|
691
738
|
}
|
|
692
739
|
continue;
|
|
693
740
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (parsed !== undefined && !sameGoal(parsed, goal.last)) {
|
|
700
|
-
goal.last = parsed;
|
|
701
|
-
yield goalStateEvent(parsed, true);
|
|
702
|
-
}
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
if (event.type === 'tool/call') {
|
|
706
|
-
const data = event.data;
|
|
707
|
-
if (data.callId && data.name) {
|
|
708
|
-
this.toolCalls.set(data.callId, {
|
|
709
|
-
name: data.name,
|
|
710
|
-
arguments: data.arguments ?? '',
|
|
711
|
-
...(data.turn === undefined ? {} : { turn: data.turn }),
|
|
712
|
-
});
|
|
713
|
-
if (this.toolCalls.size > 100) {
|
|
714
|
-
const oldest = this.toolCalls.keys().next().value;
|
|
715
|
-
if (oldest !== undefined)
|
|
716
|
-
this.toolCalls.delete(oldest);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
// 记账后继续往下走:tool/call 同时是卡片工具行的来源(translator 翻
|
|
720
|
-
// 译成 assistant.tool_use)。早期版本在这里 `continue`,把调用轨迹整个
|
|
721
|
-
// 吞掉——卡片因此只有正文、没有任何工具行。
|
|
722
|
-
}
|
|
723
|
-
if (event.type === 'approval/decided') {
|
|
724
|
-
const data = event.data;
|
|
725
|
-
if (!data.id)
|
|
726
|
-
continue;
|
|
727
|
-
this.approvalTracker.onDecided(data.id);
|
|
728
|
-
yield this.approvalResolved(data.id);
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
for (const ev of translator.eventToAgentEvents(event, sessionId)) {
|
|
732
|
-
if (ev.type === 'result') {
|
|
733
|
-
if (!abort.signal.aborted) {
|
|
734
|
-
for (const jobsEvent of await this.collectBackgroundJobs(sessionId, abort.signal)) {
|
|
735
|
-
yield jobsEvent;
|
|
736
|
-
}
|
|
737
|
-
// 收尾复核:这一轮结束时 goal 是否仍 active,决定卡片能不能收尾。
|
|
738
|
-
// 必须现读宿主,不能沿用过程中的最后一次观测——goal 可能在本轮
|
|
739
|
-
// 最后一个 step 才被 complete,也可能刚被 driver 置为 blocked。
|
|
740
|
-
const finalGoal = await this.client.goalState(sessionId, {
|
|
741
|
-
signal,
|
|
742
|
-
timeoutMs: GOAL_PROBE_TIMEOUT_MS,
|
|
743
|
-
});
|
|
744
|
-
if (!finalGoal.known) {
|
|
745
|
-
// 收尾读不到:seed 已报过 unknown 就不重复发,否则把状态降级
|
|
746
|
-
// 交给卡片(卡片据此不再宣布完成)。
|
|
747
|
-
if (goal.known) {
|
|
748
|
-
goal.known = false;
|
|
749
|
-
yield goalStateEvent(null, false);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
else if (!goal.known || !sameGoal(finalGoal.goal, goal.last)) {
|
|
753
|
-
goal.known = true;
|
|
754
|
-
goal.last = finalGoal.goal;
|
|
755
|
-
yield goalStateEvent(finalGoal.goal, true);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
yield attachContextLimit(ev, live.contextLimit);
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
741
|
+
// 事件源 3b:session/follow 事件(正文/工具行/result 收尾,含 jobs +
|
|
742
|
+
// goal 复核)。result 之后的收尾事件由 handler 按序返回,见到 result
|
|
743
|
+
// 即结束本 turn(与原 yield-then-return 逐字等价)。
|
|
744
|
+
const eventEvents = await this.handleSessionEvent(item.event, sessionId, translator, goal, live, abort, signal);
|
|
745
|
+
for (const ev of eventEvents) {
|
|
761
746
|
yield ev;
|
|
747
|
+
if (ev.type === 'result')
|
|
748
|
+
return;
|
|
762
749
|
}
|
|
763
750
|
}
|
|
764
751
|
}
|
|
@@ -769,6 +756,116 @@ export class DshRunner {
|
|
|
769
756
|
this.toolCalls.clear();
|
|
770
757
|
}
|
|
771
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* 事件源 handler:Host-wide 投影(goal + 状态条)。
|
|
761
|
+
* 返回要回放的事件(可能为空——未变化/非本 session 的帧不产事件)。
|
|
762
|
+
*/
|
|
763
|
+
handleProjectionItem(item, sessionId, goal, live) {
|
|
764
|
+
// 全 Host 流:只认本 session。未知 key 直接忽略(投影是能力发现式
|
|
765
|
+
// 扩展的,不能因为多了个 key 就报错)。
|
|
766
|
+
if (item.sessionId !== sessionId)
|
|
767
|
+
return [];
|
|
768
|
+
// goal 是会话级长期目标,单独成事件:卡片要在 agent 创建目标的当下
|
|
769
|
+
// 就显示它(否则用户只会在一小时后看到「完成」才发现有个 40 轮任务)。
|
|
770
|
+
if (item.key === 'goal') {
|
|
771
|
+
const parsed = goalFromProjection(item.value);
|
|
772
|
+
if (parsed !== undefined && !sameGoal(parsed, goal.last)) {
|
|
773
|
+
goal.last = parsed;
|
|
774
|
+
return [goalStateEvent(parsed, true)];
|
|
775
|
+
}
|
|
776
|
+
return [];
|
|
777
|
+
}
|
|
778
|
+
// 只在消费字段真的变了时才产出事件:投影帧本身按 key 去重推送,
|
|
779
|
+
// 这里再挡掉"值没变"的重复帧。不需要时间节流——卡片更新本来就有
|
|
780
|
+
// 100ms 合批窗口,与正文 delta 共用同一条节流路径。
|
|
781
|
+
if (!applyProjection(live, item.key, item.value))
|
|
782
|
+
return [];
|
|
783
|
+
return [statusEventOf(live)];
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* 事件源 handler:$events 审批/提问 waterfall(channel 状态、审批 waterfall
|
|
787
|
+
* 登记、提问卡、cancel 回流、审批卡)。
|
|
788
|
+
* 返回要回放的事件;channel 状态类帧(ready/unavailable/request)只改内部
|
|
789
|
+
* 状态、不产事件。
|
|
790
|
+
*/
|
|
791
|
+
handleApprovalWaterfallItem(item, sessionId, cwd, emittedApprovals) {
|
|
792
|
+
switch (item.kind) {
|
|
793
|
+
case 'ready':
|
|
794
|
+
this.approvalChannelAvailable = true;
|
|
795
|
+
return [];
|
|
796
|
+
case 'unavailable':
|
|
797
|
+
this.approvalChannelAvailable = false;
|
|
798
|
+
getLogger().warn(`[dsh] approval channel unavailable: ${item.message}`);
|
|
799
|
+
return [];
|
|
800
|
+
case 'request':
|
|
801
|
+
this.approvalTracker.onWaterfall({
|
|
802
|
+
eventId: item.eventId,
|
|
803
|
+
clientId: item.clientId,
|
|
804
|
+
...(item.callId ? { callId: item.callId } : {}),
|
|
805
|
+
toolName: item.toolName,
|
|
806
|
+
...(item.reason ? { reason: item.reason } : {}),
|
|
807
|
+
});
|
|
808
|
+
return [];
|
|
809
|
+
case 'question': {
|
|
810
|
+
// 同一 waterfall 在重连后会带新的 clientId 重投:覆盖写回,卡片的
|
|
811
|
+
// requestId(eventId)不变,coordinator 按重投语义刷新视图。
|
|
812
|
+
this.pendingQuestions.set(item.eventId, {
|
|
813
|
+
clientId: item.clientId,
|
|
814
|
+
questions: item.questions,
|
|
815
|
+
});
|
|
816
|
+
getLogger().info(`[dsh] user question surfaced eventId=${item.eventId} questions=${item.questions.length}`);
|
|
817
|
+
return [this.buildQuestionRequested(sessionId, item.eventId, item.questions)];
|
|
818
|
+
}
|
|
819
|
+
case 'cancelled': {
|
|
820
|
+
// 提问与审批共用 cancel 帧:提问在别处(DSH Web UI)被回答或超时后
|
|
821
|
+
// 也要清掉飞书卡片,否则上面还留着可点但已无 responder 的问题区。
|
|
822
|
+
if (this.pendingQuestions.delete(item.eventId)) {
|
|
823
|
+
getLogger().info(`[dsh] user question resolved elsewhere eventId=${item.eventId}`);
|
|
824
|
+
return [this.approvalResolved(item.eventId)];
|
|
825
|
+
}
|
|
826
|
+
const binding = this.approvalTracker.onCancelled(item.eventId);
|
|
827
|
+
return binding ? [this.approvalResolved(binding.audit.approvalId)] : [];
|
|
828
|
+
}
|
|
829
|
+
case 'approval': {
|
|
830
|
+
const audit = {
|
|
831
|
+
approvalId: item.approvalId,
|
|
832
|
+
toolName: item.toolName,
|
|
833
|
+
...(item.callId ? { callId: item.callId } : {}),
|
|
834
|
+
...(item.reason ? { reason: item.reason } : {}),
|
|
835
|
+
};
|
|
836
|
+
const binding = this.approvalTracker.onAuditAsked(audit);
|
|
837
|
+
if (!this.approvalChannelAvailable) {
|
|
838
|
+
getLogger().warn(`[dsh] approval requested approvalId=${item.approvalId} tool=${item.toolName}; live channel unavailable — falling back to Web UI`);
|
|
839
|
+
return [this.approvalFallbackText(item.toolName)];
|
|
840
|
+
}
|
|
841
|
+
if (emittedApprovals.has(item.approvalId))
|
|
842
|
+
return [];
|
|
843
|
+
emittedApprovals.add(item.approvalId);
|
|
844
|
+
return [this.buildApprovalRequested(sessionId, audit, binding, cwd)];
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* 事件源 handler:`session/follow` 会话事件。
|
|
850
|
+
* - goal/change:权威变更,不等投影直接更新;
|
|
851
|
+
* - tool/call:先记账(审批卡命令展示用)再落入 translator(卡片工具行来源);
|
|
852
|
+
* - approval/decided:审批回流,清审批卡;
|
|
853
|
+
* - 其余事件经 translator 翻译;result 事件先做收尾(后台任务名单 + goal
|
|
854
|
+
* 复核)再随 result 一起返回。
|
|
855
|
+
*/
|
|
856
|
+
async handleSessionEvent(event, sessionId, translator, goal, live, abort, signal) {
|
|
857
|
+
const ctx = { sessionId, translator, goal, live, abort, signal };
|
|
858
|
+
const terminal = SESSION_EVENT_TERMINAL_HANDLERS.get(event.type);
|
|
859
|
+
if (terminal)
|
|
860
|
+
return terminal(this, event, ctx);
|
|
861
|
+
if (event.type === 'tool/call') {
|
|
862
|
+
// 记账后继续往下走:tool/call 同时是卡片工具行的来源(translator 翻
|
|
863
|
+
// 译成 assistant.tool_use)。早期版本在这里 `continue`,把调用轨迹整个
|
|
864
|
+
// 吞掉——卡片因此只有正文、没有任何工具行。
|
|
865
|
+
recordToolCall(this.toolCalls, event);
|
|
866
|
+
}
|
|
867
|
+
return collectAgentEvents(this, event, ctx);
|
|
868
|
+
}
|
|
772
869
|
/**
|
|
773
870
|
* 收尾时仍在跑的后台任务。
|
|
774
871
|
*
|
|
@@ -1409,7 +1506,7 @@ export class DshRunner {
|
|
|
1409
1506
|
*
|
|
1410
1507
|
* turn 跑在常驻共享的 `dsh web` host 里,不在本进程的子进程树上;所以
|
|
1411
1508
|
* "杀自己的子进程"这套退出清理对它无效,桥接一退出,turn 继续无人接收地跑
|
|
1412
|
-
*
|
|
1509
|
+
* (孤儿 turn 在桥接死后可能再跑数小时)。
|
|
1413
1510
|
* 这里复用进程级退出分发器,用同步 curl 发一次 `session/cancel`。
|
|
1414
1511
|
*/
|
|
1415
1512
|
registerExitHandlers() {
|
|
@@ -26,39 +26,9 @@
|
|
|
26
26
|
import type { AgentEvent, ResultEvent } from '../types.js';
|
|
27
27
|
import type { DshAssistantStreamFrame, DshSessionEvent, DshTokenUsage } from '../../dsh/types.js';
|
|
28
28
|
export declare class DshTranslator {
|
|
29
|
-
|
|
30
|
-
private turnUsage;
|
|
31
|
-
/** 末次采样的 usage:上下文水位,或只有 live chunk 时的兜底值。 */
|
|
32
|
-
private lastUsage?;
|
|
33
|
-
/** 是否收到过 durable assistant/message.usage(唯一累加来源)。 */
|
|
34
|
-
private hasMessageUsage;
|
|
35
|
-
/** 是否只收到过 live usage chunk(无 durable message usage 时的兜底)。 */
|
|
36
|
-
private hasStreamUsage;
|
|
37
|
-
/** 当前 attempt 的 id(start 帧携带,chunk 帧也带;断开重连重放时不变)。 */
|
|
38
|
-
private attemptId;
|
|
39
|
-
/**
|
|
40
|
-
* 本 turn 末条 durable assistant 消息的正文(**完整**、未截断)。
|
|
41
|
-
*
|
|
42
|
-
* live chunk 是逐 delta 拼接、会被状态层的 MAX_TEXT_CHARS 窗口截断;
|
|
43
|
-
* durable `assistant/message` 一次就到齐全文。收 turn 时把它挂在 result 上,
|
|
44
|
-
* 卡片才能渲染完整回答(2026-11-01 输出块被截断缺陷)。
|
|
45
|
-
*/
|
|
46
|
-
private lastMessageText?;
|
|
29
|
+
private readonly state;
|
|
47
30
|
/** Translate one live assistant-stream chunk into AgentEvents. */
|
|
48
31
|
streamFrameToAgentEvents(frame: DshAssistantStreamFrame, _sessionId: string): AgentEvent[];
|
|
49
|
-
/**
|
|
50
|
-
* Record durable per-step usage — the accumulation source. assistant/message
|
|
51
|
-
* carries exactly one usage per model call and is seq-deduped by the client,
|
|
52
|
-
* so summing it gives the turn total without double counting.
|
|
53
|
-
*/
|
|
54
|
-
private recordMessageUsage;
|
|
55
|
-
/**
|
|
56
|
-
* Record a live usage chunk. Never summed: the same step also arrives as a
|
|
57
|
-
* durable assistant/message.usage, so summing both would double count. It only
|
|
58
|
-
* refreshes the watermark and covers a turn whose messages never persisted.
|
|
59
|
-
*/
|
|
60
|
-
private recordStreamUsage;
|
|
61
|
-
private chunkToAgentEvents;
|
|
62
32
|
/** Translate one session event into AgentEvents (may be empty). */
|
|
63
33
|
eventToAgentEvents(ev: DshSessionEvent, sessionId: string): AgentEvent[];
|
|
64
34
|
}
|