@sema-agent/client-core 0.73.4 → 0.74.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/CHANGELOG.md +41 -0
- package/README.md +4 -2
- package/dist/engineAgentPanelStore.d.ts +29 -0
- package/dist/engineAgentPanelStore.js +31 -1
- package/dist/fleet/fleetLedger.d.ts +20 -0
- package/dist/fleet/fleetLedger.js +103 -10
- package/dist/fleet/workflowSizeWarning.d.ts +96 -0
- package/dist/fleet/workflowSizeWarning.js +155 -0
- package/dist/fleetAgentPanelProjection.d.ts +2 -0
- package/dist/fleetAgentPanelProjection.js +42 -7
- package/dist/hitl/planReviewWire.js +10 -0
- package/dist/hitl/toolApprovalWire.d.ts +7 -1
- package/dist/hitl/toolApprovalWire.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/notifications.js +3 -0
- package/dist/subagentContentStore.d.ts +2 -0
- package/docs/INTEGRATION-CLIENTS.md +101 -8
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,47 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.74.0(2026-09-19)
|
|
53
|
+
|
|
54
|
+
> 主题:**peer sdk 地板抬到 `>=9.7.1`** + **审批卡 `readRootCandidate.covers: "exact"` 过境**(CC-64)+ **面板事件周期身份**(CC-66)+ **fleet 行离场读口**(CC-65)+ 三版缺口行更正。**minor**:peer 地板抬升是非 additive 变化(<9.7.1 的宿主装本版会收到 peer 警告);型面本身纯 additive(+1 函数,公面 1044 → 1045;`ReadRootCandidateView` / `EngineAgentPanelEvent` 两臂 / `FleetAgentRowLike` / `FleetLedgerHooks` / `BgTerminalFacts` 各加可选位;零 wire 新键)。
|
|
55
|
+
|
|
56
|
+
### Changed(🔴 peer)
|
|
57
|
+
|
|
58
|
+
- **peer `@sema-agent/sdk` 地板 `>=9.6.0` → `>=9.7.1`**(peerDependencies / lockfile / README 地板句 / `run-sdk-floor-test.mjs` 的 `FLOOR` 四处同批抬齐;devDep 自 0.73.3 起已是 `^9.7.1`)。理由:sdk 9.7.x 追平的六处型面(`Capabilities.approvalsStreamLive` / `.executionLane`、`LivePendingRow.frame`、`live_*` 审批流事件、`gates[].toolCallId`)是本包 0.73.3 起的编译基线;消费端实装已全部在 9.7.1,`>=9.6.0` 这条线没有任何物料证明跑过(public-surface 门 C-CC-3 当场红)。承诺的下线必须有见证,所以抬到有见证的那条。
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- **`readRootCandidate.covers: "exact"` 被本包剥掉**(CC-64;core ≥7.22.0 / server ≥7.85.0;core 7.23.0 起它真的到得了卡)。在场 = `dir` 这一串**不是目录**,是命令点名的那条路径本身(`cat ~/.gitconfig` 的 `~/.gitconfig`);上游契约的硬话是「消费端必须逐字添加这一串」—— 展成目录模式授不到任何东西、卡还在。本包的窄读此前只铸 `dir` / `clearsThisAsk` 两键形,于是端在结构上分不出两种出路,只能一律按目录渲。现在:`ReadRootCandidateView` +`covers?: 'exact'`,在场逐字上卡;**缺席 = 目录出路,卡上的形逐字节同旧**;出闭集(别的词 / 空串 / 非字符串 / 键在场而值 undefined)⇒ **整只座不过境**,不是摘掉坏成员其余照发(两种读法在人那一侧是相反的动作)。
|
|
63
|
+
- **更正 0.73.3 / 0.73.4 / 0.73.5 三版接入档缺口行的一句话**:「缺席上报附 `removeReason`(候壳透传)」写反了。宿主一直把 fleet 原始帧原样交给账本,`task_remove` 帧上的 `removeReason` / `cycleSeq` 逐字到得了本包;丢键的是本包。本版补读口(下)。
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **面板事件的周期身份(CC-66)**。后台 agent 同一个 `taskId` 可以复活;`EngineAgentPanelEvent` 的 `end` 臂此前只有 `taskId` / `isError` / `report`,上一周期迟到的 `end` 会把复活后的新一轮结掉(壳因此不敢把状态翻回 running)。现在 `fleet-row` 臂 +`cycleSeq?`,`end` 臂 +`cycleSeq?` / `startedAt?`:fleet 行车道在 wire 给了的时候带(`cycleSeq` = 引擎的 stop-cycle 计数,与后台通知的 `seq` 同域同轴;`startedAt` = launch 锚),通知车道在通知真带 `seq` 时带 `cycleSeq`;**流内 close 臂没有这个概念,恒缺席**。🔴 缺席 = 这一行没有代际概念(同步委派子代 / workflow agent / 顶层 run / 老引擎),**不是第一代**,绝不补 1。
|
|
68
|
+
- 判据单源 `isStaleEngineAgentPanelEnd(end, current)`:两边都有 `cycleSeq` ⇒ 只比它(小 = 陈旧);否则两边都有 `startedAt` ⇒ 比它;任一边判不出 ⇒ **不陈旧**(现行为;不因缺席丢一条真终态)。陈旧的 `end` 消费端忽略(不 settle、不写 report、不摘所有权);终报异步回写同理按发起时记下的周期核。
|
|
69
|
+
- `cycleSeq` 变了 = 新周期:投影侧用量粘性不跨周期继承(与 launch 锚变了同一处置);事件缓冲的逐键合并也以它为屏障。`FleetAgentRowLike` +`cycleSeq?`(把 `FleetTaskView` 原样交进来即得)。
|
|
70
|
+
- 🔴 **前代迟到帧在包内就挡住**(异源对抗复审两轮):投影台账上已是更新的一代、这一帧却是更早的一代 ⇒ 整只跳过(不发 `fleet-row` 去改写消费端记的身份、不发 `end`、不动台账)—— 判定与 `isStaleEngineAgentPanelEnd` **同一条规则**(cycleSeq 优先、startedAt 回退,只带 startedAt 的前代帧同样挡住);settle 去重按周期(已 settle 的是上一代、来的是可证更新的一代 ⇒ 照发);周期身份推进本身算变化(用量等键全不变、首帧就是终态,也先发 `fleet-row` 让端记下身份再发 `end`);账本的通知退休腿与留存池更新对「通知 `seq` 早于行 / 留存条目 `cycleSeq`」的前代通知一律不改写,也不入终局事实台账;行退场后账本仍按 task 记**周期水位**(有界),零命中的前代通知同样挡住;终局事实台账带 `cycleSeq`(通知报了才记),洗绿保护只在同周期内生效 —— 第 1 代 failed 在账、第 2 代 completed 到是新事实不是洗绿。身份的沿用与用量的沿用分开:同周期重复的终态帧不抹掉记住的 `startedAt` / `cycleSeq`。
|
|
71
|
+
- **fleet 行离场读口(CC-65)**:`FleetLedgerHooks` +`onTaskRemoved(removal)`,`FleetTaskRemoval = { id, removeReason?, cycleSeq?, stale?, unknownRow? }`。`removeReason`(开集原词;今天只有 `orphaned`)与 `cycleSeq`(被退那一代)在场才带。退的是**前一代**而账本里这一行已是更新的一代 ⇒ 前代迟到帧:**不删行**,读口照报并标 `stale`;账本本来就没这一行(晚连接者)⇒ 照报并标 `unknownRow`。回调抛错被吞(含 async 回调的 reject)。🔴 离场**不是终局证据**(契约附录 H):读口只陈述离场,本包不据它 settle;不装读口 ⇒ 行为逐字同旧。
|
|
72
|
+
- 门:新 `scripts/run-panel-cycle-identity-test.mjs`(57 格);`run-client-core-pure-test.mjs` +CC-64a–d。变异反证三 + 二十一枚逐格见红。
|
|
73
|
+
- `BgTerminalFacts` +`cycleSeq?`(additive)。
|
|
74
|
+
|
|
75
|
+
## 0.73.5(2026-09-19)
|
|
76
|
+
|
|
77
|
+
> 主题:**workflow 规模告警判定上收进包**(CC-58)+ plan review 陈旧重开卡作答的 outcome 措辞。**patch**:型面纯 additive(+8 常量 +3 函数,公面 1033 → 1044);零 wire 新键;peer 不动。
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- **`src/fleet/workflowSizeWarning.ts`(CC-58)**:三端 footer / 面板共用的「这条 workflow 是不是大到该提醒」判定,与 CC 同形(三个缺省帽 25 / 1 500 000 / 每 agent 先验 70 000、guideline 表 small 5 / medium 15 / large 50、两个 env 覆盖、三档文案与宽度阶梯),此前各端自己抄一份。
|
|
82
|
+
- `evaluateWorkflowSize(input)`:入参按 fleet 视图的**可选键**读(`totalCount` / `startedCount` / `doneCount` / `failedCount` / `tokens`,缺席 = 不知道),出参三态 `warn` / `ok` / `unknown`。🔴 **未知的规模不能被当作规模正常**:没有超帽的正向证据、又有一条轴判不出 ⇒ `unknown`,`missing` 点名缺键;任一轴凭在场的键就能证明超帽 ⇒ `warn`(别的键缺席不挡它)。
|
|
83
|
+
- 分母口径:`startedCount` 在场用引擎真值(真 0 ⇒ 先验;比 `doneCount + failedCount` 还小 = 陈旧读数,按下界处置);缺席时只有两键**都在场**才回退成下界,而下界只能算出投影的**上界**(`projectedTokensUpperBound`,与 `projectedTokens` 互斥,结果标 `startedIsLowerBound`):上界在帽内 ⇒ 真的在帽内 ⇒ `ok`;上界超帽**不是**超帽的证据(同一份读数补上真 `startedCount` 可能远在帽内)⇒ `unknown`,上界材料照给,要不要保守提示由端定;只有一键在场不回退;下界为 0 ⇒ 判不出,先验只在引擎亲口报 `startedCount = 0` 时可用。
|
|
84
|
+
- 帽优先级 env > 显式 guideline > 缺省;`unrestricted`、认不出的词与原型键不贡献帽;ultracode 压掉整条告警(`ok` + `suppressed`)。
|
|
85
|
+
- `readWorkflowSizeWarningEnvCaps(env)`:纯函数,`env` 由调用方交(本包不读 `process.env`);名字单源 `WORKFLOW_SIZE_WARNING_ENV_NAMES`(品牌名在前、兼容名在后);首个在场名的值不合形(含空串 / 纯空白)⇒ 键不在场,不回落第二名。
|
|
86
|
+
- `workflowSizeWarningTextForWidth(available, width)` + 三档文案常量。
|
|
87
|
+
- 新门 `scripts/run-workflow-size-warning-test.mjs`(51 格);变异反证十七枚逐格见红。
|
|
88
|
+
|
|
89
|
+
### Fixed
|
|
90
|
+
|
|
91
|
+
- **plan review 决断撞 404 `not_found.plan_review` 的 outcome 正文**:这道门已经不在了(早先的决断已生效,或已过期)—— 现场形是用户在一张陈旧的重开卡上作答。通用句「decision failed: HTTP 404 …」会被模型读成「计划被拒 / 任务失败」并据此向用户汇报、重新规划,而那份计划其实已批准并跑完。现在该码有专句:明说引擎已没有待决的 plan review、早先的决断多半已生效、**这不是拒绝也不是失败**;效果态仍是 `not_applied`(尾句不变:不说已批准 / 已拒绝,指路 `TaskOutput`)。别的 404 码照旧走通用句。
|
|
92
|
+
|
|
52
93
|
## 0.73.4(2026-09-19)
|
|
53
94
|
|
|
54
95
|
> 主题:**终帧 `permission_denials` 第三键的来源**(CC-34 重开)、**审批 feed「欠一次看」重试**(CC-62)与 **workflow run 级错误码**(CC-63)。**patch**:型面纯 additive(+1 常量,公面 1032 → 1033;`TerminalStreamObserved` 具名化并 +1 可选位;`SemaPermissionDenial` +1 可选位;TaskOutput 卡 `task` +1 超集键;`ApprovalsFeedOptions` +1 可选位;`ApprovalsFeedStats` +1 计数);零 wire 新键;peer 不动。
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
35
35
|
|
|
36
36
|
## Scope
|
|
37
37
|
|
|
38
|
-
**Version:** 0.
|
|
38
|
+
**Version:** 0.74.0
|
|
39
39
|
|
|
40
40
|
- **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
|
|
41
41
|
B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
|
|
@@ -67,7 +67,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
67
67
|
against — the tables live upstream precisely so this package does not keep a second copy that can
|
|
68
68
|
fall behind. The browser bundle really bundles the SDK through (the portability guard would
|
|
69
69
|
exit 3 rather than quietly mark it external).
|
|
70
|
-
- The declared floor is `>=9.6.0`
|
|
70
|
+
- The declared floor is `>=9.7.1` (raised from `>=9.6.0` in 0.74.0: `Capabilities.approvalsStreamLive` / `.executionLane`, `LivePendingRow.frame`, the `live_*` approval-stream events and `gates[].toolCallId` are typed from 9.7.x on, and no consumer ships 9.6.0 any more, so the older floor lost its witness; before that raised from `>=9.4.0` in 0.71.0: the `tool_disclosure` / `tool_progress` frames and `ToolApprovalFrame.readRootCandidate` are typed there; earlier: raised from `>=8.8.0` in 0.69.0: the `reasoning_end` frame and `McpStatusPanel.lastLegMcp` are typed from 9.4.0 on), and it is *witnessed*: the guard checks that an actually
|
|
71
71
|
installed SDK at that line still exports every value-level symbol this package imports and still
|
|
72
72
|
declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
|
|
73
73
|
promise, not a contract.
|
|
@@ -313,6 +313,8 @@ public-surface guard checks that last one).
|
|
|
313
313
|
| `scripts/run-tool-disclosure-progress-projection-test.mjs` | The two wire arms sdk 9.6.0 adds — `tool_disclosure` (name-only tool census: open-set `policy`, `thresholdPercent` absent ≠ default, `deferred`/`activated` full snapshots) and `tool_progress` (one frame, two beats: Bash ticks carry an output tail with `totalLines`/`totalBytes` that come and go together; other tools carry only `elapsedSeconds`) — project to neutral internal arms plus chrome arms. Required keys missing ⇒ `malformed`; bad optional keys drop only themselves; the sub-flow three-key gate keeps child frames off the leader lane; both arms are `required: false` in the arm table with duties stated (the output tail is untrusted raw and must never be fed back to the model). |
|
|
314
314
|
| `scripts/run-mcp-panel-projection-test.mjs` | The `GET /v1/sessions/:id/mcp` panel reader (`projectMcpPanel`; server >=7.77.0 adds the optional `lastLegMcp` key) and the single wording mint for its "last leg" line. Absence of `lastLegMcp` is one literal sentence that never blames the engine version (a new session, a leg outside the retention window, a leg without a manifest and an older engine all look the same on the wire); a key that is present but unreadable is a different sentence plus a `lastLegMcpUnreadable: true` mark, never folded into absence. The `mcp[]` roster goes through the same reader as the live `wiring_manifest` third section, so a replayed roster and a live one have one shape. The two faces of the panel (`servers[]` and the last-leg roster) may legitimately differ, so the view carries no agreement flag and none of the five sentences mentions `servers`. Required keys are pinned to the SDK `openapi.yaml` component bytes **0.69.0:** `fetchMcpPanel` fetches the panel through the SDK client's own `sessions.mcp` call (same transport and auth as every other read) and projects it; transport failure, an unreadable body and an empty session id all come back as `undefined`, never as a fabricated empty panel 0.71.0 adds section K: `mcpEngineLegPresence(view)` — the engine-side MCP presence tri-state read only off the panel view (`unknown` when the view could not be read, never rendered as "no MCP configured") |
|
|
315
315
|
| `scripts/run-absence-fold-census-test.mjs` | A package-wide census of the "absence folded into a positive outcome" defect shape, so that fixing the six sites this release does not merely move the shape somewhere else. The defect is defined by position, not syntax: a fallback position (the unconditional tail return, the `default:` arm, the literal minted when there is nothing to pass on, the value returned from an error path) may only say `unknown` or stay absent, never a positive word. Detection walks the syntax tree of every source file, so comments, strings and multi-line spellings cannot hide or fake a hit, and covers five forms: the right arm of `??` / `||`, the else arm of a ternary, the first return of an explicit `default:`, a `catch` block or `.catch(() => …)` arrow returning a healthy value, and a function whose last statement returns a positive word after other returns. Every remaining hit must be registered with a written reason, an unregistered hit fails the gate naming the file and line, the registered count must equal the real count so a cleared site cannot leave a spare allowance behind, and the gate proves its own teeth behind a fence (a failed self-proof refuses to report any count): each form injected into an in-memory copy must add exactly one hit, two correct spellings are pinned as non-hits, and samples inside comments or strings do not count. It also pins the headline site: the fleet panel projection no longer mints an `end` with `isError: false` on absence |
|
|
316
|
+
| `scripts/run-panel-cycle-identity-test.mjs` | The **cycle identity** on agent-panel events and the fleet ledger's **departure read-out**: a background agent may be revived under the same id, so `fleet-row` and `end` events now carry the wire's own `cycleSeq` / `startedAt` when present (absent means the row has no notion of generations, never "generation one"), `isStaleEngineAgentPanelEnd` is the single rule for ignoring a late `end` from a previous cycle (only when both sides carry a comparable identity; absence never drops a real terminal), a changed `cycleSeq` is a new cycle for usage stickiness and buffer coalescing, the notification lane carries `cycleSeq` only when the wire really sent `seq`, and `task_remove` frames reach the host through `onTaskRemoved` with `removeReason` / `cycleSeq` verbatim, a stale previous-generation removal leaving the newer row in place |
|
|
317
|
+
| `scripts/run-workflow-size-warning-test.mjs` | The **workflow size warning** verdict shared by every host footer / panel: a three-state result (`warn` / `ok` / `unknown`) read off the optional fleet view keys, where an unknown size is never reported as a normal one (absent `totalCount` / `tokens` without positive over-cap evidence is `unknown`, naming the missing keys), positive evidence on either axis wins regardless of absent keys, the per-agent denominator uses the engine's started count only when it is not below done+failed (a smaller value is a stale reading), otherwise falls back to the done+failed lower bound only when both keys are present — and a lower-bound denominator only yields an upper bound of the projection, which can prove *within cap* (`ok`, flagged) but never *over cap* (`unknown`, with the upper bound exposed) — and the prior is used only when the engine itself reports zero started agents; cap precedence env > explicit guideline > default, prototype keys never act as a guideline, the env reader is pure and does not fall through to the second name on a bad first value; caps, guideline table, env names and the three copy variants are single-sourced |
|
|
316
318
|
| `scripts/run-approvals-stream-live-capability-test.mjs` | The engine's live-approval-push self-description (`capabilities.approvalsStreamLive`, engine ≥7.87.1), read the same four-state way as its four sibling capability readers: an absent key is reported as not reported (never folded into `false`), the value must be a strict boolean, and the one decision the feed consumer needs — whether it must keep pulling suspended asks itself — is answered by `livePendingNeedsReconcile`, which only says no when the engine explicitly says it pushes. |
|
|
317
319
|
| `scripts/run-model-facing-anchors-test.mjs` | Five places where model-facing engine text is parsed back into card slots: the task-output cursor semantics machine-readable key wins over the spool marker text (and its absence is never read as `full`), the `--- result (…) ---` line accepts a parenthesised note, the bash header accepts the engine's exit-note and image variants and carries the note into the same benign-exit slot the structured path uses, the notification seed scan treats both producers' terminal words as terminal, and the parked background-task phrase is recognised. |
|
|
318
320
|
| `scripts/run-execution-lane-capability-test.mjs` | The deployment execution-lane self-description (`capabilities.executionLane`, engine ≥7.86.0), read the same four-state way as its three sibling capability readers. An absent key is reported as "not reported" and never as "tools do not run on this host": on an older engine the client keeps inferring the lane the way it did before, because reading absence as `false` would silently stop every host deployment from sending skill `baseDir`. The implication is one-way (`toolsOnThisHost: false` means `baseDir` is never sent; `true` is only a necessary condition), `provider` is read as an open non-empty string rather than a hand-copied closed set and is sanitised and bounded before display, `toolsOnThisHost` must be a strict boolean, extra members never cross, the tee never throws, stale generations are dropped whole, and the single "do tools run here" predicate uses the bit when it is present and the caller's own inference, unchanged, when it is not |
|
|
@@ -94,10 +94,24 @@ export type EngineAgentPanelEvent = {
|
|
|
94
94
|
* 渲出 1h+ 假时长。消费端拿到即覆盖行的 startTime;缺席 ⇒ 老引擎,维持现状。
|
|
95
95
|
*/
|
|
96
96
|
startedAt?: number;
|
|
97
|
+
/**
|
|
98
|
+
* 0.74.0 —— 这一行的**代际号**(引擎的 stop-cycle 计数;fleet 行帧 `cycleSeq` 与后台通知 `seq` 同域同轴:
|
|
99
|
+
* 首次派发 = 1,每次复活 +1)。消费端把它记在行上,用 {@link isStaleEngineAgentPanelEnd} 判 `end` 是不是上一周期迟到的。
|
|
100
|
+
* 🔴 缺席 = 这一行**没有代际概念**(同步委派子代 / workflow agent / 顶层 run / 老引擎),**不是第一代**。
|
|
101
|
+
*/
|
|
102
|
+
cycleSeq?: number;
|
|
97
103
|
} | {
|
|
98
104
|
kind: 'end';
|
|
99
105
|
taskId: string;
|
|
100
106
|
isError: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* 0.74.0 —— 这条终态属于**哪一个周期**。同一个 taskId 可以复活;没有周期身份时,上一周期迟到的 `end` 会把
|
|
109
|
+
* 复活后的新一轮结掉。fleet 行车道与通知车道在 wire 给了的时候带;**流内 close 臂没有这个概念,恒缺席**。
|
|
110
|
+
* 读法单源:{@link isStaleEngineAgentPanelEnd}。缺席 ⇒ 判不出 ⇒ 按现行为 settle(不因缺席丢一条真终态)。
|
|
111
|
+
*/
|
|
112
|
+
cycleSeq?: number;
|
|
113
|
+
/** 0.74.0 —— 同上,launch 锚(epoch ms);只有 fleet 行车道带得出。 */
|
|
114
|
+
startedAt?: number;
|
|
101
115
|
/** The subagent's FINAL REPORT — the delegating Task/Agent card's `tool_end.output` text (only the
|
|
102
116
|
* explicit close arm carries it; the defensive sweeps settle without one). The consumer persists it
|
|
103
117
|
* on the row (task.result) + appends it to a live-viewed transcript (拍1 查看态 seed). */
|
|
@@ -117,6 +131,21 @@ export declare function markEnginePanelTaskResident(taskId: string): void;
|
|
|
117
131
|
export declare function clearEnginePanelTaskResident(taskId: string): void;
|
|
118
132
|
export declare function isEnginePanelTaskResident(taskId: string): boolean;
|
|
119
133
|
type Listener = (ev: EngineAgentPanelEvent) => void;
|
|
134
|
+
/** 周期身份的一格(`end` 事件上的,或消费端记在行上的当前周期)。 */
|
|
135
|
+
export interface EngineAgentPanelCycleIdentity {
|
|
136
|
+
readonly cycleSeq?: number;
|
|
137
|
+
readonly startedAt?: number;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 0.74.0 —— 这条 `end` 是不是**上一周期迟到的**(单源判据,三端共用)。`current` = 消费端记在行上的当前周期
|
|
141
|
+
* (取自最近一条 `fleet-row` 的 `cycleSeq` / `startedAt`)。
|
|
142
|
+
* · 两边都有 `cycleSeq` ⇒ **只**比它:`end` 更小 ⇒ 陈旧;
|
|
143
|
+
* · 否则两边都有 `startedAt` ⇒ `end` 更早 ⇒ 陈旧;
|
|
144
|
+
* · 任一边判不出(流内 close 臂的 `end` / 没有代际概念的行 / 老引擎 / 坏形)⇒ **不陈旧** —— 按现行为 settle。
|
|
145
|
+
* 方向是宁可照旧也不因为缺席丢掉一条真终态。
|
|
146
|
+
* 陈旧的 `end`:忽略(不 settle、不写 report、不摘所有权)。
|
|
147
|
+
*/
|
|
148
|
+
export declare function isStaleEngineAgentPanelEnd(end: EngineAgentPanelCycleIdentity | null | undefined, current: EngineAgentPanelCycleIdentity | null | undefined): boolean;
|
|
120
149
|
export declare function publishEngineAgentPanelEvent(ev: EngineAgentPanelEvent): void;
|
|
121
150
|
/** Single consumer (the panel hook). Replays any buffered events, returns an unsubscribe. */
|
|
122
151
|
export declare function subscribeEngineAgentPanel(fn: Listener): () => void;
|
|
@@ -52,12 +52,42 @@ export function isEnginePanelTaskResident(taskId) {
|
|
|
52
52
|
const MAX_BUFFER = 200;
|
|
53
53
|
let listener = null;
|
|
54
54
|
const buffer = [];
|
|
55
|
-
/** 两条 fleet-row
|
|
55
|
+
/** 两条 fleet-row 都带同一种周期身份(launch 锚 / 代际号)且不相等 ⇒ 新周期(任一条没带 = 这一帧没说,不算变化)。 */
|
|
56
56
|
function launchAnchorChanged(prev, next) {
|
|
57
57
|
if (prev.kind !== 'fleet-row' || next.kind !== 'fleet-row')
|
|
58
58
|
return false;
|
|
59
|
+
if (prev.cycleSeq !== undefined && next.cycleSeq !== undefined && prev.cycleSeq !== next.cycleSeq)
|
|
60
|
+
return true;
|
|
59
61
|
return prev.startedAt !== undefined && next.startedAt !== undefined && prev.startedAt !== next.startedAt;
|
|
60
62
|
}
|
|
63
|
+
function cycleSeqOf(v) {
|
|
64
|
+
const n = v?.cycleSeq;
|
|
65
|
+
return typeof n === 'number' && Number.isInteger(n) && n >= 1 ? n : undefined;
|
|
66
|
+
}
|
|
67
|
+
function startedAtOf(v) {
|
|
68
|
+
const n = v?.startedAt;
|
|
69
|
+
return typeof n === 'number' && Number.isFinite(n) && n > 0 ? n : undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 0.74.0 —— 这条 `end` 是不是**上一周期迟到的**(单源判据,三端共用)。`current` = 消费端记在行上的当前周期
|
|
73
|
+
* (取自最近一条 `fleet-row` 的 `cycleSeq` / `startedAt`)。
|
|
74
|
+
* · 两边都有 `cycleSeq` ⇒ **只**比它:`end` 更小 ⇒ 陈旧;
|
|
75
|
+
* · 否则两边都有 `startedAt` ⇒ `end` 更早 ⇒ 陈旧;
|
|
76
|
+
* · 任一边判不出(流内 close 臂的 `end` / 没有代际概念的行 / 老引擎 / 坏形)⇒ **不陈旧** —— 按现行为 settle。
|
|
77
|
+
* 方向是宁可照旧也不因为缺席丢掉一条真终态。
|
|
78
|
+
* 陈旧的 `end`:忽略(不 settle、不写 report、不摘所有权)。
|
|
79
|
+
*/
|
|
80
|
+
export function isStaleEngineAgentPanelEnd(end, current) {
|
|
81
|
+
const ec = cycleSeqOf(end);
|
|
82
|
+
const cc = cycleSeqOf(current);
|
|
83
|
+
if (ec !== undefined && cc !== undefined)
|
|
84
|
+
return ec < cc;
|
|
85
|
+
const es = startedAtOf(end);
|
|
86
|
+
const cs = startedAtOf(current);
|
|
87
|
+
if (es !== undefined && cs !== undefined)
|
|
88
|
+
return es < cs;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
61
91
|
export function publishEngineAgentPanelEvent(ev) {
|
|
62
92
|
// 同 taskId 的任何主通道事实(tick / fleet-row = 行在场;end = 真终态)都让**缓冲里**那条缺席过期 —— 迟订阅的消费者
|
|
63
93
|
// 不许先恢复了行、再被一条旧缺席标成 unknown(已递达订阅者的缺席由订阅者按契约在这些事件上撤标)。
|
|
@@ -265,7 +265,27 @@ export type HookNoticeFrame = Extract<FleetFrame, {
|
|
|
265
265
|
* 开集只在本包加臂时扩,端按未知词兜底(`default` 当 `absent_parent` 一档处理最安全)。
|
|
266
266
|
*/
|
|
267
267
|
export type BgNotificationAcceptEvidence = 'server_fail_closed' | 'own_root' | 'session_anchor_untrusted' | 'own_parent' | 'absent_parent';
|
|
268
|
+
/**
|
|
269
|
+
* 0.74.0 —— 一条 fleet 行**离场**(`task_remove` 帧)的读口载荷。
|
|
270
|
+
* 🔴 离场**不是终局证据**:`task_remove` 在任何形态下都只说「这一行离场了」,结算的属主在 durable 侧;
|
|
271
|
+
* 要终局去读 run 面,本包不据它 settle。`removeReason`(开集原词;今天只有 `orphaned`)与 `cycleSeq`
|
|
272
|
+
* (被退那一代的代际号)**在场才带**,缺席不编。
|
|
273
|
+
*/
|
|
274
|
+
export interface FleetTaskRemoval {
|
|
275
|
+
readonly id: string;
|
|
276
|
+
readonly removeReason?: string;
|
|
277
|
+
readonly cycleSeq?: number;
|
|
278
|
+
/** 只在这条退场帧退的是**前一代**、而账本里这一行已经是更新的一代时在场:前代迟到帧,**行没有被删**。 */
|
|
279
|
+
readonly stale?: true;
|
|
280
|
+
/** 只在账本里本来就没有这一行时在场(晚连接者收到的退场帧;端可以直接忽略)。 */
|
|
281
|
+
readonly unknownRow?: true;
|
|
282
|
+
}
|
|
268
283
|
export interface FleetLedgerHooks {
|
|
284
|
+
/**
|
|
285
|
+
* 0.74.0 —— `task_remove` 帧的读口(见 {@link FleetTaskRemoval})。不给 = 行为逐字同旧(只删行)。
|
|
286
|
+
* 回调抛错被吞(fail-soft):宿主的渲染 bug 不许打断账本。
|
|
287
|
+
*/
|
|
288
|
+
onTaskRemoved?(removal: FleetTaskRemoval): void;
|
|
269
289
|
/**
|
|
270
290
|
* hook_notice 帧的**写 store** 半场(判定在 `classifyHookNoticeFrame`,本台账只做分发)。
|
|
271
291
|
* 返回值忽略;不给 = 该帧被静默丢弃(端没有承载面时的正确行为)。
|
|
@@ -8,7 +8,7 @@ import { clearBgTerminalFacts, getBgTerminalFacts, isOwnEngineRun, recordBgParen
|
|
|
8
8
|
import { rowIdTail } from '../workflow.js';
|
|
9
9
|
import { recordEngineTranscriptId } from '../subagent/engineDelegatedPrompt.js';
|
|
10
10
|
import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
11
|
-
import { TERMINAL_FLEET_TASK_STATUSES, projectTasks, projectWorkflows, wireParentId, } from './fleetProjection.js';
|
|
11
|
+
import { TERMINAL_FLEET_TASK_STATUSES, projectTasks, projectWorkflows, wireCycleSeq, wireParentId, } from './fleetProjection.js';
|
|
12
12
|
/** 终态行留存宽限窗(187 的 agents 页把完成会话留在列表;窗过即净,防长会话堆行)。 */
|
|
13
13
|
export const TERMINAL_RETAIN_MS = 60_000;
|
|
14
14
|
/** REF-CC-044(fleet2-06):per-sessionKey 注册表(W1 同款 `Map<sessionKey, T>` 形)。 */
|
|
@@ -126,6 +126,26 @@ export function createFleetLedger(hooks = {}, opts = {}) {
|
|
|
126
126
|
* settle 过 —— 在飞读面(`bgView.read`)对带标且未过窗的条目**继续按在飞计**(E4-7 解耦,
|
|
127
127
|
* 见 `EngineActiveBgTask.retiredByNotification` 顶注);行帧路径写入的条目从不带标。 */
|
|
128
128
|
const retained = new Map();
|
|
129
|
+
/**
|
|
130
|
+
* 0.74.0(异源对抗复审 R3)—— 每个 task 见过的最高代际(按行 id 尾段);**行退场后仍留**(有界 LRU):零命中的
|
|
131
|
+
* 前代通知(行已退场、留存已过窗)没有别的判据可挡,靠它挡在事实台账之外。
|
|
132
|
+
*/
|
|
133
|
+
const cycleWatermark = new Map();
|
|
134
|
+
const CYCLE_WATERMARK_MAX = 512;
|
|
135
|
+
function raiseCycleWatermark(tail, gen) {
|
|
136
|
+
if (gen === undefined || tail.length === 0)
|
|
137
|
+
return;
|
|
138
|
+
const prior = cycleWatermark.get(tail);
|
|
139
|
+
if (prior !== undefined && prior >= gen)
|
|
140
|
+
return;
|
|
141
|
+
cycleWatermark.delete(tail);
|
|
142
|
+
if (cycleWatermark.size >= CYCLE_WATERMARK_MAX) {
|
|
143
|
+
const oldest = cycleWatermark.keys().next().value;
|
|
144
|
+
if (oldest !== undefined)
|
|
145
|
+
cycleWatermark.delete(oldest);
|
|
146
|
+
}
|
|
147
|
+
cycleWatermark.set(tail, gen);
|
|
148
|
+
}
|
|
129
149
|
/** REF-CC-051(fleet2-13):畸形帧/通知早退计数(与同文件的策略性丢弃 debug 行对称留痕)。 */
|
|
130
150
|
let droppedMalformed = 0;
|
|
131
151
|
/** [2393] notif-F12:另两条丢弃路径的计数(未知帧型 / 隔离优先丢弃)。 */
|
|
@@ -290,6 +310,7 @@ export function createFleetLedger(hooks = {}, opts = {}) {
|
|
|
290
310
|
// REF-CC-039(fleet2-01):同一个字段只有一个铸点 —— 本地接收时刻,不是 `frame.ts`。
|
|
291
311
|
const receivedAtMs = nowFn();
|
|
292
312
|
taskMap.set(row.id, row); // POST-MERGE FULL row → replace by id
|
|
313
|
+
raiseCycleWatermark(rowIdTail(row.id), wireCycleSeq(row)); // 0.74.0:周期水位(退场后仍留)
|
|
293
314
|
rowMeta.set(row.id, { receivedAtMs });
|
|
294
315
|
debug(`[fleet-row] id=${row.id} parent=${row.parentId ?? '-'} agentType=${row.agentType ?? '-'}` +
|
|
295
316
|
` agentName=${row.agentName ?? '-'} name=${row.name ?? '-'} status=${row.status ?? '-'}` +
|
|
@@ -355,10 +376,42 @@ export function createFleetLedger(hooks = {}, opts = {}) {
|
|
|
355
376
|
}
|
|
356
377
|
break;
|
|
357
378
|
}
|
|
358
|
-
case 'task_remove':
|
|
359
|
-
|
|
360
|
-
|
|
379
|
+
case 'task_remove': {
|
|
380
|
+
// 0.74.0:退场帧带**被退那一代**的代际号。行已经是更新的一代(两边都报了且帧上更小)⇒ 这是前代迟到帧,
|
|
381
|
+
// 删了就把复活后的行抹掉 ⇒ 不删。任一边没报 ⇒ 判不出 ⇒ 现行为(删)。
|
|
382
|
+
const fr = frame;
|
|
383
|
+
const removedGen = typeof fr.cycleSeq === 'number' && Number.isInteger(fr.cycleSeq) && fr.cycleSeq >= 1 ? fr.cycleSeq : undefined;
|
|
384
|
+
const held = taskMap.get(frame.id);
|
|
385
|
+
const heldGen = held !== undefined ? wireCycleSeq(held) : undefined;
|
|
386
|
+
const stale = removedGen !== undefined && heldGen !== undefined && removedGen < heldGen;
|
|
387
|
+
if (!stale) {
|
|
388
|
+
taskMap.delete(frame.id);
|
|
389
|
+
rowMeta.delete(frame.id);
|
|
390
|
+
}
|
|
391
|
+
if (hooks.onTaskRemoved !== undefined) {
|
|
392
|
+
const reason = typeof fr.removeReason === 'string' && fr.removeReason.length > 0 ? fr.removeReason : undefined;
|
|
393
|
+
try {
|
|
394
|
+
const out = hooks.onTaskRemoved({
|
|
395
|
+
id: frame.id,
|
|
396
|
+
...(reason !== undefined ? { removeReason: reason } : {}),
|
|
397
|
+
...(removedGen !== undefined ? { cycleSeq: removedGen } : {}),
|
|
398
|
+
...(stale ? { stale: true } : {}),
|
|
399
|
+
...(held === undefined ? { unknownRow: true } : {}),
|
|
400
|
+
});
|
|
401
|
+
// 宿主给的可能是 async 回调:reject 同样要接住(与 onBgNotificationAccepted 同款),否则是 unhandledRejection。
|
|
402
|
+
if (out && typeof out.then === 'function') {
|
|
403
|
+
void Promise.resolve(out).catch((e) => {
|
|
404
|
+
hostLog('debug', `[fleet-frame] onTaskRemoved hook rejected: ${String(e).slice(0, 160)}`);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch (e) {
|
|
409
|
+
// 宿主读口的异常不许拆帧消费循环(与 onBgNotificationAccepted 同款);留痕走 hostLog(端的 debug 汇集面)。
|
|
410
|
+
hostLog('debug', `[fleet-frame] onTaskRemoved hook threw: ${String(e).slice(0, 160)}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
361
413
|
break;
|
|
414
|
+
}
|
|
362
415
|
case 'workflow':
|
|
363
416
|
if (frame.row?.id) {
|
|
364
417
|
wfMap.set(frame.row.id, frame.row);
|
|
@@ -551,10 +604,29 @@ export function createFleetLedger(hooks = {}, opts = {}) {
|
|
|
551
604
|
const writtenIds = new Set();
|
|
552
605
|
let applied = false;
|
|
553
606
|
let washBlocked = false;
|
|
607
|
+
/** 0.74.0:通知的 seq 早于行 / 留存条目的代际 ⇒ 前代通知;挡在状态面之外,也不入事实台账(与洗绿同一档)。 */
|
|
608
|
+
let staleBlocked = false;
|
|
609
|
+
const notifSeq = typeof n.seq === 'number' && Number.isInteger(n.seq) && n.seq >= 1 ? n.seq : undefined;
|
|
610
|
+
// 零命中路径(行已退场、留存已过窗)的前代判据 = 本 task 的周期水位;通知本身也抬水位(它是同一条轴上的读数)。
|
|
611
|
+
{
|
|
612
|
+
const mark = cycleWatermark.get(n.taskId);
|
|
613
|
+
if (notifSeq !== undefined && mark !== undefined && notifSeq < mark) {
|
|
614
|
+
staleBlocked = true;
|
|
615
|
+
debug(`[fleet-frame] bg_notification stale-cycle ignored(watermark=${mark} notif-seq=${notifSeq})`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
554
618
|
for (const id of rowIds) {
|
|
555
619
|
const known = taskMap.get(id);
|
|
556
620
|
if (!known)
|
|
557
621
|
continue;
|
|
622
|
+
// 0.74.0(异源对抗复审 R1):通知带的是**前一代**的 seq、而 live 行已是更新的一代 ⇒ 前代迟到通知,不退休这一行
|
|
623
|
+
// (退休会把行上的 cycleSeq 借给终态,消费端据此把仍在跑的新一轮结掉)。任一边没报 ⇒ 判不出 ⇒ 现行为。
|
|
624
|
+
const heldGen = wireCycleSeq(known);
|
|
625
|
+
if (notifSeq !== undefined && heldGen !== undefined && notifSeq < heldGen) {
|
|
626
|
+
staleBlocked = true;
|
|
627
|
+
debug(`[fleet-frame] bg_notification stale-cycle ignored(row=${id} row-cycle=${heldGen} notif-seq=${notifSeq})`);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
558
630
|
const rowStatus = known.status ?? '';
|
|
559
631
|
if (TERMINAL_FLEET_TASK_STATUSES.has(rowStatus)) {
|
|
560
632
|
// 行车道已 settle 本行(终态行帧在先):retained 已由 case 'task' 写入行帧真值。
|
|
@@ -615,6 +687,13 @@ export function createFleetLedger(hooks = {}, opts = {}) {
|
|
|
615
687
|
continue;
|
|
616
688
|
if (id !== n.taskId && rowIdTail(id) !== n.taskId)
|
|
617
689
|
continue;
|
|
690
|
+
// 0.74.0(异源对抗复审 R2):留存池这条独立成路的更新同样按周期判 —— 否则被 live 路挡掉的前代通知从这里改写新一代的结局。
|
|
691
|
+
const retainedGen = wireCycleSeq(entry.row);
|
|
692
|
+
if (notifSeq !== undefined && retainedGen !== undefined && notifSeq < retainedGen) {
|
|
693
|
+
staleBlocked = true;
|
|
694
|
+
debug(`[fleet-frame] bg_notification stale-cycle ignored(retained=${id} row-cycle=${retainedGen} notif-seq=${notifSeq})`);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
618
697
|
const prior = entry.row.status ?? '';
|
|
619
698
|
if (prior === n.status) {
|
|
620
699
|
applied = true; // 幂等重投(G5):同值不动、**不续窗**(回声风暴不得无限延长读面在飞窗)
|
|
@@ -638,15 +717,29 @@ export function createFleetLedger(hooks = {}, opts = {}) {
|
|
|
638
717
|
// completed 而面板渲 failed = 同一帧两写分叉,REF-CC-040 腿三的反面)。零命中且零拦截
|
|
639
718
|
// (行帧车道整条丢失、留存已过窗)⇒ 按既有事实台账自身做同方向核(killed/failed 在账,
|
|
640
719
|
// completed 回声不得洗绿它)。
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
720
|
+
// 0.74.0:事实台账带周期 —— 洗绿保护只在**同周期**内生效(第 1 代 failed 在账、第 2 代 completed 是新事实不是洗绿);
|
|
721
|
+
// 在账的周期比来的通知新 ⇒ 前代回声,不记。
|
|
722
|
+
const factsAccepted = !staleBlocked &&
|
|
723
|
+
(applied ||
|
|
724
|
+
(!washBlocked &&
|
|
725
|
+
(() => {
|
|
726
|
+
const prior = getBgTerminalFacts(n.taskId);
|
|
727
|
+
if (prior === undefined)
|
|
728
|
+
return true;
|
|
729
|
+
if (notifSeq !== undefined && prior.cycleSeq !== undefined) {
|
|
730
|
+
if (notifSeq < prior.cycleSeq)
|
|
731
|
+
return false;
|
|
732
|
+
if (notifSeq > prior.cycleSeq)
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
return !isWashGreen(prior.status, n.status);
|
|
736
|
+
})()));
|
|
737
|
+
if (factsAccepted)
|
|
738
|
+
raiseCycleWatermark(n.taskId, notifSeq);
|
|
647
739
|
if (factsAccepted) {
|
|
648
740
|
recordBgTerminalFacts(n.taskId, {
|
|
649
741
|
status: n.status,
|
|
742
|
+
...(notifSeq !== undefined ? { cycleSeq: notifSeq } : {}),
|
|
650
743
|
...(typeof n.summary === 'string' ? { summary: n.summary } : {}),
|
|
651
744
|
// 🔴 SDK 0.0.117 把 `recentSteps` 的三键 declare 成**必填** string,但那是**声称**不是保证:
|
|
652
745
|
// 帧从 wire 上来,脏项(null / 缺键 / 非串)在类型面之外仍可能到达,而 BgTerminalFacts
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/fleet/workflowSizeWarning.ts —— workflow **规模告警**的判定(0.73.5)。
|
|
3
|
+
*
|
|
4
|
+
* 三端 footer / 面板都要回答同一个问题:「这条 workflow 是不是大到该提醒用户了」。判定与 CC 同形
|
|
5
|
+
* (三个缺省帽、每 agent 用量估算、guideline 表、两个 env 覆盖、三档文案与宽度阶梯),属于客户端公共逻辑,
|
|
6
|
+
* 由本包单源;端只负责读环境、读设置、量宽度与渲染。
|
|
7
|
+
*
|
|
8
|
+
* 🔴 **入参按 fleet 视图的三态键读**(`FleetWorkflowView`:`totalCount` / `startedCount` / `doneCount` /
|
|
9
|
+
* `failedCount` / `tokens` 全是可选位,缺席 = wire 没报 = 不知道)。出参三态:
|
|
10
|
+
* · `warn` —— 有**正向证据**证明至少一条轴超帽(别的键缺席不挡它;下界分母算出的上界**不算**证据);
|
|
11
|
+
* · `ok` —— 两条轴都判得出,且都在帽内(或 ultracode 压掉了整条告警);
|
|
12
|
+
* · `unknown` —— 没有超帽的正向证据,且至少一条轴判不出。**未知的规模不能被当作规模正常**:
|
|
13
|
+
* `unknown` 绝不折成 `ok`,`missing` 点名缺了哪几键。
|
|
14
|
+
*
|
|
15
|
+
* 🔴 **分母口径**(每 agent 用量 `u = tokens / started`):
|
|
16
|
+
* · `startedCount` 在场且不小于 `doneCount + failedCount` ⇒ 用引擎真值(含真 0:「一个都还没起」是一条真信息
|
|
17
|
+
* ⇒ 用先验);比已完成数还小(帧间竞态)⇒ 这份读数是陈旧的,按下面的下界处置;
|
|
18
|
+
* · 缺席 ⇒ 只有 `doneCount` / `failedCount` **两键都在场**才回退成下界,结果标 `startedIsLowerBound`。
|
|
19
|
+
* 下界偏小 ⇒ `u` 偏大 ⇒ 算出来的是投影的**上界**(`projectedTokensUpperBound`,不是 `projectedTokens`):
|
|
20
|
+
* 上界在帽内 ⇒ 真的在帽内 ⇒ 可以判 `ok`;上界超帽**不是**超帽的证据(同一份读数补上真 `startedCount`
|
|
21
|
+
* 可能远在帽内)⇒ 投影轴判不出,`unknown`。想保守一点的端可以自己拿上界去提示,判词不替它撒谎;
|
|
22
|
+
* · 下界为 0 ⇒ 每 agent 用量判不出:「0 个完成」不是「一个没起」的证据,先验只在引擎**亲口**报
|
|
23
|
+
* `startedCount = 0` 时可用;
|
|
24
|
+
* · 只有一键在场 ⇒ 不回退(缺席的那键不读作 0)。
|
|
25
|
+
*/
|
|
26
|
+
/** 缺省 agent 数帽。 */
|
|
27
|
+
export declare const WORKFLOW_SIZE_WARNING_AGENT_CAP = 25;
|
|
28
|
+
/** 缺省 token 帽(已花 ∨ 投影)。 */
|
|
29
|
+
export declare const WORKFLOW_SIZE_WARNING_TOKEN_CAP = 1500000;
|
|
30
|
+
/** 还没有任何 agent 起跑时的每 agent 用量先验。 */
|
|
31
|
+
export declare const WORKFLOW_SIZE_WARNING_TOKENS_PER_AGENT = 70000;
|
|
32
|
+
/** 显式 `workflowSizeGuideline` → agent 数帽。`unrestricted` 与缺省(未配置)**不贡献帽**,故不在表里。 */
|
|
33
|
+
export declare const WORKFLOW_SIZE_GUIDELINE_AGENT_CAPS: Readonly<Record<'small' | 'medium' | 'large', number>>;
|
|
34
|
+
/** 两个 env 覆盖各自的名字,按读取优先级排(品牌名在前,兼容名在后)。本包不读 `process.env`,名字单源在这里。 */
|
|
35
|
+
export declare const WORKFLOW_SIZE_WARNING_ENV_NAMES: Readonly<{
|
|
36
|
+
agents: readonly [string, string];
|
|
37
|
+
tokens: readonly [string, string];
|
|
38
|
+
}>;
|
|
39
|
+
/** 三档文案(宽 → 窄)。 */
|
|
40
|
+
export declare const WORKFLOW_SIZE_WARNING_TEXT_LONG = "\u26A0 Large workflow \u00B7 /workflows to stop";
|
|
41
|
+
export declare const WORKFLOW_SIZE_WARNING_TEXT_SHORT = "\u26A0 Large workflow";
|
|
42
|
+
export declare const WORKFLOW_SIZE_WARNING_TEXT_MIN = "\u26A0";
|
|
43
|
+
export interface WorkflowSizeInput {
|
|
44
|
+
/** workflow 的 agent 总数(`FleetWorkflowView.totalCount`)。缺席 = 不知道。 */
|
|
45
|
+
readonly totalCount?: number;
|
|
46
|
+
/** 已起跑的 agent 数(引擎真值)。缺席 ⇒ 见头注的下界回退。 */
|
|
47
|
+
readonly startedCount?: number;
|
|
48
|
+
readonly doneCount?: number;
|
|
49
|
+
readonly failedCount?: number;
|
|
50
|
+
/** 已花 token(`FleetWorkflowView.tokens`)。缺席 = 不知道,**不是 0**。 */
|
|
51
|
+
readonly tokens?: number;
|
|
52
|
+
/** ultracode 档压掉整条告警。 */
|
|
53
|
+
readonly ultracodeActive?: boolean;
|
|
54
|
+
/** 设置里**显式配置**的 `workflowSizeGuideline`;未配置就别传(缺省档不贡献帽)。 */
|
|
55
|
+
readonly guidelineSize?: string;
|
|
56
|
+
/** env 覆盖(最高优先级);正有限数才生效。 */
|
|
57
|
+
readonly envAgentCap?: number;
|
|
58
|
+
readonly envTokenCap?: number;
|
|
59
|
+
}
|
|
60
|
+
export type WorkflowSizeMissingKey = 'totalCount' | 'tokens' | 'startedCount';
|
|
61
|
+
interface WorkflowSizeFacts {
|
|
62
|
+
readonly agentCap: number;
|
|
63
|
+
readonly tokenCap: number;
|
|
64
|
+
readonly scheduledAgents?: number;
|
|
65
|
+
readonly totalTokens?: number;
|
|
66
|
+
/** 投影(已花 ∨ 每 agent 用量 × 总数);只在分母是引擎真值时在场。 */
|
|
67
|
+
readonly projectedTokens?: number;
|
|
68
|
+
/** 投影的**上界**;只在分母用了 `doneCount + failedCount` 下界(且下界 > 0)时在场,与 `projectedTokens` 互斥。 */
|
|
69
|
+
readonly projectedTokensUpperBound?: number;
|
|
70
|
+
/** 只在分母用了下界时在场。 */
|
|
71
|
+
readonly startedIsLowerBound?: true;
|
|
72
|
+
}
|
|
73
|
+
export type WorkflowSizeVerdict = (WorkflowSizeFacts & {
|
|
74
|
+
readonly kind: 'warn';
|
|
75
|
+
readonly axis: 'agents' | 'tokens' | 'both';
|
|
76
|
+
}) | (WorkflowSizeFacts & {
|
|
77
|
+
readonly kind: 'ok';
|
|
78
|
+
readonly suppressed?: true;
|
|
79
|
+
}) | (WorkflowSizeFacts & {
|
|
80
|
+
readonly kind: 'unknown';
|
|
81
|
+
readonly missing: readonly WorkflowSizeMissingKey[];
|
|
82
|
+
});
|
|
83
|
+
/** 规模判定。纯函数:每个环境量都由调用方注入。 */
|
|
84
|
+
export declare function evaluateWorkflowSize(input: WorkflowSizeInput): WorkflowSizeVerdict;
|
|
85
|
+
/**
|
|
86
|
+
* 两个 env 覆盖的读口。`env` 由调用方交(本包不碰 `process.env`)。每组按 {@link WORKFLOW_SIZE_WARNING_ENV_NAMES}
|
|
87
|
+
* 的顺序取**第一个在场的名字**;那个名字的值不是正有限数 ⇒ 该键不在场,**不回落**到第二个名字
|
|
88
|
+
* (显式写错不该被悄悄换源)。
|
|
89
|
+
*/
|
|
90
|
+
export declare function readWorkflowSizeWarningEnvCaps(env: Readonly<Record<string, string | undefined>> | undefined): {
|
|
91
|
+
envAgentCap?: number;
|
|
92
|
+
envTokenCap?: number;
|
|
93
|
+
};
|
|
94
|
+
/** 宽度阶梯:挑放得下的最长一档;连字形都放不下 ⇒ `undefined`(整段不渲)。`width` 由调用方注入(与渲染器同一把尺)。 */
|
|
95
|
+
export declare function workflowSizeWarningTextForWidth(available: number, width: (s: string) => number): string | undefined;
|
|
96
|
+
export {};
|