@zhushanwen/pi-subagent-workflow 7.3.4 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -12
- package/agents/analyst.md +61 -0
- package/agents/coder.md +70 -0
- package/agents/debugger.md +67 -0
- package/agents/doc-reviewer.md +3 -3
- package/agents/explorer.md +50 -18
- package/agents/general-purpose.md +19 -8
- package/agents/orchestrator.md +37 -32
- package/agents/planner.md +45 -11
- package/agents/researcher.md +53 -11
- package/agents/reviewer.md +74 -0
- package/package.json +1 -1
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/agent-registry.test.ts +13 -11
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
- package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
- package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
- package/src/execution/__tests__/crash-recovery.test.ts +8 -2
- package/src/execution/__tests__/delivery-methods.test.ts +385 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -72
- package/src/execution/__tests__/execution-record.test.ts +199 -0
- package/src/execution/__tests__/finalize-record.test.ts +170 -14
- package/src/execution/__tests__/format.test.ts +131 -7
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
- package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
- package/src/execution/__tests__/index-session-start.test.ts +257 -5
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/list-component.test.ts +59 -5
- package/src/execution/__tests__/list-fields.test.ts +109 -0
- package/src/execution/__tests__/model-resolver.test.ts +38 -1
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/notifier-flush.test.ts +209 -7
- package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
- package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
- package/src/execution/__tests__/record-store.test.ts +158 -52
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
- package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
- package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
- package/src/execution/__tests__/session-pending.test.ts +61 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
- package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
- package/src/execution/__tests__/spawn-args.test.ts +127 -49
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/status-refactor.test.ts +345 -0
- package/src/execution/__tests__/stdin-writer.test.ts +97 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +49 -11
- package/src/execution/__tests__/timeout-integration.test.ts +27 -13
- package/src/execution/__tests__/tool-action.test.ts +12 -10
- package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/worktree-manager.test.ts +292 -89
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +90 -13
- package/src/execution/host-mode.ts +1 -1
- package/src/execution/lifecycle-manager.ts +484 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +61 -16
- package/src/execution/model-resolver.ts +26 -5
- package/src/execution/notifier.ts +69 -12
- package/src/execution/pi-invocation.ts +21 -1
- package/src/execution/record-store.ts +554 -107
- package/src/execution/session-pending.ts +116 -45
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +290 -75
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/stdin-writer.ts +93 -7
- package/src/execution/stream-sink.ts +20 -3
- package/src/execution/subagent-service.ts +867 -138
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +204 -22
- package/src/execution/worktree-manager.ts +128 -49
- package/src/execution/worktree-registry.ts +13 -2
- package/src/index.ts +277 -19
- package/src/injectors/subagent-list-injector.ts +26 -8
- package/src/injectors/workflow-list-injector.ts +25 -8
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +18 -5
- package/src/interface/__tests__/tool-render.test.ts +15 -13
- package/src/interface/bg-notify-render.ts +32 -8
- package/src/interface/command-actions.ts +26 -7
- package/src/interface/commands.ts +21 -22
- package/src/interface/format.ts +44 -17
- package/src/interface/gui-mappers.ts +6 -8
- package/src/interface/helpers.ts +170 -10
- package/src/interface/list-component.ts +53 -14
- package/src/interface/subagent-actions.ts +235 -17
- package/src/interface/subagent-tool.ts +82 -17
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +11 -24
- package/src/interface/tool-workflow.ts +20 -35
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +1 -1
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
- package/src/orchestration/__tests__/args-validator.test.ts +1 -1
- package/src/orchestration/__tests__/config-loader.test.ts +38 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/skill-discovery.test.ts +157 -0
- package/src/orchestration/__tests__/test-mocks.ts +191 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
- package/src/orchestration/agent-opts-resolver.ts +4 -1
- package/src/orchestration/args-validator.ts +2 -2
- package/src/orchestration/config-loader.ts +30 -1
- package/src/orchestration/error-recovery.ts +133 -29
- package/src/orchestration/execute-agent-call.ts +31 -7
- package/src/orchestration/jsonl-run-store.ts +287 -40
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +135 -132
- package/src/orchestration/models/__tests__/trace.test.ts +408 -0
- package/src/orchestration/models/budget.ts +1 -1
- package/src/orchestration/models/run-runtime.ts +15 -17
- package/src/orchestration/models/run-spec.ts +2 -2
- package/src/orchestration/models/run-state.ts +3 -3
- package/src/orchestration/models/trace.ts +95 -15
- package/src/orchestration/models/types.ts +8 -9
- package/src/orchestration/models/workflow-run.ts +50 -71
- package/src/orchestration/models/workflow-script.ts +32 -1
- package/src/orchestration/skill-discovery.ts +30 -0
- package/src/orchestration/worker-handle.ts +1 -1
- package/src/orchestration/worker-host.ts +1 -1
- package/src/orchestration/worker-script-builder.ts +29 -10
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
- package/src/shared/__tests__/resource-discovery.test.ts +55 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +16 -0
- package/src/shared/resource-discovery.ts +147 -58
- package/src/shared/schema-jsonify.ts +53 -0
- package/workflows/README.md +4 -4
- package/agents/code-reviewer.md +0 -47
- package/agents/context-builder.md +0 -21
- package/agents/oracle.md +0 -34
- package/agents/worker.md +0 -20
|
@@ -14,6 +14,7 @@ import { visibleWidth } from "@earendil-works/pi-tui";
|
|
|
14
14
|
import { computeElapsedSeconds } from "../execution/execution-record.ts";
|
|
15
15
|
import type { SubagentService } from "../execution/subagent-service.ts";
|
|
16
16
|
import type { SubagentRecord } from "../execution/types.ts";
|
|
17
|
+
import { displayAgentName } from "../shared/agent-ref.ts";
|
|
17
18
|
import {
|
|
18
19
|
firstLine,
|
|
19
20
|
formatDisplayItem,
|
|
@@ -76,6 +77,15 @@ const DETAIL_LEN_PROBE_WIDTH = 9999;
|
|
|
76
77
|
const VERT_CENTER_DIVISOR = 2;
|
|
77
78
|
/** spinner 帧切换粒度(与 Date.now() 配合选帧)。 */
|
|
78
79
|
const SPINNER_FRAME_MS = 250;
|
|
80
|
+
/**
|
|
81
|
+
* [perf] 同帧 records 共享窗口(swf-perf-impl cleanup TC3/IF3/DM5)。
|
|
82
|
+
* 250ms 动画帧内 hasRunning 与 buildLines(含 detail 路径 children 计算)各自
|
|
83
|
+
* collectRecords(LIST_LIMIT) 全量合并扫描(内存 running + 磁盘 light 重建)——帧内合一。
|
|
84
|
+
* 取值 50:须 < store 事件 debounce 120ms(事件驱动渲染大概率 TTL 已过走新鲜扫描)
|
|
85
|
+
* 且 > 同 tick 内 hasRunning→render 微秒级间隔(命中复用)。最坏滞后 = 渲染合帧场景下
|
|
86
|
+
* 50ms 内旧快照一帧,下一 animTimer tick / 事件渲染自愈,对 250ms 动画无感知差异。
|
|
87
|
+
*/
|
|
88
|
+
const FRAME_TTL_MS = 50;
|
|
79
89
|
/** 顶框嵌入标题(分屏模式)。 */
|
|
80
90
|
const TITLE_SPLIT = "Subagents";
|
|
81
91
|
/** 分屏分区线左/右嵌入标题。 */
|
|
@@ -85,7 +95,8 @@ const TITLE_RIGHT = "Detail";
|
|
|
85
95
|
/**
|
|
86
96
|
* 全屏带框左右分屏 list 组件。
|
|
87
97
|
*
|
|
88
|
-
* 不缓存行(records
|
|
98
|
+
* 不缓存行(render 每次重建;records 经 collectRecordsFrame 帧缓存拉取——TTL 50ms 内
|
|
99
|
+
* 同帧共享一份,保证 store 变化后刷新由 TTL/事件 debounce 兜底,见 FRAME_TTL_MS)。
|
|
89
100
|
* 缓存的是「上次 render 的 width×rows」(用于 invalidate 后强制重建)。
|
|
90
101
|
*/
|
|
91
102
|
export class SubagentsListComponent implements Component {
|
|
@@ -94,6 +105,9 @@ export class SubagentsListComponent implements Component {
|
|
|
94
105
|
private closeFn: () => void = () => {};
|
|
95
106
|
/** 动画 timer 句柄(dispose 兜底清理)。 */
|
|
96
107
|
private animTimer: ReturnType<typeof setInterval> | undefined;
|
|
108
|
+
/** [perf] 帧缓存:TTL 内复用同一份 records(collectRecordsFrame,TC3/IF3/DM5)。 */
|
|
109
|
+
private frameRecords: SubagentRecord[] | undefined;
|
|
110
|
+
private frameAt = 0;
|
|
97
111
|
|
|
98
112
|
constructor(
|
|
99
113
|
private readonly service: SubagentService,
|
|
@@ -117,7 +131,29 @@ export class SubagentsListComponent implements Component {
|
|
|
117
131
|
|
|
118
132
|
/** 是否有 running record(动画 timer 据此决定是否刷新)。 */
|
|
119
133
|
hasRunning(): boolean {
|
|
120
|
-
return this.
|
|
134
|
+
return this.collectRecordsFrame().some((r) => r.status === "running");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* [perf] 帧缓存读取(TC3/IF3/DM5):TTL(FRAME_TTL_MS=50ms)内复用同一份 records,
|
|
139
|
+
* 过期/未缓存则调 service.collectRecords 刷新 {frameRecords, frameAt}。
|
|
140
|
+
* hasRunning / handleInput / buildLines / buildDetailContent(children) 四个消费点统一走这里。
|
|
141
|
+
* invalidate() 刻意不清帧缓存:invalidate 在 timer 序列(hasRunning→invalidate→requestRender)
|
|
142
|
+
* 的共享路径中间,清了共享即失效;新鲜度由 TTL 独立保证 + animTimer 250ms tick 双重兜底。
|
|
143
|
+
*/
|
|
144
|
+
private collectRecordsFrame(): SubagentRecord[] {
|
|
145
|
+
if (this.frameRecords !== undefined && Date.now() - this.frameAt <= FRAME_TTL_MS) {
|
|
146
|
+
return this.frameRecords;
|
|
147
|
+
}
|
|
148
|
+
const records = this.service.collectRecords(LIST_LIMIT);
|
|
149
|
+
this.frameRecords = records;
|
|
150
|
+
this.frameAt = Date.now();
|
|
151
|
+
return records;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** [perf] 选中项详情数据:优先全量(懒加载 + per-file 缓存),拿不到回退 light。 */
|
|
155
|
+
private fullRecordOf(r: SubagentRecord): SubagentRecord {
|
|
156
|
+
return this.service.getFullRecord(r.id) ?? r;
|
|
121
157
|
}
|
|
122
158
|
|
|
123
159
|
invalidate(): void {
|
|
@@ -138,16 +174,17 @@ export class SubagentsListComponent implements Component {
|
|
|
138
174
|
handleInput(data: string): void {
|
|
139
175
|
if (this.state.disposed) return;
|
|
140
176
|
|
|
141
|
-
const records = applyFilter(this.
|
|
177
|
+
const records = applyFilter(this.collectRecordsFrame(), this.state.filterText);
|
|
142
178
|
const selected = records[this.state.selectedIdx] ?? null;
|
|
143
179
|
// 详情翻屏上下文:视口高 = 右侧 body 高(内框高 - SPLIT_FIXED_LINES),
|
|
144
180
|
// contentLines = 详情内容总行数(含元数据/段头/eventLog/result/error,单一数据源)。
|
|
145
181
|
// 与 renderRightDetail 的 viewH + max 计算保持一致。
|
|
182
|
+
// [perf] 详情长度需重数据:选中项懒加载全量(带缓存)。
|
|
146
183
|
const innerRows = Math.max(MIN_INNER_ROWS, this.termRows() - PAD_ROWS);
|
|
147
184
|
const bodyH = Math.max(1, innerRows - SPLIT_FIXED_LINES);
|
|
148
185
|
const detailCtx: DetailKeyContext = {
|
|
149
186
|
viewportHeight: bodyH,
|
|
150
|
-
contentLines: selected ? this.detailContentLength(selected) : 0,
|
|
187
|
+
contentLines: selected ? this.detailContentLength(this.fullRecordOf(selected)) : 0,
|
|
151
188
|
};
|
|
152
189
|
|
|
153
190
|
const result = this.keyHandler(data, records, this.state, selected, this.service, detailCtx, this.notify);
|
|
@@ -192,7 +229,7 @@ export class SubagentsListComponent implements Component {
|
|
|
192
229
|
const innerWidth = Math.max(MIN_INNER_WIDTH, width - PAD_COLS);
|
|
193
230
|
const innerRows = Math.max(MIN_INNER_ROWS, rows - PAD_ROWS);
|
|
194
231
|
|
|
195
|
-
const allRecords = this.
|
|
232
|
+
const allRecords = this.collectRecordsFrame();
|
|
196
233
|
const records = applyFilter(allRecords, this.state.filterText);
|
|
197
234
|
|
|
198
235
|
// 先在内框尺寸下生成框行
|
|
@@ -302,9 +339,12 @@ export class SubagentsListComponent implements Component {
|
|
|
302
339
|
|
|
303
340
|
const selected = records[this.state.selectedIdx] ?? null;
|
|
304
341
|
const inDetail = this.state.detailMode; // 阶段 2:右侧滚动焦点
|
|
342
|
+
// [perf] 预览/详情需重数据(displayItems/turns/tokens/result):选中项按需全量加载
|
|
343
|
+
// (per-file 缓存,stat 校验);列表其余行保持 light(identity+状态),列表扫描不开销全量解析。
|
|
344
|
+
const selectedFull = selected ? this.fullRecordOf(selected) : null;
|
|
305
345
|
// 预构建详情内容(inDetail 时):分区线标题(detailScrollInfo 算长度)与右列(renderRightDetail 渲染)
|
|
306
346
|
// 共用同一份,避免每帧双倍构建(animTimer 250ms 触发,长 eventLog 下有感)。
|
|
307
|
-
const detailContent = inDetail &&
|
|
347
|
+
const detailContent = inDetail && selectedFull ? this.buildDetailContent(selectedFull, rightWidth) : null;
|
|
308
348
|
|
|
309
349
|
const lines: string[] = [];
|
|
310
350
|
|
|
@@ -313,7 +353,7 @@ export class SubagentsListComponent implements Component {
|
|
|
313
353
|
|
|
314
354
|
// filter 行(阶段 2 时隐藏 filter 提示,显示锚定提示)
|
|
315
355
|
const filterLine = inDetail
|
|
316
|
-
? t.fg("dim", `Pinned: ${selected?.agent
|
|
356
|
+
? t.fg("dim", `Pinned: ${selected?.agent ? displayAgentName(selected.agent) : ""} · Esc to return to list`)
|
|
317
357
|
: (this.state.filterText
|
|
318
358
|
? `${t.fg("dim", "filter: ")}${t.bold(this.state.filterText)}${t.fg("accent", "_")}`
|
|
319
359
|
: `${t.fg("dim", "filter: ")}${t.fg("accent", "_")}`);
|
|
@@ -322,7 +362,7 @@ export class SubagentsListComponent implements Component {
|
|
|
322
362
|
// 分区线(嵌入左/右标题,分段着色)
|
|
323
363
|
const leftTitleStyled = t.fg("accent", t.bold(` ${TITLE_LEFT} `));
|
|
324
364
|
const rightTitleStyled = inDetail
|
|
325
|
-
? t.fg("accent", t.bold(` ${TITLE_RIGHT}${this.detailScrollInfo(
|
|
365
|
+
? t.fg("accent", t.bold(` ${TITLE_RIGHT}${this.detailScrollInfo(selectedFull, bodyH, detailContent?.length)} `))
|
|
326
366
|
: t.fg("accent", t.bold(` ${TITLE_RIGHT} `));
|
|
327
367
|
lines.push(
|
|
328
368
|
this.b("├") + segFillColored(leftTitleStyled, this.dash(), leftWidth)
|
|
@@ -346,8 +386,8 @@ export class SubagentsListComponent implements Component {
|
|
|
346
386
|
));
|
|
347
387
|
leftLines = this.renderLeftColumn(records, leftWidth, leftStart, bodyH);
|
|
348
388
|
rightLines = inDetail
|
|
349
|
-
? this.renderRightDetail(
|
|
350
|
-
: this.renderRightPreview(
|
|
389
|
+
? this.renderRightDetail(selectedFull, rightWidth, bodyH, detailContent)
|
|
390
|
+
: this.renderRightPreview(selectedFull, rightWidth, bodyH);
|
|
351
391
|
}
|
|
352
392
|
const bodyRows = Math.max(leftLines.length, rightLines.length, bodyH);
|
|
353
393
|
for (let i = 0; i < bodyRows; i++) {
|
|
@@ -421,7 +461,7 @@ export class SubagentsListComponent implements Component {
|
|
|
421
461
|
const depthTag = r.depth > 0 ? ` ${t.fg("dim", `[L${r.depth + 1}]`)}` : "";
|
|
422
462
|
// slug 非空时在 agent 后展示(accent 色),空串时省略。
|
|
423
463
|
const slugTag = r.slug ? ` ${t.fg("accent", r.slug)}` : "";
|
|
424
|
-
const label = `${iconStr} ${sid}${depthTag} ${r.agent}${slugTag} ${t.fg("dim", modeTag)} ${t.fg("dim", dur)}`;
|
|
464
|
+
const label = `${iconStr} ${sid}${depthTag} ${displayAgentName(r.agent)}${slugTag} ${t.fg("dim", modeTag)} ${t.fg("dim", dur)}`;
|
|
425
465
|
// 阶段 2:锚定行 accent + ▶;其余行 dim。阶段 1:选中 accent + →,其余正常。
|
|
426
466
|
const content = inDetail
|
|
427
467
|
? (selected ? t.fg("accent", label) : t.fg("dim", label))
|
|
@@ -449,7 +489,7 @@ export class SubagentsListComponent implements Component {
|
|
|
449
489
|
lines.push("");
|
|
450
490
|
}
|
|
451
491
|
}
|
|
452
|
-
lines.push(truncLine(`${t.bold(record.agent)} ${t.fg("dim", `· ${record.model}`)}`, width));
|
|
492
|
+
lines.push(truncLine(`${t.bold(displayAgentName(record.agent))} ${t.fg("dim", `· ${record.model}`)}`, width));
|
|
453
493
|
lines.push(truncLine(
|
|
454
494
|
t.fg("dim", `${record.status} · ${record.turns} turns · ${formatTokens(record.totalTokens)} · ${formatElapsedSeconds(elapsedSec(record))}`),
|
|
455
495
|
width,
|
|
@@ -550,8 +590,7 @@ export class SubagentsListComponent implements Component {
|
|
|
550
590
|
// parent 不需外部数据;children 需查同 session 的 record(collectRecords 有磁盘缓存,开销可接受)。
|
|
551
591
|
const parentLabel = record.parentRecordId ? shortId(record.parentRecordId) : "(root)";
|
|
552
592
|
content.push(truncLine(t.fg("dim", `parent: ${parentLabel}`), width));
|
|
553
|
-
const childIds = this.
|
|
554
|
-
.collectRecords(LIST_LIMIT)
|
|
593
|
+
const childIds = this.collectRecordsFrame()
|
|
555
594
|
.filter((r) => r.parentRecordId === record.id)
|
|
556
595
|
.map((r) => shortId(r.id));
|
|
557
596
|
content.push(truncLine(
|
|
@@ -18,12 +18,19 @@ import {
|
|
|
18
18
|
|
|
19
19
|
import { SLUG_MAX_LENGTH } from "../execution/execute-options-mapper.ts";
|
|
20
20
|
import { computeElapsedSeconds } from "../execution/execution-record.ts";
|
|
21
|
+
import { isResumable } from "../execution/lifecycle-predicates.ts";
|
|
22
|
+
import type { ExecutionRecord } from "../execution/types.ts";
|
|
21
23
|
import type { ModelInfo } from "../execution/model-resolver.ts";
|
|
22
24
|
import type { SubagentService } from "../execution/subagent-service.ts";
|
|
25
|
+
import { displayAgentName } from "../shared/agent-ref.ts";
|
|
23
26
|
import type {
|
|
24
27
|
BgResponse,
|
|
25
28
|
CancelResponse,
|
|
29
|
+
CloseResponse,
|
|
30
|
+
ExecutionStatus,
|
|
31
|
+
ExternalState,
|
|
26
32
|
ListResponse,
|
|
33
|
+
MessageResponse,
|
|
27
34
|
SubagentListItem,
|
|
28
35
|
SubagentRecord,
|
|
29
36
|
SubagentToolResult,
|
|
@@ -70,6 +77,10 @@ export interface StartHandlerInput {
|
|
|
70
77
|
worktree?: boolean;
|
|
71
78
|
/** 覆盖子 agent 工作目录(默认 mainCwd)。 */
|
|
72
79
|
cwd?: string;
|
|
80
|
+
/** 可持续对话模式(true = chatMode,轮次完成进 idle 等续聊)。 */
|
|
81
|
+
conversation?: boolean;
|
|
82
|
+
/** 空闲超时毫秒数(仅 conversation 模式有意义,覆盖默认 5min)。 */
|
|
83
|
+
idleTimeoutMs?: number;
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
/** start 领域对象(adapter 包成 bgResponse)。 */
|
|
@@ -114,18 +125,53 @@ export interface CancelHandlerResult {
|
|
|
114
125
|
* 不新增 sessionId 到 ExecutionRecord(YAGNI,修跨 session 清理是独立问题)。
|
|
115
126
|
*/
|
|
116
127
|
|
|
117
|
-
/**
|
|
118
|
-
function
|
|
128
|
+
/** exhaustiveness 兜底:default 分支把 status 收敛为 never,ExecutionStatus 加态时 tsc 报错。 */
|
|
129
|
+
function assertNever(value: never): string {
|
|
130
|
+
return String(value);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 内部 ExecutionStatus → 对外 state 映射(设计决策 10 细则 3)。
|
|
135
|
+
* v4 B-1 两态收敛后的真实映射只有两条:
|
|
136
|
+
* running → active / closed → ended(closed 统一终态,含 cancelled)
|
|
137
|
+
* ExternalState 仍声明 waiting/error 四态联合(对外契约不变),但当前状态机不产生
|
|
138
|
+
* 这两个值——它们是历史多态映射(idle→waiting / failed+crashed→error)的遗留声明。
|
|
139
|
+
* 未来内部加态必须扩展此处,漏加会在 default 分支编译报错(而非静默返回 undefined
|
|
140
|
+
* 让 state 字段以无主值进入 listResponse JSON)。
|
|
141
|
+
*/
|
|
142
|
+
export function mapExternalState(status: ExecutionStatus): ExternalState {
|
|
143
|
+
switch (status) {
|
|
144
|
+
case "running":
|
|
145
|
+
return "active";
|
|
146
|
+
case "closed":
|
|
147
|
+
// v4 B-1: closed 统一终态(含 cancelled)。对外映射为 ended。
|
|
148
|
+
return "ended";
|
|
149
|
+
default:
|
|
150
|
+
throw new Error(`mapExternalState: unhandled ExecutionStatus ${assertNever(status)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** SubagentRecord → SubagentListItem(state 四态主字段 + status 调试字段,duration 实时计算)。
|
|
155
|
+
* [v4 A-6] 新增 parent/resumable/closedReason:parent 从 record.parentRecordId 派生
|
|
156
|
+
* (配合 A-5 直接父守卫),resumable 从 isResumable 派生(B-1「可续聊」对外表达),
|
|
157
|
+
* closedReason 透传(SP-4 级联关闭告知替代 before_agent_start 注入)。
|
|
158
|
+
* agent 是 GUI/TUI list 共用的显示名——取 basename 短名(displayAgentName),
|
|
159
|
+
* 完整路径保留在 record.agent(数据层)。 */
|
|
160
|
+
export function recordToListItem(r: SubagentRecord): SubagentListItem {
|
|
119
161
|
return {
|
|
120
162
|
subagentId: r.id,
|
|
121
|
-
agent: r.agent,
|
|
163
|
+
agent: displayAgentName(r.agent),
|
|
122
164
|
slug: r.slug,
|
|
165
|
+
state: mapExternalState(r.status),
|
|
123
166
|
status: r.status,
|
|
124
167
|
mode: r.mode,
|
|
125
168
|
duration: computeElapsedSeconds(r),
|
|
126
169
|
model: r.model,
|
|
127
170
|
totalTokens: r.totalTokens,
|
|
128
171
|
sessionFile: r.sessionFile,
|
|
172
|
+
parent: r.parentRecordId,
|
|
173
|
+
resumable: isResumable(r),
|
|
174
|
+
closedReason: r.closedReason,
|
|
129
175
|
};
|
|
130
176
|
}
|
|
131
177
|
|
|
@@ -171,10 +217,11 @@ export async function startHandler(
|
|
|
171
217
|
fork: input.fork,
|
|
172
218
|
worktree: input.worktree,
|
|
173
219
|
cwd: input.cwd,
|
|
220
|
+
conversation: input.conversation,
|
|
221
|
+
idleTimeoutMs: input.idleTimeoutMs,
|
|
174
222
|
ctxModel,
|
|
175
223
|
signal,
|
|
176
|
-
// background
|
|
177
|
-
onUpdate: undefined,
|
|
224
|
+
// background detached 运行,完成由 notify 驱动新 turn。
|
|
178
225
|
});
|
|
179
226
|
|
|
180
227
|
return {
|
|
@@ -207,7 +254,12 @@ export function listHandler(
|
|
|
207
254
|
// 防截断(先多取再过滤)已下沉到 store 层——这里直接传 limit + filter。
|
|
208
255
|
const filter = includeFinished ? "all" : "running";
|
|
209
256
|
const all = service.collectRecords(limit, filter);
|
|
210
|
-
|
|
257
|
+
// [perf] collectRecords 磁盘源是 light(无 totalTokens/model 等):SubagentListItem 对
|
|
258
|
+
// LLM 消费方暴露 totalTokens/model,逐项 getFullRecord 补全(per-file 缓存,仅首次
|
|
259
|
+
// 全量解析;显式 tool 调用非渲染热路径,成本可接受)。
|
|
260
|
+
const items: SubagentListItem[] = all.map((r) =>
|
|
261
|
+
recordToListItem(service.getFullRecord(r.id) ?? r),
|
|
262
|
+
);
|
|
211
263
|
const running = items.filter((i) => i.status === "running").length;
|
|
212
264
|
|
|
213
265
|
return { response: { running, items } };
|
|
@@ -245,6 +297,14 @@ export async function cancelHandler(
|
|
|
245
297
|
if (rec.mode !== "background") {
|
|
246
298
|
throw new Error(`Cannot cancel subagent ${id} (unsupported mode: ${rec.mode})`);
|
|
247
299
|
}
|
|
300
|
+
// 对话模式 cancel = close(force:true) 别名(设计决策 5):chatMode record(running/idle)
|
|
301
|
+
// 走 close 行为路径(idle 终态化 done;running 立即 SIGTERM cancelled),
|
|
302
|
+
// 返回 cancel 响应(向后兼容 cancel action 的返回类型)。非 chatMode 保持现有 cancel 行为。
|
|
303
|
+
if (rec.chatMode) {
|
|
304
|
+
const chatRecord = service.getRecordForAction(id);
|
|
305
|
+
await service.closeSubagent(chatRecord, true);
|
|
306
|
+
return { subagentId: id, response: { cancelled: true } };
|
|
307
|
+
}
|
|
248
308
|
// step 3: service.cancel boolean(list-view 契约不变);false = 已终态(CAS 抢锁失败)。
|
|
249
309
|
// 注意:不嵌入 rec.status——findRecord 快照可能已过期(TOCTOU:cancel 期间 detached
|
|
250
310
|
// 路径 CAS 到 done/failed)。重新查当前状态,避免「status: running」与「already finished」矛盾。
|
|
@@ -259,6 +319,127 @@ export async function cancelHandler(
|
|
|
259
319
|
return { subagentId: id, response: { cancelled: true } };
|
|
260
320
|
}
|
|
261
321
|
|
|
322
|
+
// ============================================================
|
|
323
|
+
// message handler(M2-B3 对话模式续聊/插入)
|
|
324
|
+
// ============================================================
|
|
325
|
+
|
|
326
|
+
export interface MessageHandlerInput {
|
|
327
|
+
subagentId?: string;
|
|
328
|
+
text?: string;
|
|
329
|
+
interrupt?: boolean;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** message 领域对象(adapter 包成 messageResponse)。 */
|
|
333
|
+
export type MessageHandlerResult = {
|
|
334
|
+
kind: "message";
|
|
335
|
+
subagentId: string;
|
|
336
|
+
response: MessageResponse;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* message action handler:向对话模式 subagent 续聊/插入消息。
|
|
341
|
+
*
|
|
342
|
+
* 状态 × interrupt 自动映射(agent 只表达意图):
|
|
343
|
+
* running → deliverMessage 热路径(进程活:prompt + streamingBehavior,interrupt=true
|
|
344
|
+
* 抢占 / false 排队,pi 权威裁决 busy/idle)
|
|
345
|
+
* 进程死 → deliverMessage 冷路径(resumeRound 重开 session + prompt,interrupt 自动
|
|
346
|
+
* 退化,agent 无感)
|
|
347
|
+
* 终态 → throw ended(正常路径不命中——终态 record 已 archive,getRecordForAction 先 throw not found)
|
|
348
|
+
* (旧 deliverToRunning busy 投递已删除——SP-5 upgrade 后 running 恒走 deliverMessage)
|
|
349
|
+
*
|
|
350
|
+
* 归属守卫(决策 3):getRecordForAction 内部校验 rootSessionId。
|
|
351
|
+
*
|
|
352
|
+
* @throws Error subagentId/text 缺失 / 不存在或非本 session 所有 / 已结束
|
|
353
|
+
*/
|
|
354
|
+
export async function messageHandler(
|
|
355
|
+
service: SubagentService,
|
|
356
|
+
input: MessageHandlerInput | undefined,
|
|
357
|
+
): Promise<MessageHandlerResult> {
|
|
358
|
+
const id = input?.subagentId?.trim();
|
|
359
|
+
if (!id) throw new Error("messageParam.subagentId is required for action:'message'");
|
|
360
|
+
const text = input?.text?.trim();
|
|
361
|
+
if (!text) throw new Error(
|
|
362
|
+
"messageParam.text is required for action:'message' (must not be whitespace-only). " +
|
|
363
|
+
'Correct: {"action":"message","messageParam":{"subagentId":"sa-...","text":"your follow-up"}}',
|
|
364
|
+
);
|
|
365
|
+
const interrupt = input?.interrupt === true;
|
|
366
|
+
|
|
367
|
+
// 归属守卫(决策 3):getRecordForAction 内部校验 rootSessionId
|
|
368
|
+
const record = service.getRecordForAction(id);
|
|
369
|
+
|
|
370
|
+
// SP-5 one-shot upgrade:非 chatMode 的 active record(running/idle)收到 message 时
|
|
371
|
+
// 自动升级为 chatMode,后续走 deliverMessage 统一投递路径(热路径或冷路径 resume)。
|
|
372
|
+
// closed/cancelled 终态 record 不可 upgrade(getRecordForAction 已抛 not found)。
|
|
373
|
+
// chatMode 是 ExecutionRecord 的 readonly 字段,用 Mutable<T> 显式断言绕过 readonly 约束(upgrade 语义)。
|
|
374
|
+
// Object.assign 隐式绕过 readonly 不可追踪,改为单字段显式赋值。
|
|
375
|
+
// [v4 A-3] 进程内 upgrade 入口(SP-5)——one-shot 首条 message 触发 upgrade 置位
|
|
376
|
+
// chatMode=true。与 subagent-service.ts getRecordForAction 磁盘重建(跨重启恢复入口)
|
|
377
|
+
// 分工:本入口服务进程内 one-shot,跨重启路径恒被 getRecordForAction 磁盘重建绕过
|
|
378
|
+
// (该处无条件 chatMode=true)。改动这两处必须协同,带 S3 回归。
|
|
379
|
+
if (!record.chatMode && record.status === "running") {
|
|
380
|
+
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
|
|
381
|
+
(record as Mutable<ExecutionRecord>).chatMode = true;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// [V2 决策 3] chatMode 统一投递:按进程死活分流(热路径 prompt+streamingBehavior / 冷路径 resume),
|
|
385
|
+
// 不按 record.status(V2 进程长驻,idle 态进程仍活,续聊走热路径 prompt 而非重开 session)。
|
|
386
|
+
// SP-5:upgrade 后 record.chatMode 已为 true,统一进此分支。
|
|
387
|
+
if (record.chatMode) {
|
|
388
|
+
await service.deliverMessage(record, text, interrupt);
|
|
389
|
+
} else {
|
|
390
|
+
// 终态(closed/cancelled):防御性兜底(终态 record 已 archive,正常走 not found)
|
|
391
|
+
throw new Error(
|
|
392
|
+
`subagent ${id} has ended (status: ${record.status}), cannot message. ` +
|
|
393
|
+
`Recovery: use action:'close' to clean up, then action:'start' a new subagent.`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return { kind: "message", subagentId: id, response: { delivered: true } };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ============================================================
|
|
400
|
+
// close handler(M2-B3 对话模式结束)
|
|
401
|
+
// ============================================================
|
|
402
|
+
|
|
403
|
+
export interface CloseHandlerInput {
|
|
404
|
+
subagentId?: string;
|
|
405
|
+
force?: boolean;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** close 领域对象(adapter 包成 closeResponse)。 */
|
|
409
|
+
export type CloseHandlerResult = {
|
|
410
|
+
kind: "close";
|
|
411
|
+
subagentId: string;
|
|
412
|
+
response: CloseResponse;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* close action handler:结束 subagent(对话模式为主,one-shot 同样支持)。
|
|
417
|
+
*
|
|
418
|
+
* force 语义(设计决策 5/10,v4 B-1 两态收敛 + M5 修正):
|
|
419
|
+
* force:false(默认)= 优雅关闭——
|
|
420
|
+
* 无在跑轮(等待续聊 timer armed / 无活进程)→ 立即终态化(closed + user-close,回收保活进程)
|
|
421
|
+
* 有活进程在跑轮 → 置 closeAfterRound,轮完成时终态化(chatMode 消费点 onRoundSettled,
|
|
422
|
+
* one-shot 在 runAndFinalize 轮完成分支)——返回 {closed:true} 即承诺轮结束后资源已释放
|
|
423
|
+
* force:true = 立即终止——running 立即 SIGTERM(cancelBackground 显式 kill)+ closed+cancelled
|
|
424
|
+
*
|
|
425
|
+
* 行为分流委托 service.closeSubagent(归属守卫由 getRecordForAction 把关)。
|
|
426
|
+
* 已终态 record 由 getRecordForAction throw not found(「已结束的不能再操作」语义)。
|
|
427
|
+
*/
|
|
428
|
+
export async function closeHandler(
|
|
429
|
+
service: SubagentService,
|
|
430
|
+
input: CloseHandlerInput | undefined,
|
|
431
|
+
): Promise<CloseHandlerResult> {
|
|
432
|
+
const id = input?.subagentId?.trim();
|
|
433
|
+
if (!id) throw new Error("closeParam.subagentId is required for action:'close'");
|
|
434
|
+
const force = input?.force === true;
|
|
435
|
+
|
|
436
|
+
// 归属守卫(决策 3)+ 行为分流(service.closeSubagent)
|
|
437
|
+
const record = service.getRecordForAction(id);
|
|
438
|
+
await service.closeSubagent(record, force);
|
|
439
|
+
|
|
440
|
+
return { kind: "close", subagentId: id, response: { closed: true } };
|
|
441
|
+
}
|
|
442
|
+
|
|
262
443
|
// ============================================================
|
|
263
444
|
// adapter(领域对象 → SubagentToolResult + {content, details})
|
|
264
445
|
// ============================================================
|
|
@@ -270,7 +451,9 @@ export async function cancelHandler(
|
|
|
270
451
|
type AdapterInput =
|
|
271
452
|
| { action: "start"; domain: StartHandlerResult }
|
|
272
453
|
| { action: "list"; domain: ListHandlerResult }
|
|
273
|
-
| { action: "cancel"; domain: CancelHandlerResult }
|
|
454
|
+
| { action: "cancel"; domain: CancelHandlerResult }
|
|
455
|
+
| { action: "message"; domain: MessageHandlerResult }
|
|
456
|
+
| { action: "close"; domain: CloseHandlerResult };
|
|
274
457
|
|
|
275
458
|
export function adapter(
|
|
276
459
|
input: AdapterInput,
|
|
@@ -280,20 +463,35 @@ export function adapter(
|
|
|
280
463
|
let result: SubagentToolResult;
|
|
281
464
|
if (action === "start") {
|
|
282
465
|
const d = input.domain;
|
|
283
|
-
|
|
466
|
+
// MF-3(决策 10 细则 4):LLM content (text) 用 null 瘦身,防诱导 agent 用 read 绕过工具
|
|
467
|
+
// 直接读 session 文件。真实 sessionFile 仅 details 保留(供 GUI/程序化消费)。
|
|
468
|
+
result = { action, subagentId: d.subagentId, sessionFile: null, slug: d.slug, bgResponse: d.response };
|
|
284
469
|
} else if (action === "list") {
|
|
285
470
|
result = { action, subagentId: null, sessionFile: null, listResponse: input.domain.response };
|
|
286
|
-
} else {
|
|
471
|
+
} else if (action === "cancel") {
|
|
287
472
|
result = { action, subagentId: input.domain.subagentId, sessionFile: null, cancelResponse: input.domain.response };
|
|
473
|
+
} else if (action === "message") {
|
|
474
|
+
result = { action, subagentId: input.domain.subagentId, sessionFile: null, messageResponse: input.domain.response };
|
|
475
|
+
} else {
|
|
476
|
+
// action === "close"
|
|
477
|
+
result = { action, subagentId: input.domain.subagentId, sessionFile: null, closeResponse: input.domain.response };
|
|
288
478
|
}
|
|
289
479
|
|
|
290
480
|
// content JSON:LLM 看的结构化结果(schema 模式 parsedOutput 作为嵌套 JSON 值可接受)。
|
|
291
481
|
const text = JSON.stringify(result);
|
|
292
482
|
|
|
483
|
+
// MF-3:start 的 LLM content 已瘦身(sessionFile:null),details 保留真实 sessionFile 供 GUI/程序化消费。
|
|
484
|
+
// result 已是合法 SubagentToolResult(start 变体 sessionFile:null);start 时重建一个带真实 sessionFile 的副本。
|
|
485
|
+
let detailsBase: SubagentToolResult = result;
|
|
486
|
+
if (action === "start") {
|
|
487
|
+
const d = input.domain;
|
|
488
|
+
detailsBase = { action: "start", subagentId: d.subagentId, sessionFile: d.sessionFile ?? null, slug: d.slug, bgResponse: d.response };
|
|
489
|
+
}
|
|
490
|
+
|
|
293
491
|
// GUI 协议:RPC 模式下附加结构化渲染数据(union 各成员已声明 __gui__?,无需强转)
|
|
294
492
|
const details: SubagentToolResult = ctx && isGuiCapable(ctx)
|
|
295
|
-
? { ...
|
|
296
|
-
:
|
|
493
|
+
? { ...detailsBase, __gui__: guiResult(buildGuiComponent(input, result)) }
|
|
494
|
+
: detailsBase;
|
|
297
495
|
|
|
298
496
|
// [W3 修复] list action 追加 reminder text block:LLM 调 list 时提醒不要轮询。
|
|
299
497
|
// reminder 作为第二个 text block(独立追加,不污染 details/JSON schema)。
|
|
@@ -308,16 +506,20 @@ export function adapter(
|
|
|
308
506
|
};
|
|
309
507
|
}
|
|
310
508
|
|
|
311
|
-
/**
|
|
509
|
+
/**
|
|
510
|
+
* 按 input.action 构造对应的 GuiComponent。
|
|
511
|
+
* 分支判定用 discriminated key(input.action)而非独立宽类型参数——TS 自动
|
|
512
|
+
* 收窄 input.domain 到对应 HandlerResult,无需 `as` 断言(与 adapter() 同款模式)。
|
|
513
|
+
*/
|
|
312
514
|
export function buildGuiComponent(
|
|
313
|
-
action: string,
|
|
314
515
|
input: AdapterInput,
|
|
315
516
|
_result: SubagentToolResult,
|
|
316
517
|
) {
|
|
518
|
+
const { action } = input;
|
|
317
519
|
if (action === "start") {
|
|
318
520
|
// subagent-trace 多层语义(agent名+slug+状态)用 card(stats-line) 组合表达。
|
|
319
521
|
// 利用 input.domain 的身份信息,让并发 subagent 可区分。
|
|
320
|
-
const d = input.domain
|
|
522
|
+
const d = input.domain;
|
|
321
523
|
return guiComponent("card", {
|
|
322
524
|
header: d.slug ? `${d.slug}` : d.subagentId.slice(0, SUBAGENT_ID_PREVIEW),
|
|
323
525
|
body: [guiComponent("stats-line", {
|
|
@@ -326,7 +528,7 @@ export function buildGuiComponent(
|
|
|
326
528
|
});
|
|
327
529
|
}
|
|
328
530
|
if (action === "list") {
|
|
329
|
-
const listResp = input.domain
|
|
531
|
+
const listResp = input.domain;
|
|
330
532
|
return guiComponent("list-tree", {
|
|
331
533
|
items: listResp.response.items.map((it) => ({
|
|
332
534
|
label: it.slug ? `${it.agent} · ${it.slug} · ${it.subagentId}` : `${it.agent} · ${it.subagentId}`,
|
|
@@ -335,8 +537,24 @@ export function buildGuiComponent(
|
|
|
335
537
|
})),
|
|
336
538
|
});
|
|
337
539
|
}
|
|
338
|
-
|
|
540
|
+
if (action === "message") {
|
|
541
|
+
return guiComponent("stats-line", {
|
|
542
|
+
items: [{ label: "messaged", value: input.domain.subagentId, severity: "ok" }],
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
if (action === "close") {
|
|
546
|
+
return guiComponent("stats-line", {
|
|
547
|
+
items: [{ label: "closed", value: input.domain.subagentId, severity: "warn" }],
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
// cancel(fall-through:前四分支已 return)。
|
|
551
|
+
// 穷尽检查(同 mapExternalState 的 assertNever 先例):AdapterInput 未来新增
|
|
552
|
+
// action 时,action 在此不再收窄为 "cancel",assertNever 的 never 参数处编译
|
|
553
|
+
// 报错——防止新分支静默落入 cancelled 渲染。
|
|
554
|
+
if (action !== "cancel") {
|
|
555
|
+
throw new Error(`buildGuiComponent: unhandled action ${assertNever(action)}`);
|
|
556
|
+
}
|
|
339
557
|
return guiComponent("stats-line", {
|
|
340
|
-
items: [{ label: "cancelled", value:
|
|
558
|
+
items: [{ label: "cancelled", value: input.domain.subagentId, severity: "warn" }],
|
|
341
559
|
});
|
|
342
560
|
}
|