@sema-agent/client-core 0.2.0 → 0.3.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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/engineAgentPanelStore.ts 逐字搬入(零 import;壳侧改 re-export)。
3
+ * 🔴 同 subagentContentStore:buffer/listener/residentTaskIds 是单实例语义,发布侧(bridge/fleet
4
+ * 投影)与订阅侧(chrome-agentprogress 的面板 hook)必须同实例,否则事件发进一个副本、面板订另一个。
5
+ */
6
+ /**
7
+ * engineAgentPanelStore — dep-free side-channel store bridging ENGINE-side subagent progress
8
+ * (`task_progress` ticks, design/99 MF-10) to CC's CoordinatorTaskPanel (the footer live-agent panel).
9
+ *
10
+ * WHY A STORE (the retryStatusStore / liveQuestionStore pattern): the upstream bridge is a plain
11
+ * generator with no React/AppState access, while the panel reads `AppState.tasks` — so the bridge
12
+ * PUBLISHES neutral events here and a sema-owned hook inside chrome-agentprogress.tsx (already mounted
13
+ * under the footer) SUBSCRIBES and applies them to AppState via registerTask/updateTaskState. CC files
14
+ * stay verbatim; the AppState write side lives in the sema override that owns the panel.
15
+ *
16
+ * WHY THE PANEL (CC-187 parity): a local sync AgentTool renders an inline progress TRAIL from the
17
+ * subagent's real messages. An engine tick carries ONLY rollup numbers ({toolUses, totalTokens}) — the
18
+ * exact shape of CC's `AgentProgress` — and CC's own precedent for out-of-process agents (remote agent
19
+ * tasks) is the TASK PANEL + task-notification, not a synthetic inline trail. Faithful mapping, no
20
+ * fabricated subagent messages.
21
+ *
22
+ * Buffering: ticks published before the panel mounts are buffered (bounded) and replayed to the first
23
+ * subscriber, so early frames in a fast turn aren't lost to mount timing.
24
+ */
25
+ export type EngineAgentPanelEvent = {
26
+ kind: 'tick';
27
+ taskId: string;
28
+ /** Subagent display name off the wire tick (redacted upstream); absent ⇒ keep taskId. */
29
+ name?: string;
30
+ /** The delegating Task tool_use's `description` (the card title), when the bridge could bind one. */
31
+ description?: string;
32
+ /** The delegated prompt off the bound tool_use input, when present. */
33
+ prompt?: string;
34
+ /** core 1.262.0 [558]B (service 1.154.0 wire) — the child's most recent tool intent as ONE
35
+ * human line ("Bash npm test"), service-redacted. The panel consumer maps it onto the row's
36
+ * `progress.summary` (CC's own live-activity display slot); absent ⇒ slot untouched. */
37
+ currentAction?: string;
38
+ toolUses: number;
39
+ totalTokens: number;
40
+ } | {
41
+ /**
42
+ * 行帧 lane 投影(2026-07-26 「后台 Agent 子代不进 Background 面板」案)。
43
+ *
44
+ * 一手取证(verify/_bgagent-panel-lane-live-pty.mjs 首跑,engine 1.277.4 真机):两个
45
+ * `run_in_background:true` 的 fork 子代**活跃干活整跑**,`task_progress` tick 探针 **0 条**、
46
+ * fleet 行帧探针 **24 条** —— 父 turn 秒收口即关 SSE,子代的 tick 根本没有通道到壳;行帧
47
+ * lane(常开 fleet stream)才是后台子代唯一在场的 lane。面板的 local_agent 行此前**只**由
48
+ * tick 注册 ⇒ 产品自印的「N background agents launched (↓ to manage)」指向一个看不到它们的
49
+ * 面板(还顺带让 `ctrl+x ctrl+k … stop all agents` 的渲染门在真产品里永不成立)。
50
+ *
51
+ * 为什么是独立事件形而不是复用 'tick':两条 lane 的**可选性**不同 —— tick 的 `toolUses` 是
52
+ * 必填数,行帧的是**可选**位(server 1.278.0 才有,老引擎没有)。复用 tick 会强迫本 lane 在
53
+ * 缺席时编一个 0,把前台子代由真 tick 写进去的进度数字每秒清一次。消费端对本事件的规矩:
54
+ * 只建/只喂**本 lane 亲手建的行**,tick 一到就永久让位(fleetOwnedRowIds 摘除)。
55
+ */
56
+ kind: 'fleet-row';
57
+ taskId: string;
58
+ /** 行帧 agentType(fleet 行 label 列,如 'fork')→ 行的 agentType。 */
59
+ name?: string;
60
+ /** 行帧投影出的任务描述(如 'alpha-probe')→ 行的 description(面板行 label)。 */
61
+ description?: string;
62
+ /** core 1.429 → server 1.288([1831]②):子代当前工具的结构化形。`toolName` verbatim
63
+ * (查壳工具注册表),`target` 已上游 redact + 300 bound。缺席=旧端或子代从未跑过工具。 */
64
+ currentTool?: {
65
+ toolName: string;
66
+ target?: string;
67
+ };
68
+ /** 行帧累计 token。 */
69
+ totalTokens?: number;
70
+ /**
71
+ * server 1.278.0 起行帧也带的**累计**工具调用数(修「后台子代详情页 tool 计数恒 0」)。
72
+ *
73
+ * 🔴 三态,别写 `?? 0`:
74
+ * · 键在 ⇒ 真值(含真 0 = 引擎说「一个工具都没跑」);
75
+ * · 键不在 ⇒ 老引擎没这个位 = **不知道**,消费端如实留空/保留行上已有值,绝不写 0。
76
+ * 🔴 **累计不是增量**:bg sink 的 usage 是累计口径(与 `turn_end` 的每轮增量相反,后者在
77
+ * upstreamBridge 里要累加)——消费端直接赋值,绝不 `+=`。
78
+ *
79
+ * ⚠️ 时序差(server 主动提醒,不是接错):1.278.0 只在 **bg lane** 上线;workflow/subagent
80
+ * 子代走的 `task_progress`(tick lane)的 `usage.toolUses` 在 core 类型里本来就必填、数据一直
81
+ * 在,只是那条 lane 也没发 —— 已补齐但搭 1.278.0 之后的下一车。所以短期内「有的行有、有的
82
+ * 行没有」是**预期形**,两条 lane 的接线口径已按同一套三态写好。
83
+ */
84
+ toolUses?: number;
85
+ /**
86
+ * server 1.278.0 `transcriptId`(= core 铸的子代 childSessionId)—— 转录锚,**不是** prompt。
87
+ * 委派 prompt 走它:壳侧凭这个 id 去转录读面取首条 user 消息(engineDelegatedPrompt.ts),
88
+ * 修「详情页 Prompt 段恒空」。缺席 ⇒ 维持现状(诚实留白)。
89
+ */
90
+ transcriptId?: string;
91
+ } | {
92
+ kind: 'end';
93
+ taskId: string;
94
+ isError: boolean;
95
+ /** The subagent's FINAL REPORT — the delegating Task/Agent card's `tool_end.output` text (only the
96
+ * explicit close arm carries it; the defensive sweeps settle without one). The consumer persists it
97
+ * on the row (task.result) + appends it to a live-viewed transcript (拍1 查看态 seed). */
98
+ report?: string;
99
+ } | {
100
+ /** Exception-path safety net (published by seamQuery after every turn, throw or return):
101
+ * the consumer settles ALL rows it owns that are still running. Idempotent. */
102
+ kind: 'sweep';
103
+ };
104
+ export declare function markEnginePanelTaskResident(taskId: string): void;
105
+ export declare function clearEnginePanelTaskResident(taskId: string): void;
106
+ export declare function isEnginePanelTaskResident(taskId: string): boolean;
107
+ type Listener = (ev: EngineAgentPanelEvent) => void;
108
+ export declare function publishEngineAgentPanelEvent(ev: EngineAgentPanelEvent): void;
109
+ /** Single consumer (the panel hook). Replays any buffered events, returns an unsubscribe. */
110
+ export declare function subscribeEngineAgentPanel(fn: Listener): () => void;
111
+ export {};
@@ -0,0 +1,93 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/engineAgentPanelStore.ts 逐字搬入(零 import;壳侧改 re-export)。
3
+ * 🔴 同 subagentContentStore:buffer/listener/residentTaskIds 是单实例语义,发布侧(bridge/fleet
4
+ * 投影)与订阅侧(chrome-agentprogress 的面板 hook)必须同实例,否则事件发进一个副本、面板订另一个。
5
+ */
6
+ /**
7
+ * engineAgentPanelStore — dep-free side-channel store bridging ENGINE-side subagent progress
8
+ * (`task_progress` ticks, design/99 MF-10) to CC's CoordinatorTaskPanel (the footer live-agent panel).
9
+ *
10
+ * WHY A STORE (the retryStatusStore / liveQuestionStore pattern): the upstream bridge is a plain
11
+ * generator with no React/AppState access, while the panel reads `AppState.tasks` — so the bridge
12
+ * PUBLISHES neutral events here and a sema-owned hook inside chrome-agentprogress.tsx (already mounted
13
+ * under the footer) SUBSCRIBES and applies them to AppState via registerTask/updateTaskState. CC files
14
+ * stay verbatim; the AppState write side lives in the sema override that owns the panel.
15
+ *
16
+ * WHY THE PANEL (CC-187 parity): a local sync AgentTool renders an inline progress TRAIL from the
17
+ * subagent's real messages. An engine tick carries ONLY rollup numbers ({toolUses, totalTokens}) — the
18
+ * exact shape of CC's `AgentProgress` — and CC's own precedent for out-of-process agents (remote agent
19
+ * tasks) is the TASK PANEL + task-notification, not a synthetic inline trail. Faithful mapping, no
20
+ * fabricated subagent messages.
21
+ *
22
+ * Buffering: ticks published before the panel mounts are buffered (bounded) and replayed to the first
23
+ * subscriber, so early frames in a fast turn aren't lost to mount timing.
24
+ */
25
+ // ── #6(frame-lane-matrix)session 常驻行台账 ─────────────────────────────────────────────────
26
+ // 行寿命 ≠ turn 寿命:跨 turn 存活的 bg 子代行(tick 绑不到本 turn 开着的卡 = inert/unbound)不该被
27
+ // turn 末防御 sweep(bridge settlePanelTasks(null) / seamQuery 'sweep' 事件)结成假 completed——一旦
28
+ // 假结,消费端 tick 门(status!=='running' ⇒ return prev)会把后续真 tick 全丢,真终态(failed)也
29
+ // 改不动(冻死)。台账 module-level(publisher=bridge 按绑定活性写;两个 sweep 消费点按它跳过;
30
+ // 真终态通知(task_notification 帧 / enqueueBgChildNotification 合成)settle 时摘除)。
31
+ const residentTaskIds = new Set();
32
+ export function markEnginePanelTaskResident(taskId) {
33
+ residentTaskIds.add(taskId);
34
+ }
35
+ export function clearEnginePanelTaskResident(taskId) {
36
+ residentTaskIds.delete(taskId);
37
+ }
38
+ export function isEnginePanelTaskResident(taskId) {
39
+ return residentTaskIds.has(taskId);
40
+ }
41
+ const MAX_BUFFER = 200;
42
+ let listener = null;
43
+ const buffer = [];
44
+ export function publishEngineAgentPanelEvent(ev) {
45
+ if (listener) {
46
+ try {
47
+ listener(ev);
48
+ }
49
+ catch {
50
+ // A consumer render error must never tear down the bridge's stream drain.
51
+ }
52
+ return;
53
+ }
54
+ // Buffered (panel not mounted yet / temporarily unmounted). Ticks COALESCE per taskId (only the
55
+ // latest rollup matters — usage is cumulative), so lifecycle events ('end'/'sweep') are never the
56
+ // ones squeezed out by a long unmount (adversarial review w0zwpa251 #4: dropping a buffered 'end'
57
+ // left a row running forever).
58
+ if (ev.kind === 'tick') {
59
+ const i = buffer.findIndex(b => b.kind === 'tick' && b.taskId === ev.taskId);
60
+ if (i >= 0) {
61
+ buffer[i] = ev;
62
+ return;
63
+ }
64
+ }
65
+ if (buffer.length < MAX_BUFFER)
66
+ buffer.push(ev);
67
+ else if (ev.kind !== 'tick') {
68
+ // Lifecycle events must survive a full buffer: evict the oldest tick to make room.
69
+ const i = buffer.findIndex(b => b.kind === 'tick');
70
+ if (i >= 0)
71
+ buffer.splice(i, 1);
72
+ buffer.push(ev);
73
+ }
74
+ }
75
+ /** Single consumer (the panel hook). Replays any buffered events, returns an unsubscribe. */
76
+ export function subscribeEngineAgentPanel(fn) {
77
+ listener = fn;
78
+ if (buffer.length > 0) {
79
+ const pending = buffer.splice(0, buffer.length);
80
+ for (const ev of pending) {
81
+ try {
82
+ fn(ev);
83
+ }
84
+ catch {
85
+ // ignore — same guard as publish
86
+ }
87
+ }
88
+ }
89
+ return () => {
90
+ if (listener === fn)
91
+ listener = null;
92
+ };
93
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/engineCapsCache.ts 逐字搬入(零依赖;壳侧改 re-export)。
3
+ * 🔴 **本批刻意不做「三套 caps 缓存收编成一套」**(设计稿 §2.4.1-B 风险点 + census §2.4):收编是
4
+ * 行为改动,要单独一条测试与单独一批(B1 只搬现行为,不夹带)。selfKnowledge / sideChannel 的
5
+ * 两套单键探测仍在壳侧,记账给下一批。
6
+ */
7
+ /**
8
+ * engineCapsCache — 引擎 /v1/capabilities 的通用 boot-kick 缓存(同步读口)。
9
+ *
10
+ * selfKnowledge/sideChannel 各自有单键探测(先行件,语义已钉测试);本模块是后续键的通用面
11
+ * (首个消费者:compactionModel,[1487]② server 1.243 接线的 cli 半场)。纪律同款:
12
+ * · kick 幂等(in-flight 去重),绝不 throw;
13
+ * · 判定固化;探测失败不缓存(引擎未起/瞬断 → 下次 kick 再判);
14
+ * · 同步读口 boolean(未判 = false = 调用方回落,version-safe)。
15
+ * 与单键探测不同处:缓存整个 caps 对象(一次探测服务后续所有键),false 不设 TTL——本缓存随
16
+ * 每次 createLiveConversationClient 构造重 kick(引擎温切重启后新构造自然重探)。
17
+ */
18
+ /** 构造期 kick(async 幂等);probe = client.capabilities 薄闭包。 */
19
+ export declare function kickEngineCapsProbe(baseUrl: string, probe: () => Promise<unknown>): void;
20
+ /** 同步读口:该 base 的 caps 里 key 是否字面 true(未判/缺键 = false)。 */
21
+ export declare function engineCapTrue(baseUrl: string | undefined, key: string): boolean;
22
+ /**
23
+ * 同步读口(字符串键):该 base 的 caps 里 key 的字符串值,未判/缺键/非字符串 ⇒ undefined。
24
+ *
25
+ * 首个消费者 = `hooksWireCaps` 的 `/goal` CC-Stop 语义判档,它要读 `capabilities.version`
26
+ * (SDK `Capabilities.version`,server 侧 = `serviceVersion()`,与 `/health.version` 同源)。
27
+ * 🔴 纪律同 {@link engineCapTrue}:**未判即 undefined**,调用方必须按「探不到 ⇒ 降级」处理 ——
28
+ * 引擎能力位缺席时任何「假定它有」的分支都是对用户/模型的谎报。
29
+ */
30
+ export declare function engineCapString(baseUrl: string | undefined, key: string): string | undefined;
31
+ /** 测试钩子。 */
32
+ export declare function __resetEngineCapsCacheForTests(): void;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/engineCapsCache.ts 逐字搬入(零依赖;壳侧改 re-export)。
3
+ * 🔴 **本批刻意不做「三套 caps 缓存收编成一套」**(设计稿 §2.4.1-B 风险点 + census §2.4):收编是
4
+ * 行为改动,要单独一条测试与单独一批(B1 只搬现行为,不夹带)。selfKnowledge / sideChannel 的
5
+ * 两套单键探测仍在壳侧,记账给下一批。
6
+ */
7
+ /**
8
+ * engineCapsCache — 引擎 /v1/capabilities 的通用 boot-kick 缓存(同步读口)。
9
+ *
10
+ * selfKnowledge/sideChannel 各自有单键探测(先行件,语义已钉测试);本模块是后续键的通用面
11
+ * (首个消费者:compactionModel,[1487]② server 1.243 接线的 cli 半场)。纪律同款:
12
+ * · kick 幂等(in-flight 去重),绝不 throw;
13
+ * · 判定固化;探测失败不缓存(引擎未起/瞬断 → 下次 kick 再判);
14
+ * · 同步读口 boolean(未判 = false = 调用方回落,version-safe)。
15
+ * 与单键探测不同处:缓存整个 caps 对象(一次探测服务后续所有键),false 不设 TTL——本缓存随
16
+ * 每次 createLiveConversationClient 构造重 kick(引擎温切重启后新构造自然重探)。
17
+ */
18
+ const capsByBase = new Map();
19
+ const inFlight = new Set();
20
+ /** 构造期 kick(async 幂等);probe = client.capabilities 薄闭包。 */
21
+ export function kickEngineCapsProbe(baseUrl, probe) {
22
+ if (!baseUrl || capsByBase.has(baseUrl) || inFlight.has(baseUrl))
23
+ return;
24
+ inFlight.add(baseUrl);
25
+ void (async () => {
26
+ try {
27
+ const caps = await probe();
28
+ if (caps && typeof caps === 'object') {
29
+ capsByBase.set(baseUrl, caps);
30
+ }
31
+ }
32
+ catch {
33
+ // 失败不缓存:下次 kick 再判
34
+ }
35
+ finally {
36
+ inFlight.delete(baseUrl);
37
+ }
38
+ })();
39
+ }
40
+ /** 同步读口:该 base 的 caps 里 key 是否字面 true(未判/缺键 = false)。 */
41
+ export function engineCapTrue(baseUrl, key) {
42
+ if (!baseUrl)
43
+ return false;
44
+ return capsByBase.get(baseUrl)?.[key] === true;
45
+ }
46
+ /**
47
+ * 同步读口(字符串键):该 base 的 caps 里 key 的字符串值,未判/缺键/非字符串 ⇒ undefined。
48
+ *
49
+ * 首个消费者 = `hooksWireCaps` 的 `/goal` CC-Stop 语义判档,它要读 `capabilities.version`
50
+ * (SDK `Capabilities.version`,server 侧 = `serviceVersion()`,与 `/health.version` 同源)。
51
+ * 🔴 纪律同 {@link engineCapTrue}:**未判即 undefined**,调用方必须按「探不到 ⇒ 降级」处理 ——
52
+ * 引擎能力位缺席时任何「假定它有」的分支都是对用户/模型的谎报。
53
+ */
54
+ export function engineCapString(baseUrl, key) {
55
+ if (!baseUrl)
56
+ return undefined;
57
+ const v = capsByBase.get(baseUrl)?.[key];
58
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
59
+ }
60
+ /** 测试钩子。 */
61
+ export function __resetEngineCapsCacheForTests() {
62
+ capsByBase.clear();
63
+ inFlight.clear();
64
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/engineInlineTaskStats.ts 逐字搬入(零依赖;壳侧改 re-export)。
3
+ * 🔴 单实例语义:写方 = bridge 的 task_progress tick,读方 = 行内 group 行的 useSyncExternalStore。
4
+ */
5
+ /**
6
+ * engineInlineTaskStats — dep-free side-channel store bridging ENGINE-side subagent progress
7
+ * (`task_progress` ticks) to the INLINE transcript group row ("Running N agents…" / AgentProgressLine).
8
+ *
9
+ * WHY (clay dogfood 2026-07-03): in engine mode the shell AgentTool's call() never runs, so the inline
10
+ * group row's per-agent stats (tool uses / tokens) and status line — both computed from LOCAL
11
+ * ProgressMessages (overrides/agent.tsx calculateAgentStats / extractLastToolInfo) — stay frozen at
12
+ * `0 tool uses` / `Initializing…` for the whole run and after completion. The engine's `task_progress`
13
+ * ticks DO carry the rollup ({toolUses, totalTokens, durationMs}) and the bridge already binds each
14
+ * tick to its delegating tool_use card (bindTaskCard, P1② explicit parentToolCallId) — but only fed
15
+ * the FOOTER panel store. This store is the inline twin: keyed by the delegating tool_use id (cardId),
16
+ * latest cumulative tick per card, frozen on the card's close (tool_end) / turn-end sweep so the
17
+ * finished group row keeps the final values.
18
+ *
19
+ * Same pattern as engineAgentPanelStore (module-level, no React/AppState deps on the write side); the
20
+ * read side is a useSyncExternalStore hook inside the sema-owned AgentProgressLine override — a
21
+ * subscription, NOT a render-time map read, because the memoized transcript tree won't re-render on
22
+ * ticks by itself. Local (non-engine) AgentTool runs never write here, so the local path is untouched
23
+ * (fallback only consulted when the card id has an entry).
24
+ */
25
+ export interface EngineInlineTaskStats {
26
+ /** Cumulative tool uses off the latest tick (frozen at settle). */
27
+ toolUses: number;
28
+ /** Cumulative tokens off the latest tick (frozen at settle). */
29
+ totalTokens: number;
30
+ /** Cumulative run duration off the latest tick — feeds the "Working…" status line. */
31
+ durationMs: number;
32
+ /** core 1.262.0 [558]B (service 1.154.0 wire) — the child's most recent tool intent as ONE human
33
+ * line ("Bash npm test"), service-redacted. Replaces the generic "Working…" status line while the
34
+ * card is live; absent on old engines ⇒ the status line is unchanged. */
35
+ currentAction?: string;
36
+ /** The card closed (tool_end or turn-end sweep): values above are final. */
37
+ done: boolean;
38
+ isError: boolean;
39
+ }
40
+ /** Bridge write side — latest cumulative tick for the card. No-op once the card settled (frozen finals). */
41
+ export declare function publishEngineInlineTaskTick(cardId: string, tick: {
42
+ toolUses: number;
43
+ totalTokens: number;
44
+ durationMs: number;
45
+ currentAction?: string;
46
+ }): void;
47
+ /** Bridge write side — the delegating card closed: freeze the finals. Idempotent. */
48
+ export declare function settleEngineInlineTaskStats(cardId: string, isError: boolean): void;
49
+ /** Turn-end / abort sweep twin — settle every card still live (a subagent never emits a terminal tick). */
50
+ export declare function settleAllEngineInlineTaskStats(): void;
51
+ /** Read side — reference-stable snapshot per card (object replaced only on update). */
52
+ export declare function getEngineInlineTaskStats(cardId: string): EngineInlineTaskStats | undefined;
53
+ /**
54
+ * Read side, id-less — the SOLE live (un-settled) card's stats, or undefined when 0 or ≥2 are live.
55
+ *
56
+ * WHY (#64 B 面 pty 实测,2026-07-11): the SINGLE-card in-flight render path
57
+ * (overrides/agent.tsx renderToolUseProgressMessage, zero local progressMessages in engine mode)
58
+ * has NO toolUseID in scope — the CC Tool-interface method signature doesn't carry it, and the
59
+ * call-site wrapper (AssistantToolUseMessage.tsx) is CC-verbatim. Attribution without an id is only
60
+ * safe when it is unambiguous, so this returns the stats ONLY when exactly one live card exists
61
+ * (the adversarial-review posture of bindTaskCard: better honest-generic than bound wrong; with ≥2
62
+ * live engine subagents the row keeps today's Initializing…). Reference-stable (returns the same
63
+ * per-card snapshot object the keyed getter hands out).
64
+ */
65
+ export declare function getSoleLiveEngineInlineTaskStats(): EngineInlineTaskStats | undefined;
66
+ export declare function subscribeEngineInlineTaskStats(fn: () => void): () => void;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/engineInlineTaskStats.ts 逐字搬入(零依赖;壳侧改 re-export)。
3
+ * 🔴 单实例语义:写方 = bridge 的 task_progress tick,读方 = 行内 group 行的 useSyncExternalStore。
4
+ */
5
+ /**
6
+ * engineInlineTaskStats — dep-free side-channel store bridging ENGINE-side subagent progress
7
+ * (`task_progress` ticks) to the INLINE transcript group row ("Running N agents…" / AgentProgressLine).
8
+ *
9
+ * WHY (clay dogfood 2026-07-03): in engine mode the shell AgentTool's call() never runs, so the inline
10
+ * group row's per-agent stats (tool uses / tokens) and status line — both computed from LOCAL
11
+ * ProgressMessages (overrides/agent.tsx calculateAgentStats / extractLastToolInfo) — stay frozen at
12
+ * `0 tool uses` / `Initializing…` for the whole run and after completion. The engine's `task_progress`
13
+ * ticks DO carry the rollup ({toolUses, totalTokens, durationMs}) and the bridge already binds each
14
+ * tick to its delegating tool_use card (bindTaskCard, P1② explicit parentToolCallId) — but only fed
15
+ * the FOOTER panel store. This store is the inline twin: keyed by the delegating tool_use id (cardId),
16
+ * latest cumulative tick per card, frozen on the card's close (tool_end) / turn-end sweep so the
17
+ * finished group row keeps the final values.
18
+ *
19
+ * Same pattern as engineAgentPanelStore (module-level, no React/AppState deps on the write side); the
20
+ * read side is a useSyncExternalStore hook inside the sema-owned AgentProgressLine override — a
21
+ * subscription, NOT a render-time map read, because the memoized transcript tree won't re-render on
22
+ * ticks by itself. Local (non-engine) AgentTool runs never write here, so the local path is untouched
23
+ * (fallback only consulted when the card id has an entry).
24
+ */
25
+ const statsByCardId = new Map();
26
+ const listeners = new Set();
27
+ function emit() {
28
+ for (const fn of listeners) {
29
+ try {
30
+ fn();
31
+ }
32
+ catch {
33
+ // a consumer render error must never disturb the bridge's stream drain
34
+ }
35
+ }
36
+ }
37
+ /** Bridge write side — latest cumulative tick for the card. No-op once the card settled (frozen finals). */
38
+ export function publishEngineInlineTaskTick(cardId, tick) {
39
+ const prev = statsByCardId.get(cardId);
40
+ if (prev?.done)
41
+ return;
42
+ statsByCardId.set(cardId, { ...tick, done: false, isError: false });
43
+ emit();
44
+ }
45
+ /** Bridge write side — the delegating card closed: freeze the finals. Idempotent. */
46
+ export function settleEngineInlineTaskStats(cardId, isError) {
47
+ const prev = statsByCardId.get(cardId);
48
+ if (!prev || prev.done)
49
+ return;
50
+ statsByCardId.set(cardId, { ...prev, done: true, isError });
51
+ emit();
52
+ }
53
+ /** Turn-end / abort sweep twin — settle every card still live (a subagent never emits a terminal tick). */
54
+ export function settleAllEngineInlineTaskStats() {
55
+ let changed = false;
56
+ for (const [cardId, s] of statsByCardId) {
57
+ if (s.done)
58
+ continue;
59
+ statsByCardId.set(cardId, { ...s, done: true, isError: false });
60
+ changed = true;
61
+ }
62
+ if (changed)
63
+ emit();
64
+ }
65
+ /** Read side — reference-stable snapshot per card (object replaced only on update). */
66
+ export function getEngineInlineTaskStats(cardId) {
67
+ return statsByCardId.get(cardId);
68
+ }
69
+ /**
70
+ * Read side, id-less — the SOLE live (un-settled) card's stats, or undefined when 0 or ≥2 are live.
71
+ *
72
+ * WHY (#64 B 面 pty 实测,2026-07-11): the SINGLE-card in-flight render path
73
+ * (overrides/agent.tsx renderToolUseProgressMessage, zero local progressMessages in engine mode)
74
+ * has NO toolUseID in scope — the CC Tool-interface method signature doesn't carry it, and the
75
+ * call-site wrapper (AssistantToolUseMessage.tsx) is CC-verbatim. Attribution without an id is only
76
+ * safe when it is unambiguous, so this returns the stats ONLY when exactly one live card exists
77
+ * (the adversarial-review posture of bindTaskCard: better honest-generic than bound wrong; with ≥2
78
+ * live engine subagents the row keeps today's Initializing…). Reference-stable (returns the same
79
+ * per-card snapshot object the keyed getter hands out).
80
+ */
81
+ export function getSoleLiveEngineInlineTaskStats() {
82
+ let sole;
83
+ for (const s of statsByCardId.values()) {
84
+ if (s.done)
85
+ continue;
86
+ if (sole !== undefined)
87
+ return undefined; // ≥2 live — ambiguous, stay honest
88
+ sole = s;
89
+ }
90
+ return sole;
91
+ }
92
+ export function subscribeEngineInlineTaskStats(fn) {
93
+ listeners.add(fn);
94
+ return () => {
95
+ listeners.delete(fn);
96
+ };
97
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/engineToolLabelStore.ts 逐字搬入(零依赖;壳侧改 re-export)。
3
+ */
4
+ /**
5
+ * src/sema/engineToolLabelStore.ts — core 工具**展示名**(`tool_start.label` / `tool_end.label`)的
6
+ * 壳侧侧信道(sema-owned 新文件,零 CC 基座改动)。
7
+ *
8
+ * 为什么是侧信道而不是把 label 留在 tool_use block 上(P0-2 的关键判断):
9
+ * tool_use content block 会**进壳 transcript**(jsonl 持久 + /compact 时原样直发 provider)。
10
+ * Anthropic/OpenAI 两侧对 tool_use block 都是严格形 `{type,id,name,input}`,多一个 `label` 键
11
+ * = 400 invalid_request —— 这正是「件1 中断事故批 G」踩过的坑(裸对象 content 落 transcript,
12
+ * /compact 直发 ark/volces 400)。所以 bridge 在渲染前把 label 从 block 上摘掉(同
13
+ * eventId/parentToolCallId 待遇),存到这里,渲染面按 toolCallId 查。
14
+ *
15
+ * 消费面:components/messages/AssistantToolUseMessage.tsx 的 **unknown-tool fallback** ——
16
+ * 也只在那里消费。已注册工具(壳本地 registry 命中)的卡标题恒走 CC 的 `tool.userFacingName()`,
17
+ * 绝不被引擎 label 覆盖(CC 逐字对齐优先;core label 与 CC 的 MCP 卡形 `server - tool (MCP)` 不同形,
18
+ * 覆盖 = 制造分叉)。
19
+ *
20
+ * 今天的真实命中面(2026-07-26 直连钉版 core 1.416 实测):core 只在 `label !== name` 时才上 wire
21
+ * (runtask.js:1406),而唯一这样的一族是 **MCP 远端工具** —— `name = mcp__<server>__<tool>`、
22
+ * `label = <server>:<tool>`(core/mcp.js:902)。即:引擎侧连着、壳侧没连的那台 MCP server 的工具,
23
+ * 修前卡头是裸 `mcp__foo__bar`,修后是 core 的展示名 `foo:bar`。
24
+ *
25
+ * 生命周期:按 toolCallId 键的有界 Map(FIFO 淘汰),纯 chrome 数据,不持久、不回喂模型。
26
+ */
27
+ /** 登记一个 tool call 的 core 展示名(空/非串忽略)。 */
28
+ export declare function recordEngineToolLabel(toolCallId: unknown, label: unknown): void;
29
+ /** 读一个 tool call 的 core 展示名(未登记 ⇒ undefined,调用方回落原有形状)。 */
30
+ export declare function engineToolLabel(toolCallId: unknown): string | undefined;
31
+ /** 测试用:清空台账。 */
32
+ export declare function resetEngineToolLabels(): void;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/engineToolLabelStore.ts 逐字搬入(零依赖;壳侧改 re-export)。
3
+ */
4
+ /**
5
+ * src/sema/engineToolLabelStore.ts — core 工具**展示名**(`tool_start.label` / `tool_end.label`)的
6
+ * 壳侧侧信道(sema-owned 新文件,零 CC 基座改动)。
7
+ *
8
+ * 为什么是侧信道而不是把 label 留在 tool_use block 上(P0-2 的关键判断):
9
+ * tool_use content block 会**进壳 transcript**(jsonl 持久 + /compact 时原样直发 provider)。
10
+ * Anthropic/OpenAI 两侧对 tool_use block 都是严格形 `{type,id,name,input}`,多一个 `label` 键
11
+ * = 400 invalid_request —— 这正是「件1 中断事故批 G」踩过的坑(裸对象 content 落 transcript,
12
+ * /compact 直发 ark/volces 400)。所以 bridge 在渲染前把 label 从 block 上摘掉(同
13
+ * eventId/parentToolCallId 待遇),存到这里,渲染面按 toolCallId 查。
14
+ *
15
+ * 消费面:components/messages/AssistantToolUseMessage.tsx 的 **unknown-tool fallback** ——
16
+ * 也只在那里消费。已注册工具(壳本地 registry 命中)的卡标题恒走 CC 的 `tool.userFacingName()`,
17
+ * 绝不被引擎 label 覆盖(CC 逐字对齐优先;core label 与 CC 的 MCP 卡形 `server - tool (MCP)` 不同形,
18
+ * 覆盖 = 制造分叉)。
19
+ *
20
+ * 今天的真实命中面(2026-07-26 直连钉版 core 1.416 实测):core 只在 `label !== name` 时才上 wire
21
+ * (runtask.js:1406),而唯一这样的一族是 **MCP 远端工具** —— `name = mcp__<server>__<tool>`、
22
+ * `label = <server>:<tool>`(core/mcp.js:902)。即:引擎侧连着、壳侧没连的那台 MCP server 的工具,
23
+ * 修前卡头是裸 `mcp__foo__bar`,修后是 core 的展示名 `foo:bar`。
24
+ *
25
+ * 生命周期:按 toolCallId 键的有界 Map(FIFO 淘汰),纯 chrome 数据,不持久、不回喂模型。
26
+ */
27
+ /** 上界:一个长会话里 tool call 无限增长,标签只是渲染辅助,超界按 FIFO 淘汰最旧的。 */
28
+ const MAX_ENTRIES = 500;
29
+ const labels = new Map();
30
+ /** 登记一个 tool call 的 core 展示名(空/非串忽略)。 */
31
+ export function recordEngineToolLabel(toolCallId, label) {
32
+ if (typeof toolCallId !== 'string' || toolCallId.length === 0)
33
+ return;
34
+ if (typeof label !== 'string' || label.trim().length === 0)
35
+ return;
36
+ // 已有值不覆盖:tool_start 先到,tool_end 只是补位(两者同源同值)。
37
+ if (labels.has(toolCallId))
38
+ return;
39
+ if (labels.size >= MAX_ENTRIES) {
40
+ const oldest = labels.keys().next();
41
+ if (!oldest.done)
42
+ labels.delete(oldest.value);
43
+ }
44
+ labels.set(toolCallId, label.trim());
45
+ }
46
+ /** 读一个 tool call 的 core 展示名(未登记 ⇒ undefined,调用方回落原有形状)。 */
47
+ export function engineToolLabel(toolCallId) {
48
+ if (typeof toolCallId !== 'string' || toolCallId.length === 0)
49
+ return undefined;
50
+ return labels.get(toolCallId);
51
+ }
52
+ /** 测试用:清空台账。 */
53
+ export function resetEngineToolLabels() {
54
+ labels.clear();
55
+ }
@@ -0,0 +1,31 @@
1
+ /** 行帧投影后的最小 agent 行形(fleet-tree 的 FleetTask 是它的超集)。 */
2
+ export interface FleetAgentRowLike {
3
+ id: string;
4
+ /** fleet 行 label 列 = agent identity(agentName ?? agentType),如 'fork'。 */
5
+ name?: string;
6
+ /** fleet 行描述列 = 任务描述,如 'alpha-probe'(面板行 label 用它)。 */
7
+ description?: string;
8
+ status?: string;
9
+ tokens?: number;
10
+ /** server 1.278.0 `toolUses` —— 子代**累计**工具调用数。🔴 三态:有键真值(含真 0)/无键
11
+ * `undefined`=老引擎不知道。绝不在本层 `?? 0`(那等于把「不知道」冒充成「跑了 0 个」)。 */
12
+ toolUses?: number;
13
+ /** server 1.278.0 `transcriptId`(子代 childSessionId)—— 委派 prompt 的取件锚,不是 prompt 本身。 */
14
+ transcriptId?: string;
15
+ /** server 1.288.0 / core 1.429.0([1831]②)—— 子代当前工具的结构化形。缺席=旧端或从未跑过工具。 */
16
+ currentTool?: {
17
+ toolName: string;
18
+ target?: string;
19
+ };
20
+ }
21
+ /** 行从投影集消失多久之后按 completed 兜底 settle(重连空窗 / snapshot 清表远小于此)。 */
22
+ export declare const ABSENT_SETTLE_MS = 30000;
23
+ /** 测试钩:清投影台账。 */
24
+ export declare function resetFleetAgentPanelProjection(): void;
25
+ /** 台账读面(测试/诊断):当前记着的行数。 */
26
+ export declare function fleetAgentProjectionSize(): number;
27
+ /**
28
+ * 把一次 fleet 投影(已经过 own 判别 + workflow belt 的 FleetTask[])喂给面板事件流。
29
+ * 幂等、无副作用地重复调用安全 —— 每秒被 footer 调用两次就是常态。
30
+ */
31
+ export declare function projectFleetAgentRows(rows: readonly FleetAgentRowLike[], nowMs?: number): void;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/fleetTaskDesc.ts 逐字搬入(零依赖;壳侧改 re-export)。
3
+ */
4
+ /**
5
+ * src/sema/fleetTaskDesc.ts — footer fleet 行「label/描述列」纯投影助手(零依赖,从 fleetClient.ts 提出
6
+ * 供单测直取——fleetClient 拖 SDK/appState/notification 重图,无法轻量 bundle;行为与提出前逐字同源)。
7
+ */
8
+ /** collapse a wire label to a single trimmed line (drops embedded newlines/runs). 187 row labels are always
9
+ * one short line; the <FleetTree/> column (Math.min(28,…) + wrap="truncate") does the visible ellipsis, so we
10
+ * only normalize here — no double-ellipsis, faithful to the 187 render. */
11
+ export declare function collapseLabel(s: string): string;
12
+ /** CC row-body fallback(CC 真身锚 2026-07-20 人类剧本帧:`◯ general-purpose t1calc`——row body 从第一帧
13
+ * 起就是 Agent 工具的 description)。引擎子行帧不带 `description` 字段,任务描述只在 `name` 里,且有两形
14
+ * (07-24 D2 wire 实证,engine 1.271.0 帧 dump):
15
+ * ① 出生帧:name = `${agentType}: ${description}` 合成形(如 "fork: t1calc")→ 剥前缀复原;
16
+ * ② 后续帧:引擎把 name 重写为【裸任务描述】(如 "t1calc",无前缀)→ 直接按描述投影——
17
+ * 修前只认①,②一到描述列即塌空(footer 只剩 `◯ fork`,D2 症状)。
18
+ * 防重复列 belt:裸 name 恰为 identity 回声(=agentType 或 =agentName)时不当描述(label 列已渲它)。
19
+ * 纯投影非双源;真进度(last tool name)到帧后仍然优先(调用侧 projectDescription 先行)。 */
20
+ export declare function taskDescFromName(name: string | undefined, agentType: string | undefined, agentName?: string): string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/fleetTaskDesc.ts 逐字搬入(零依赖;壳侧改 re-export)。
3
+ */
4
+ /**
5
+ * src/sema/fleetTaskDesc.ts — footer fleet 行「label/描述列」纯投影助手(零依赖,从 fleetClient.ts 提出
6
+ * 供单测直取——fleetClient 拖 SDK/appState/notification 重图,无法轻量 bundle;行为与提出前逐字同源)。
7
+ */
8
+ /** collapse a wire label to a single trimmed line (drops embedded newlines/runs). 187 row labels are always
9
+ * one short line; the <FleetTree/> column (Math.min(28,…) + wrap="truncate") does the visible ellipsis, so we
10
+ * only normalize here — no double-ellipsis, faithful to the 187 render. */
11
+ export function collapseLabel(s) {
12
+ return s.replace(/\s+/g, ' ').trim();
13
+ }
14
+ /** CC row-body fallback(CC 真身锚 2026-07-20 人类剧本帧:`◯ general-purpose t1calc`——row body 从第一帧
15
+ * 起就是 Agent 工具的 description)。引擎子行帧不带 `description` 字段,任务描述只在 `name` 里,且有两形
16
+ * (07-24 D2 wire 实证,engine 1.271.0 帧 dump):
17
+ * ① 出生帧:name = `${agentType}: ${description}` 合成形(如 "fork: t1calc")→ 剥前缀复原;
18
+ * ② 后续帧:引擎把 name 重写为【裸任务描述】(如 "t1calc",无前缀)→ 直接按描述投影——
19
+ * 修前只认①,②一到描述列即塌空(footer 只剩 `◯ fork`,D2 症状)。
20
+ * 防重复列 belt:裸 name 恰为 identity 回声(=agentType 或 =agentName)时不当描述(label 列已渲它)。
21
+ * 纯投影非双源;真进度(last tool name)到帧后仍然优先(调用侧 projectDescription 先行)。 */
22
+ export function taskDescFromName(name, agentType, agentName) {
23
+ if (!name || !agentType)
24
+ return '';
25
+ const prefix = `${agentType}: `;
26
+ if (name.startsWith(prefix))
27
+ return collapseLabel(name.slice(prefix.length));
28
+ const collapsed = collapseLabel(name);
29
+ if (collapsed &&
30
+ collapsed !== collapseLabel(agentType) &&
31
+ collapsed !== collapseLabel(agentName ?? '')) {
32
+ return collapsed;
33
+ }
34
+ return '';
35
+ }