@sema-agent/client-core 0.62.2 → 0.63.1

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.
@@ -0,0 +1,236 @@
1
+ /**
2
+ * src/toolRoster.ts — 一条腿的**工具名册**(`wiring_manifest.tools: ToolRoster`)窄读器 +
3
+ * `tool_roster_delta` 应用(0.63.0;L-161;engine >=7.66.0 有臂 / >=7.9.0 真推 /
4
+ * sdk 8.6.0 型面 + 8.7.0 `pathTarget` 三键)。
5
+ *
6
+ * -- 名册回答的是三端一直在猜的那一问 --------------------------------------------------------
7
+ * 「这条腿到底挂了哪些工具、每一只是什么面」此前只能猜:`liveInitToolFace.ts` 那三张词表
8
+ * (`ENGINE_HANDS_BAND` / `ENGINE_SCENARIO_EXTRAS_DEFAULT` / `ENGINE_RUNNER_FACE`)是 2026-07-16
9
+ * 一次 tap 实测的**估计值**,按 pin 的引擎版本定稿;外接别的引擎、别的场景、别的 env 门就会偏。
10
+ * 名册是**引擎自己说的**:每只工具的身份(契约 id / 形状指纹 / 能力 id)、轴(effect / egress /
11
+ * irreversibility)、面(family / pathTarget / renderHints)。
12
+ * 🔴 **名册在场用名册,缺席才回落三表**。本批**不删**那三张表(名册只在 effective 半场、且只有
13
+ * >=7.9.0 的引擎才真推,回落还得留着);`liveInitToolFace.ts` 的头注登记了退役条款:
14
+ * **名册恒在场的版本到货即删表**。
15
+ *
16
+ * -- 🔴 绝不半张名册(照抄上游的纪律)--------------------------------------------------------
17
+ * 引擎的 fail-open 台账逐字写着:载荷过不了 core 自己的判形器 => 那一段缺席 / 那一帧不发,
18
+ * **绝不半张脸上 wire**;并写明反方向更坏 ——「把一份判不了形的名册照发,消费端会把读不出的行
19
+ * 当成**这只工具没挂**」。本窄读器照抄这条:**任一行读不出 => 整只 `undefined`**;`count` 与真实
20
+ * 行数对不上同理(core 在铸点 run-time 断言两者相等)。少几行的名册比没有名册坏。
21
+ * ⚠️ 例外只有**面**:一行的 `pathTarget` / `renderHints` 自己形坏时只丢那一格,行还在 —— 面不是
22
+ * 身份,丢一张面不等于「这只工具没挂」。
23
+ *
24
+ * -- 🔴 `fromDigest` 对不上**不是拒绝** ------------------------------------------------------
25
+ * 契约里唯一的硬话:`fromDigest` 对不上时,**携带的整只名册无论如何都是新状态**,只有 `summary`
26
+ * 变得不可用(消费端记一次 skew 后按快照重同步)。所以 {@link applyToolRosterDelta} 在 skew 时
27
+ * **照换名册、只丢 summary**。拒绝换会让消费端永远抱着一份过期名册 —— 那比一次 skew 坏得多,
28
+ * 而且它把「位置从名册来、永远不从 summary 来」这条契约反过来用了。
29
+ *
30
+ * -- 开集读 -----------------------------------------------------------------------------------
31
+ * `source`(七词)/ `effect` / `family` / `access` 等词表的属主都是引擎,一律**原样透传**,不窄读成
32
+ * 枚举 —— 那会在引擎加词当天把一份真名册判没。
33
+ */
34
+ const str = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
35
+ const strArr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string') : undefined;
36
+ const obj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v) ? v : undefined;
37
+ /** 指纹判据:**恰** 16 位小写 hex(引擎铸的就是这个形;放宽会让一个坏锚看起来能用)。 */
38
+ const DIGEST_RE = /^[0-9a-f]{16}$/;
39
+ function readPathTarget(raw) {
40
+ const o = obj(raw);
41
+ if (o === undefined)
42
+ return undefined;
43
+ const param = str(o.param);
44
+ const access = str(o.access);
45
+ // 两必填键读不出 => 这张面不成形。**只丢面,不丢行**(面不是身份)。
46
+ if (param === undefined || access === undefined)
47
+ return undefined;
48
+ const aliases = strArr(o.aliases);
49
+ const base = str(o.base);
50
+ const absent = str(o.absent);
51
+ const patternParam = str(o.patternParam);
52
+ return {
53
+ param,
54
+ access,
55
+ ...(aliases !== undefined ? { aliases } : {}),
56
+ ...(o.skillScopeEligible === true ? { skillScopeEligible: true } : {}),
57
+ ...(base !== undefined ? { base } : {}),
58
+ ...(absent !== undefined ? { absent } : {}),
59
+ ...(patternParam !== undefined ? { patternParam } : {}),
60
+ };
61
+ }
62
+ function readEntry(raw) {
63
+ const o = obj(raw);
64
+ if (o === undefined)
65
+ return undefined;
66
+ const name = str(o.name);
67
+ const source = str(o.source);
68
+ // 身份两键读不出 => 这一行不成形 => 由调用方把**整只名册**判没(见模块顶注)。
69
+ if (name === undefined || source === undefined)
70
+ return undefined;
71
+ const originRaw = obj(o.origin);
72
+ const peer = originRaw !== undefined ? str(originRaw.peer) : undefined;
73
+ const declaredBy = originRaw !== undefined ? str(originRaw.declaredBy) : undefined;
74
+ const contractRaw = obj(o.contract);
75
+ const contractId = contractRaw !== undefined ? str(contractRaw.contractId) : undefined;
76
+ const implementationRevision = contractRaw !== undefined ? str(contractRaw.implementationRevision) : undefined;
77
+ const hintsRaw = obj(o.renderHints);
78
+ const ruleFaceRaw = obj(o.ruleFace);
79
+ return {
80
+ name,
81
+ source,
82
+ ...(strArr(o.aliases) !== undefined ? { aliases: strArr(o.aliases) } : {}),
83
+ ...(peer !== undefined
84
+ ? { origin: { peer, ...(declaredBy !== undefined ? { declaredBy } : {}) } }
85
+ : {}),
86
+ ...(contractId !== undefined
87
+ ? { contract: { contractId, ...(implementationRevision !== undefined ? { implementationRevision } : {}) } }
88
+ : {}),
89
+ ...(str(o.shapeDigest) !== undefined ? { shapeDigest: str(o.shapeDigest) } : {}),
90
+ ...(str(o.wireSchemaDigest) !== undefined ? { wireSchemaDigest: str(o.wireSchemaDigest) } : {}),
91
+ ...(str(o.cardId) !== undefined ? { cardId: str(o.cardId) } : {}),
92
+ ...(str(o.capabilityId) !== undefined ? { capabilityId: str(o.capabilityId) } : {}),
93
+ ...(str(o.effect) !== undefined ? { effect: str(o.effect) } : {}),
94
+ ...(typeof o.egress === 'boolean' ? { egress: o.egress } : {}),
95
+ ...(str(o.irreversibility) !== undefined ? { irreversibility: str(o.irreversibility) } : {}),
96
+ ...(str(o.contentOrigin) !== undefined ? { contentOrigin: str(o.contentOrigin) } : {}),
97
+ ...(str(o.family) !== undefined ? { family: str(o.family) } : {}),
98
+ ...(readPathTarget(o.pathTarget) !== undefined
99
+ ? { pathTarget: readPathTarget(o.pathTarget) }
100
+ : {}),
101
+ ...(hintsRaw !== undefined
102
+ ? {
103
+ renderHints: {
104
+ ...(str(hintsRaw.userFacingName) !== undefined ? { userFacingName: str(hintsRaw.userFacingName) } : {}),
105
+ ...(str(hintsRaw.activity) !== undefined ? { activity: str(hintsRaw.activity) } : {}),
106
+ ...(strArr(hintsRaw.summaryParams) !== undefined ? { summaryParams: strArr(hintsRaw.summaryParams) } : {}),
107
+ ...(strArr(hintsRaw.resultCards) !== undefined ? { resultCards: strArr(hintsRaw.resultCards) } : {}),
108
+ ...(str(hintsRaw.approvalCard) !== undefined ? { approvalCard: str(hintsRaw.approvalCard) } : {}),
109
+ },
110
+ }
111
+ : {}),
112
+ ...(ruleFaceRaw !== undefined
113
+ ? {
114
+ ruleFace: {
115
+ ...(strArr(ruleFaceRaw.primaryParams) !== undefined ? { primaryParams: strArr(ruleFaceRaw.primaryParams) } : {}),
116
+ ...(strArr(ruleFaceRaw.params) !== undefined ? { params: strArr(ruleFaceRaw.params) } : {}),
117
+ },
118
+ }
119
+ : {}),
120
+ };
121
+ }
122
+ /** 一只**已经剥出来**的名册体(不带 `tools` 包层)→ 视图;坏形 => `undefined`。 */
123
+ function readRoster(raw) {
124
+ const o = obj(raw);
125
+ if (o === undefined)
126
+ return undefined;
127
+ const digest = typeof o.digest === 'string' && DIGEST_RE.test(o.digest) ? o.digest : undefined;
128
+ if (digest === undefined)
129
+ return undefined;
130
+ if (!Array.isArray(o.entries))
131
+ return undefined;
132
+ if (typeof o.count !== 'number' || !Number.isInteger(o.count) || o.count < 0)
133
+ return undefined;
134
+ // 🔴 count 是引擎在铸点 run-time 断言过的量;对不上 => 这份载荷在路上被改过或被半截序列化。
135
+ if (o.count !== o.entries.length)
136
+ return undefined;
137
+ const entries = [];
138
+ for (const raw2 of o.entries) {
139
+ const e = readEntry(raw2);
140
+ // 🔴 绝不半张名册:一行读不出 => 整只判没(见模块顶注)。
141
+ if (e === undefined)
142
+ return undefined;
143
+ entries.push(e);
144
+ }
145
+ const schemaVersion = typeof o.schemaVersion === 'number' && Number.isFinite(o.schemaVersion) ? o.schemaVersion : 1;
146
+ return { schemaVersion, count: o.count, digest, entries };
147
+ }
148
+ /**
149
+ * `wiring_manifest` 的 `tools` 段 → 名册视图;整段缺席 / 任一行读不出 / `count` 对不上 =>
150
+ * `undefined`,绝不抛出。
151
+ *
152
+ * ⚠️ **空名册是正面事实**(这条腿一只工具都没挂),照读成 0 行 —— 与 `wiring_manifest.mcp[]` 的
153
+ * 空数组同一条已定谳的纪律,把它折成缺席等于把引擎明说的一句话读成「不知道」。
154
+ * ⚠️ 只在 **effective 半场**(live 帧)才有;诊断端点返回的 `static` 半场恒无 —— 那里的缺席是
155
+ * 结构性的,不是「这台引擎太老」。
156
+ */
157
+ export function projectToolRoster(manifest) {
158
+ const o = obj(manifest);
159
+ if (o === undefined)
160
+ return undefined;
161
+ if (!('tools' in o))
162
+ return undefined;
163
+ return readRoster(o.tools);
164
+ }
165
+ /** 名册里的工具名,**按声明序**(= 模型看到的 `tools[]` 序)。 */
166
+ export function toolRosterNames(roster) {
167
+ return roster?.entries.map((e) => e.name) ?? [];
168
+ }
169
+ /** 名册的一行 → 端用的面。行不成形(身份两键读不出)=> `undefined`,绝不抛出。 */
170
+ export function toolShimFromRoster(entry) {
171
+ const e = readEntry(entry);
172
+ if (e === undefined)
173
+ return undefined;
174
+ return {
175
+ name: e.name,
176
+ source: e.source,
177
+ ...(e.family !== undefined ? { family: e.family } : {}),
178
+ ...(e.renderHints?.userFacingName !== undefined ? { userFacingName: e.renderHints.userFacingName } : {}),
179
+ ...(e.renderHints?.activity !== undefined ? { activity: e.renderHints.activity } : {}),
180
+ ...(e.renderHints?.approvalCard !== undefined ? { approvalCard: e.renderHints.approvalCard } : {}),
181
+ ...(e.renderHints?.summaryParams !== undefined ? { summaryParams: e.renderHints.summaryParams } : {}),
182
+ ...(e.effect !== undefined ? { effect: e.effect } : {}),
183
+ ...(e.egress !== undefined ? { egress: e.egress } : {}),
184
+ ...(e.irreversibility !== undefined ? { irreversibility: e.irreversibility } : {}),
185
+ ...(e.pathTarget?.param !== undefined ? { pathTargetParam: e.pathTarget.param } : {}),
186
+ ...(e.origin?.peer !== undefined ? { peer: e.origin.peer } : {}),
187
+ ...(e.cardId !== undefined ? { cardId: e.cardId } : {}),
188
+ ...(e.capabilityId !== undefined ? { capabilityId: e.capabilityId } : {}),
189
+ };
190
+ }
191
+ function readSummary(raw) {
192
+ const o = obj(raw);
193
+ if (o === undefined)
194
+ return undefined;
195
+ const added = strArr(o.added);
196
+ const removed = strArr(o.removed);
197
+ const changed = strArr(o.changed);
198
+ if (added === undefined || removed === undefined || changed === undefined)
199
+ return undefined;
200
+ // 数组里混进非串时 `strArr` 会静默滤掉 —— 那等于交一份少了名字的名单,与「绝不半张」同罪。
201
+ if (!Array.isArray(o.added) || added.length !== o.added.length ||
202
+ !Array.isArray(o.removed) || removed.length !== o.removed.length ||
203
+ !Array.isArray(o.changed) || changed.length !== o.changed.length) {
204
+ return undefined;
205
+ }
206
+ // 契约:三张名单**互不相交**。交了集就是坏 summary。
207
+ const all = [...added, ...removed, ...changed];
208
+ if (new Set(all).size !== all.length)
209
+ return undefined;
210
+ return { added, removed, changed };
211
+ }
212
+ /**
213
+ * `tool_roster_delta` → 新的持有状态。**纯函数**(不改入参)。
214
+ *
215
+ * 🔴 `fromDigest` 对不上**不是拒绝**(契约里唯一的硬话):携带的整只名册无论如何都是新状态,
216
+ * 只有 `summary` 变得不可用。拒绝换会让消费端永远抱着一份过期名册 —— 比一次 skew 坏得多。
217
+ * 🔴 手上**没有**名册(首帧就是 delta)同样算 `skew`:没有可比的 digest 就是比不出来,而
218
+ * 「比不出来」不许被读成「对上了」。
219
+ * 🔴 帧本身判不出形 => **什么都没发生**:原样交回入参那一份(不清空 —— 清空会把一次读不懂的帧
220
+ * 变成一次「所有工具都没挂」)。
221
+ */
222
+ export function applyToolRosterDelta(held, delta) {
223
+ const d = obj(delta);
224
+ const next = d !== undefined ? readRoster(d.roster) : undefined;
225
+ if (d === undefined || next === undefined || typeof d.fromDigest !== 'string') {
226
+ return { roster: held, applied: false, skew: false };
227
+ }
228
+ const skew = held === undefined || held.digest !== d.fromDigest;
229
+ const summary = skew ? undefined : readSummary(d.summary);
230
+ return {
231
+ roster: next,
232
+ applied: true,
233
+ skew,
234
+ ...(summary !== undefined ? { summary } : {}),
235
+ };
236
+ }
package/dist/workflow.js CHANGED
@@ -96,7 +96,10 @@ export function projectWorkflowTaskOutput(jsonSrc) {
96
96
  if (!r || typeof r !== 'object')
97
97
  continue;
98
98
  const label = typeof r.label === 'string' && r.label.length > 0 ? r.label : '(agent)';
99
- const st = typeof r.status === 'string' ? r.status : '?';
99
+ // 🔴 `parseWorkflowPollEnvelope` **同一判**(0.63.1,G-20 附带②):同一条 wire 上的
100
+ // 同一个字段,两条链路的「在场」判据必须同形。空串走 `typeof` 单门会被当成一个读数
101
+ // 渲成 `[]` —— 一对什么也没说的方括号,还长得像一个真答案。空串 = 缺席。
102
+ const st = typeof r.status === 'string' && r.status.length > 0 ? r.status : '?';
100
103
  const errLine = typeof r.error === 'string' && r.error.length > 0
101
104
  ? ` — ${r.error.split('\n')[0].slice(0, 120)}`
102
105
  : '';
@@ -67,9 +67,23 @@ import { hostLog } from './host.js';
67
67
  // (workflowMonitor.ts) is the 187-faithful consumed set. We read the known fields, coerce
68
68
  // the vocabularies, and fill the shell-local render defaults the wire does not carry — same discipline as
69
69
  // projectTasks/projectWorkflows in fleet/fleetProjection.ts.
70
+ /**
71
+ * ── `parked` 在这一族投影里的纪律(0.63.1;G-20)────────────────────────────────────────────
72
+ * 「停在审批门上」是一个**说得出口的状态词**,不是一个未知的未来值:那条腿在有人做决定之前
73
+ * **永远不会**自己往前走一步,而 `running` 承诺的恰恰是「等着就好」。⇒ 下面四处窄读一律给它
74
+ * 一条**自己的臂**,开集兜底(未知未来值 → 活跃)一个字节都不动。
75
+ *
76
+ * 三级词表今天各自的真实供给(engine core 7.10.0,亲核):
77
+ * · **agent 行** —— `WorkflowItemStatus` 含 `parked`(#642),`deriveAgentDisplayStatus` 直接产
78
+ * `"parked"`(`AgentDisplayStatus` 六词)⇒ 这是**今天就会到**的读数。
79
+ * · **run 级** —— 仍闭在 running|completed|failed;一条腿 park ⇒ 整只 run 按
80
+ * `WorkflowAgentParkedError` 收在 `failed` ⇒ `parked` 今天到不了这一位,那条臂是**前向预挂**。
81
+ * · **phase 级** —— 上游明说「a phase or group never parks」,同为前向预挂;本包另从桶里的腿
82
+ * **推**出这一格(见 `projectPhases`)。
83
+ */
70
84
  /** run status → the monitor's run-level vocabulary. SDK: queued|running|completed|failed|(open). The monitor
71
- * run status is running|done|failed|stopped|paused — there is no run-level 'queued', so a queued run reads as
72
- * 'running' (the header shows the raw status text anyway; the box treats non-done/failed as active). */
85
+ * run status is running|done|failed|stopped|paused|parked — there is no run-level 'queued', so a queued run
86
+ * reads as 'running' (the header shows the raw status text anyway; the box treats non-done/failed as active). */
73
87
  function coerceRunStatus(s) {
74
88
  switch (s) {
75
89
  case 'completed':
@@ -78,6 +92,8 @@ function coerceRunStatus(s) {
78
92
  return 'failed';
79
93
  case 'running':
80
94
  return 'running';
95
+ case 'parked':
96
+ return 'parked'; // 前向预挂(见上方族头注):词真到了就原样透出,绝不折成「在跑」
81
97
  default:
82
98
  return 'running'; // queued / unknown-future → render as an active run (open-set fallback)
83
99
  }
@@ -94,6 +110,8 @@ function coercePhaseStatus(s) {
94
110
  case 'queued':
95
111
  case 'pending':
96
112
  return 'not-started';
113
+ case 'parked':
114
+ return 'parked'; // 前向预挂,同 coerceRunStatus
97
115
  default:
98
116
  return 'running';
99
117
  }
@@ -123,6 +141,11 @@ function coerceAgentState(s) {
123
141
  // !workflowActive — the run IS terminal whenever core emits this).
124
142
  case 'interrupted':
125
143
  return 'inactive';
144
+ // 🔴 core 7.10.0 #642:这条腿耐久挂在一张审批卡上(行上记着 checkpoint token)。**今天就会到**
145
+ // 的读数(raw `status` 与 `displayStatus` 两条腿都会带),原词透出——折进 `progress` 就是把
146
+ // 「要人去批」渲成「等它跑完」。
147
+ case 'parked':
148
+ return 'parked';
126
149
  default:
127
150
  return 'progress'; // unknown-future live status → render as an active agent (open-set fallback)
128
151
  }
@@ -174,7 +197,10 @@ function projectAgent(row, index) {
174
197
  // queued|running|done|failed|interrupted) over the RAW record `status` (running|completed|failed): the raw
175
198
  // vocabulary carries no queued/interrupted arm — a queued agent's record status is literally 'running', so
176
199
  // coercing `row.status` alone rendered every queued agent as "Running" (the [840] parity gap).
177
- const displayStatus = typeof rec.displayStatus === 'string' ? rec.displayStatus : undefined;
200
+ // 🔴 空串 = 缺席(0.63.1,G-20 同形存量⑤):`??` 只挡 null/undefined,一个空串的 `displayStatus`
201
+ // 会把 raw `status` 整个静默吃掉(`'' ?? row.status` === `''`)—— 那条腿真正的状态词连回落
202
+ // 这条路都走不到。与 `workflow.ts` 两条链路同尺:空串不是读数。
203
+ const displayStatus = typeof rec.displayStatus === 'string' && rec.displayStatus.length > 0 ? rec.displayStatus : undefined;
178
204
  const state = coerceAgentState(displayStatus ?? row.status);
179
205
  const activity = projectActivity(row.activity);
180
206
  const last = activity.length ? activity[activity.length - 1] : undefined;
@@ -276,10 +302,9 @@ function projectPhases(run, agents) {
276
302
  if (distinct.length > 0) {
277
303
  return distinct.map(title => {
278
304
  const bucket = agents.filter(a => a._phase === title);
279
- const allDone = bucket.every(a => a.state === 'done');
280
305
  return {
281
306
  title,
282
- status: bucket.some(a => a.state === 'error') ? 'failed' : allDone ? 'done' : 'running',
307
+ status: bucketStatus(bucket),
283
308
  doneCount: bucket.filter(a => a.state === 'done').length,
284
309
  totalCount: bucket.length,
285
310
  agents: bucket.map(stripPhase),
@@ -289,13 +314,45 @@ function projectPhases(run, agents) {
289
314
  return [
290
315
  {
291
316
  title: 'Workflow',
292
- status: run.status === 'completed' ? 'done' : run.status === 'failed' ? 'failed' : 'running',
317
+ // 同形第二处(G-20):这条三元式与 `coerceRunStatus` 读的是同一位 run 状态,兜底也同形 ——
318
+ // 只修一处等于把病搬个家。`parked` 走 `coerceRunStatus` 的同一条臂,不在这里另写一遍判据。
319
+ status: synthPhaseStatus(run.status, agents),
293
320
  doneCount: agents.filter(a => a.state === 'done').length,
294
321
  totalCount: agents.length,
295
322
  agents: agents.map(stripPhase),
296
323
  },
297
324
  ];
298
325
  }
326
+ /**
327
+ * 一桶腿 → 这一格的状态词。
328
+ *
329
+ * 🔴 `parked` 的判据是「**这一格里没有一条腿能自己往前走**」:全员不是完成就是停在门上,且至少有
330
+ * 一条真停着。混着还在跑的腿时**仍报 running** —— 那一格确实还在跑,渲成 parked 是另一个方向的谎。
331
+ * 🔴 `failed` 仍先判:一格里既有失败又有停着的腿时,先要人看的是那条失败的。
332
+ */
333
+ function bucketStatus(bucket) {
334
+ if (bucket.some(a => a.state === 'error'))
335
+ return 'failed';
336
+ if (bucket.length > 0 && bucket.every(a => a.state === 'done'))
337
+ return 'done';
338
+ if (bucket.some(a => a.state === 'parked') && bucket.every(a => a.state === 'parked' || a.state === 'done')) {
339
+ return 'parked';
340
+ }
341
+ return 'running';
342
+ }
343
+ /** 合成 `Workflow` 单格(路径三:wire 既无结构化 phases 也无 agent 上的 phase 标)的状态词。
344
+ * 跟 run 走(同 {@link coerceRunStatus} 的臂),run 说不出 `parked` 时再从腿上推一次 —— 今天真正
345
+ * 会带 `parked` 的正是腿那一级,而路径三的这一格代表的就是整只 run 的全部腿。 */
346
+ function synthPhaseStatus(runStatus, agents) {
347
+ if (runStatus === 'completed')
348
+ return 'done';
349
+ if (runStatus === 'parked')
350
+ return 'parked';
351
+ const derived = bucketStatus(agents);
352
+ if (derived === 'parked')
353
+ return 'parked';
354
+ return runStatus === 'failed' ? 'failed' : 'running';
355
+ }
299
356
  /** THE projector: SDK `WorkflowRun` (client.workflows.get) → the monitor's `WorkflowRunState`. Pure/total —
300
357
  * tolerates absent/permissive fields and never throws (the graceful-degrade contract lives above, in the
301
358
  * source loop; this just maps a well-formed run). Exported for the mock-parity unit cross-check. */
@@ -14,8 +14,13 @@
14
14
  * 留在 TUI 的:`computeVisibleWindow`(滚动窗口数学 = 视口概念,端专属)、glyph/color 映射、
15
15
  * 以及整棵 JSX。壳侧 shim = 那个 tsx 改成从本包 re-export 这两样,自己不再定义。
16
16
  */
17
- export type AgentState = 'start' | 'progress' | 'queued' | 'done' | 'error' | 'inactive';
18
- export type DisplayStatus = 'queued' | 'running' | 'done' | 'failed' | 'skipped' | 'interrupted';
17
+ export type AgentState = 'start' | 'progress' | 'queued' | 'done' | 'error' | 'inactive'
18
+ /** sema 超集:这条腿耐久挂在一张审批卡上(engine core 7.10.0 #642)。见上方头注。 */
19
+ | 'parked';
20
+ export type DisplayStatus = 'queued' | 'running' | 'done' | 'failed' | 'skipped' | 'interrupted'
21
+ /** sema 超集:同上。🔴 端的 glyph/颜色映射是**穷尽形**,加这一词就必须同批加一条 case
22
+ * (措辞按「waiting for approval」一类,别复用 running 的字与色)。 */
23
+ | 'parked';
19
24
  export interface WorkflowToolCall {
20
25
  name: string;
21
26
  summary?: string;
@@ -61,7 +66,10 @@ export interface WorkflowAgent {
61
66
  }
62
67
  export interface WorkflowPhase {
63
68
  title: string;
64
- status: 'not-started' | 'running' | 'done' | 'failed';
69
+ /** 🔴 `parked` = 这一格里没有一条腿能自己往前走(全员不是完成就是停在审批门上)。见 `AgentState`
70
+ * 的同名头注;上游今天不给 phase 铸这个词(core:「a phase or group never parks」),本包这一位有两个
71
+ * 来路:①上游哪天真铸了 ⇒ 原词透出而不是折成 running;②本包从桶里的腿**推**出来。 */
72
+ status: 'not-started' | 'running' | 'done' | 'failed' | 'parked';
65
73
  doneCount: number;
66
74
  totalCount: number;
67
75
  agents: WorkflowAgent[];
@@ -73,7 +81,11 @@ export interface WorkflowRunState {
73
81
  summary?: string;
74
82
  script: string;
75
83
  scriptPath?: string;
76
- status: 'running' | 'done' | 'failed' | 'stopped' | 'paused';
84
+ /** 🔴 `parked`(0.63.1,additive):**前向预挂** —— engine core 7.10.0 run 级词表仍闭在
85
+ * running|completed|failed(一条腿 park ⇒ 整只 run 按 `WorkflowAgentParkedError` 收在 `failed`),
86
+ * 所以今天没有任何受支持的引擎在这一位上发 `parked`。留这条臂是为了「词真到了就原样透出」,
87
+ * 绝不是说它现在会到 —— 判「这条 run 停着等人批」今天要看**腿**那一级(见 `AgentState.parked`)。 */
88
+ status: 'running' | 'done' | 'failed' | 'stopped' | 'paused' | 'parked';
77
89
  cloud?: boolean;
78
90
  agentCount: number;
79
91
  totalTokens: number;
@@ -97,5 +109,9 @@ export interface WorkflowRunState {
97
109
  * fields on the active path. The AgentState type carries exactly this input set, so the type contract now
98
110
  * matches the atlas vocabulary 1:1 — the branches below consume only atlas input states and the output
99
111
  * DisplayStatus set + glyph/color mapping ($jn) is identical.
112
+ *
113
+ * 🔴 **`parked` 是这条规则上唯一的 sema 超集臂**(0.63.1):atlas 没有它,因为 CC 的编排里没有耐久
114
+ * 审批这一层。它**不派生**——上游直接铸这个词(engine core 7.10.0 #642 的 agent 行 `status` 与
115
+ * `deriveAgentDisplayStatus` 产物),这里只是不许把它折掉。臂的**位置**是承重的,理由写在分支处。
100
116
  */
101
117
  export declare function agentDisplayStatus(a: WorkflowAgent, workflowActive: boolean): DisplayStatus;
@@ -29,6 +29,10 @@
29
29
  * fields on the active path. The AgentState type carries exactly this input set, so the type contract now
30
30
  * matches the atlas vocabulary 1:1 — the branches below consume only atlas input states and the output
31
31
  * DisplayStatus set + glyph/color mapping ($jn) is identical.
32
+ *
33
+ * 🔴 **`parked` 是这条规则上唯一的 sema 超集臂**(0.63.1):atlas 没有它,因为 CC 的编排里没有耐久
34
+ * 审批这一层。它**不派生**——上游直接铸这个词(engine core 7.10.0 #642 的 agent 行 `status` 与
35
+ * `deriveAgentDisplayStatus` 产物),这里只是不许把它折掉。臂的**位置**是承重的,理由写在分支处。
32
36
  */
33
37
  export function agentDisplayStatus(a, workflowActive) {
34
38
  if (a.state === 'done')
@@ -36,6 +40,12 @@ export function agentDisplayStatus(a, workflowActive) {
36
40
  // atlas: `state==="error"` → skipped?"skipped":"failed".
37
41
  if (a.state === 'error')
38
42
  return a.skipped ? 'skipped' : 'failed';
43
+ // 🔴 sema 超集臂,**位置承重**:必须在 `!workflowActive` 之前判。一条腿 park 之后 run 自己收在
44
+ // `failed`(core `WorkflowAgentParkedError`)⇒ `workflowActive` 恒假 ⇒ 放到后面就永远被
45
+ // `interrupted` 吃掉,而 interrupted 说的是「run 结束时它还没跑完」——那是一件**已经过去**的事,
46
+ // parked 说的是「它还挂在那儿,等一个人做决定」,是一件**还能动手**的事。两句下一步不同。
47
+ if (a.state === 'parked')
48
+ return 'parked';
39
49
  // atlas `!t` (workflow not active) → interrupted (also the `inactive`-state landing).
40
50
  if (!workflowActive)
41
51
  return 'interrupted';