@sema-agent/client-core 0.6.0 → 0.8.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.
Files changed (68) hide show
  1. package/README.md +31 -11
  2. package/dist/adapt.d.ts +4 -2
  3. package/dist/adapt.js +225 -25
  4. package/dist/compensations.d.ts +63 -0
  5. package/dist/compensations.js +138 -0
  6. package/dist/detachWire.d.ts +141 -0
  7. package/dist/detachWire.js +182 -0
  8. package/dist/diff/patch.d.ts +29 -0
  9. package/dist/diff/patch.js +45 -0
  10. package/dist/engineSessionParam.d.ts +11 -0
  11. package/dist/engineSessionParam.js +41 -0
  12. package/dist/engineWireTarget.d.ts +14 -0
  13. package/dist/engineWireTarget.js +53 -0
  14. package/dist/finalVerifyWire.d.ts +74 -0
  15. package/dist/finalVerifyWire.js +63 -0
  16. package/dist/fleet/fleetLedger.d.ts +132 -0
  17. package/dist/fleet/fleetLedger.js +393 -0
  18. package/dist/fleet/fleetProjection.d.ts +181 -0
  19. package/dist/fleet/fleetProjection.js +258 -0
  20. package/dist/headlessPermissionModeWire.d.ts +55 -0
  21. package/dist/headlessPermissionModeWire.js +111 -0
  22. package/dist/headlessReconnectWire.d.ts +96 -0
  23. package/dist/headlessReconnectWire.js +141 -0
  24. package/dist/hooksWireCaps.d.ts +91 -0
  25. package/dist/hooksWireCaps.js +359 -0
  26. package/dist/host.d.ts +34 -2
  27. package/dist/host.js +6 -0
  28. package/dist/index.d.ts +74 -1
  29. package/dist/index.js +104 -1
  30. package/dist/interactiveToolsWire.d.ts +48 -0
  31. package/dist/interactiveToolsWire.js +86 -0
  32. package/dist/limitsWire.d.ts +89 -0
  33. package/dist/limitsWire.js +225 -0
  34. package/dist/liveInitToolFace.d.ts +53 -0
  35. package/dist/liveInitToolFace.js +251 -0
  36. package/dist/model/modelFamilies.json +63 -0
  37. package/dist/model/providerPresets.d.ts +46 -0
  38. package/dist/model/providerPresets.js +160 -0
  39. package/dist/model/providerPresets.json +1179 -0
  40. package/dist/notifications.d.ts +7 -0
  41. package/dist/notifications.js +32 -0
  42. package/dist/sandboxWire.d.ts +75 -0
  43. package/dist/sandboxWire.js +138 -0
  44. package/dist/scenarioWire.d.ts +62 -0
  45. package/dist/scenarioWire.js +115 -0
  46. package/dist/seam.d.ts +52 -5
  47. package/dist/seam.js +15 -2
  48. package/dist/subagent/engineCompactWire.d.ts +12 -0
  49. package/dist/subagent/engineCompactWire.js +176 -0
  50. package/dist/subagent/engineDelegatedPrompt.d.ts +44 -0
  51. package/dist/subagent/engineDelegatedPrompt.js +205 -0
  52. package/dist/subagent/engineRowStopGate.d.ts +18 -0
  53. package/dist/subagent/engineRowStopGate.js +54 -0
  54. package/dist/subagent/engineSubagentOutput.d.ts +15 -0
  55. package/dist/subagent/engineSubagentOutput.js +98 -0
  56. package/dist/subagent/engineSubagentSteer.d.ts +10 -0
  57. package/dist/subagent/engineSubagentSteer.js +68 -0
  58. package/dist/subagent/engineSubagentTail.d.ts +40 -0
  59. package/dist/subagent/engineSubagentTail.js +241 -0
  60. package/dist/subagent/engineTaskHandleWire.d.ts +90 -0
  61. package/dist/subagent/engineTaskHandleWire.js +212 -0
  62. package/dist/toolResult.d.ts +118 -0
  63. package/dist/toolResult.js +774 -0
  64. package/dist/workflowClient.d.ts +42 -0
  65. package/dist/workflowClient.js +467 -0
  66. package/dist/workflowMonitor.d.ts +94 -0
  67. package/dist/workflowMonitor.js +43 -0
  68. package/package.json +5 -3
@@ -0,0 +1,176 @@
1
+ /**
2
+ * ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):cli `src/sema/engineCompactWire.ts` 整搬。
3
+ *
4
+ * 🔴 搬迁差分:
5
+ * · `wireConfig()`(六份手抄的**最后一份**,而且这一份已经多长了一行 principal 注释 ——
6
+ * 漂移的活样本)→ `engineWireTarget()`;
7
+ * · `getActiveEngineTaskId()` → `activeEngineRunId()`(SessionPort);
8
+ * · `logForDebugging` → `hostLog('debug', …)`;
9
+ * · `setTimeout(...)+t.unref?.()` → `hostTimers()`(**本文件是 TimersPort 的第一个消费者**)。
10
+ * 🔴 端没装 TimersPort 的后果**说清楚**:两处延迟发射(404 单次重试 / bind 后 1.5s 发射)
11
+ * 会**整个不发生** —— `/compact` 在 idle armed 之后就再也不会真正打到引擎,而命令行显示的
12
+ * 还是 'armed'。这不是「少个定时器」,是一条静默失效的功能腿。所以缺 port 时留 debug 日志,
13
+ * 并且 `hostPortMisses()` 里能看见(端自检恒应为空)。
14
+ *
15
+ * ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
16
+ *
17
+ * engineCompactWire — manual /compact reaches the ENGINE (K-1c), not just the client projection.
18
+ *
19
+ * Problem (clay dogfood 07-03): in live mode `/compact` ran only the CLIENT-side compaction (the local
20
+ * message projection) — the engine session's context was never compacted, so the next turn still paid
21
+ * the full window.
22
+ *
23
+ * Engine contract (service server.ts:1874, core 1.156 `TaskStream.compact()`):
24
+ * POST /v1/runs/:id/compact → 202; compaction runs at the next SAFE turn boundary and a
25
+ * `compacted{trigger:"manual"}` event rides the run's OWN stream. The verb only applies to a run LIVE
26
+ * on the replica — and `/compact` is typed while IDLE, when there is no live run. So the wire is
27
+ * arm-then-fire:
28
+ * - `/compact` mid-turn (active engine run bound) → fire immediately against that run.
29
+ * - `/compact` idle → arm a pending flag; the host's run_started bind (onEngineTaskBound) fires it
30
+ * against the NEXT turn's run — the engine compacts at that turn's first safe boundary, exactly
31
+ * the semantics CC users expect ("everything before this point is summarized").
32
+ *
33
+ * SDK 0.0.52 长出 runs.compact verb(SDK 宪法迁移批)—— raw fetch 退役,改走 AgentClient
34
+ * (engineWireSdk 工厂;baseUrl/token/principal 纪律与 live wire 同源)。Fire-and-forget + fail-soft
35
+ * 语义不变:409/501/网络错都只留 debug 日志,端侧投影压缩仍是可见结果。
36
+ * 迁移差分记账:①原 raw POST 无超时 → SDK 缺省 60s 预算(compact 是 202 即回的 arm verb,本地
37
+ * 口毫秒级,纯上界改善);②404 单次 2s 延迟重试(早 header bind 跑赢 run-row 持久化)逐字保留。
38
+ */
39
+ import { hostLog, hostTimers } from '../host.js';
40
+ import { makeEngineWireClient } from '../engineWireSdk.js';
41
+ import { engineWireTarget } from '../engineWireTarget.js';
42
+ import { activeEngineRunId, engineSessionParamSpread } from '../engineSessionParam.js';
43
+ let pendingEngineCompact = false;
44
+ /** Test/introspection hook. */
45
+ export function isEngineCompactPending() {
46
+ return pendingEngineCompact;
47
+ }
48
+ /**
49
+ * [488] 定谳(2026-07-07 指纹探针,server 1.130.0):TOC 本地引擎(无 TiDB runStore)的
50
+ * manual-compact verb 是 BY-DESIGN 501(handler 第一道闸 `!deps.runStore → 501`,capabilities
51
+ * 也如实报 `manualCompact:false`)——之前端没消费这个 gate,verb 打过去 501 被 fire-and-forget
52
+ * 吞掉,引擎 session 从未压缩=「静默 no-op」的真机理。这里 lazy 探一次 /v1/capabilities 并缓存;
53
+ * false ⇒ requestEngineCompact 返回 'unsupported'(不再装作 armed/fired)。探测失败 fail-open
54
+ * (null=未知 ⇒ 保持旧行为发 verb,错误面不变)。
55
+ */
56
+ let manualCompactCapable = null;
57
+ let capabilitiesProbe = null;
58
+ function probeManualCompactCapability() {
59
+ if (capabilitiesProbe)
60
+ return;
61
+ const cfg = engineWireTarget();
62
+ if (!cfg)
63
+ return;
64
+ capabilitiesProbe = (async () => {
65
+ try {
66
+ // SDK 0.0.52 client.capabilities()(typed Capabilities.manualCompact)。5s 预算经 client
67
+ // timeoutMs(对抗复审 MED 原语义:hung engine 不 pin probe promise);非 2xx/网络错/超时 SDK
68
+ // 统一抛错 → 与原「non-200 也重试」同归 catch 置 null 允许后续重试。
69
+ const client = makeEngineWireClient({
70
+ baseUrl: cfg.baseUrl,
71
+ ...(cfg.token ? { token: cfg.token } : {}),
72
+ principal: cfg.principal,
73
+ timeoutMs: 5000,
74
+ });
75
+ if (!client) {
76
+ capabilitiesProbe = null;
77
+ return;
78
+ }
79
+ const caps = await client.capabilities();
80
+ if (typeof caps.manualCompact === 'boolean')
81
+ manualCompactCapable = caps.manualCompact;
82
+ else
83
+ capabilitiesProbe = null; // shape we don't understand — retry later
84
+ }
85
+ catch {
86
+ capabilitiesProbe = null; // non-2xx/network error/timeout: unknown stays fail-open, next call re-probes
87
+ }
88
+ })();
89
+ }
90
+ /** Test hook: reset the cached capability (fresh engine / respawn). */
91
+ export function resetManualCompactCapability() {
92
+ manualCompactCapable = null;
93
+ capabilitiesProbe = null;
94
+ }
95
+ /**
96
+ * 延迟发射(`setTimeout` + `unref` 的 port 形)。缺 TimersPort ⇒ **不发射**并留日志 ——
97
+ * 静默 drop 会让 `/compact` 看起来 armed 了却永不落地(见文件头)。
98
+ */
99
+ function deferFire(ms, fn, why) {
100
+ const timers = hostTimers();
101
+ if (!timers) {
102
+ hostLog('debug', `engineCompactWire: no TimersPort — ${why} 不会发射(端漏装 port,/compact 这条腿静默失效)`);
103
+ return;
104
+ }
105
+ timers.setTimer(ms, fn);
106
+ }
107
+ async function postCompact(taskId, retriesLeft = 1) {
108
+ const cfg = engineWireTarget();
109
+ if (!cfg)
110
+ return;
111
+ // SDK runs.compact(0.0.52)——POST /v1/runs/:id/compact body {}(无 instructions 时与原 '{}' 逐字
112
+ // 同 wire);header 姿势 = Transport buildHeaders(Bearer 仅有 token 时 + x-agent-principal +
113
+ // content-type),与原手抄同三头。
114
+ const client = makeEngineWireClient({
115
+ baseUrl: cfg.baseUrl,
116
+ ...(cfg.token ? { token: cfg.token } : {}),
117
+ principal: cfg.principal,
118
+ });
119
+ if (!client)
120
+ return;
121
+ try {
122
+ const ack = await client.runs.compact(taskId, engineSessionParamSpread());
123
+ hostLog('debug', `engineCompactWire: POST compact ${taskId} → ok${ack?.status ? ` (${ack.status})` : ''}`);
124
+ }
125
+ catch (e) {
126
+ const status = e?.status;
127
+ if (typeof status === 'number') {
128
+ hostLog('debug', `engineCompactWire: POST compact ${taskId} → ${status}`);
129
+ // 404 = the early header bind outran the run-row persist — one deferred retry covers it.
130
+ if (status === 404 && retriesLeft > 0) {
131
+ deferFire(2000, () => void postCompact(taskId, retriesLeft - 1), '404 单次重试');
132
+ }
133
+ return;
134
+ }
135
+ hostLog('debug', `engineCompactWire: compact fire failed (best-effort): ${String(e)}`);
136
+ }
137
+ }
138
+ /**
139
+ * The /compact command's live half. Mid-turn: fire now. Idle: arm for the next run bind.
140
+ * Returns what it did (for the command's display line / tests). No-op ('offline') without a live
141
+ * wire; 'unsupported' when the engine's capabilities said manualCompact:false ([488] TOC-local) —
142
+ * the client-side projection compaction still runs, but the ENGINE session context does not shrink.
143
+ */
144
+ export function requestEngineCompact() {
145
+ if (!engineWireTarget())
146
+ return 'offline';
147
+ probeManualCompactCapability(); // lazy, cached; first call may still ride the old path (fail-open)
148
+ if (manualCompactCapable === false) {
149
+ hostLog('debug', 'engineCompactWire: engine capabilities report manualCompact:false — verb suppressed (TOC-local runStore-less engine, [488])');
150
+ return 'unsupported';
151
+ }
152
+ const taskId = activeEngineRunId();
153
+ if (taskId) {
154
+ void postCompact(taskId);
155
+ return 'fired';
156
+ }
157
+ pendingEngineCompact = true;
158
+ return 'armed';
159
+ }
160
+ /**
161
+ * host run_started hook: a new engine run just bound. If a manual compact is pending, fire it —
162
+ * the engine compacts at this turn's first safe boundary.
163
+ *
164
+ * Timing: the bind fires off the X-Task-Id RESPONSE HEADER — the earliest possible signal, which can
165
+ * precede the server's run-row persist (a straight POST here raced it to a 404 in the live harness).
166
+ * Compaction runs at the next safe TURN BOUNDARY anyway, so a short defer loses nothing; retry once
167
+ * more for slow persists. Both shots are best-effort (postCompact never throws).
168
+ */
169
+ const BOUND_FIRE_DELAY_MS = 1500;
170
+ export function onEngineTaskBound(taskId) {
171
+ probeManualCompactCapability(); // warm the capability cache off the first turn bind, before any /compact
172
+ if (!pendingEngineCompact)
173
+ return;
174
+ pendingEngineCompact = false;
175
+ deferFire(BOUND_FIRE_DELAY_MS, () => void postCompact(taskId), 'bind 后发射');
176
+ }
@@ -0,0 +1,44 @@
1
+ /** 记一个子代行的转录锚(幂等;server <1.278 永不调用 = 台账恒空,取件腿整条静默不动)。 */
2
+ export declare function recordEngineTranscriptId(taskId: string, transcriptId: string): void;
3
+ /** 读面(取件腿 + 测试)。 */
4
+ export declare function getEngineTranscriptId(taskId: string): string | undefined;
5
+ /** 测试钩:清台账 + 在飞记号。 */
6
+ export declare function __resetEngineDelegatedPromptForTests(): void;
7
+ /**
8
+ * content → 纯文本。tool_result 型正文返回 undefined —— 那是子代自己跑工具的回填,**不是**委派
9
+ * prompt(这条判别就是「别把子代自己的消息当委派 prompt」的落点)。
10
+ */
11
+ export declare function delegatedPromptText(content: unknown): string | undefined;
12
+ /**
13
+ * 从转录里挑出委派 prompt = **最后一条非 tool_result 的 user 消息**(倒扫)。
14
+ * 🔴 倒扫不是随手选的:fork 型子代的转录以**父会话历史**开头,正扫会拿到宿主自己的首轮提示
15
+ * (live pty 实测的错答案);全新子会话型只有一条 user 文本消息,倒扫结果与正扫相同。
16
+ */
17
+ export declare function pickDelegatedPrompt(messages: readonly unknown[]): string | undefined;
18
+ /** 读面注入点(离线测试用):替身返回转录窗口条目数组。 */
19
+ type TranscriptWindowReader = (transcriptId: string) => Promise<readonly unknown[] | null>;
20
+ export declare function __setTranscriptWindowReaderForTests(r: TranscriptWindowReader | null): void;
21
+ /** `fetchDelegatedPrompt` 的结果 —— 三态,**端必须分得开**(见下面 outcome 的键注)。 */
22
+ export interface DelegatedPromptFetch {
23
+ /** 挑到的委派 prompt;`undefined` = 没拿到(看 `outcome` 分清是哪种没拿到)。 */
24
+ prompt?: string;
25
+ /**
26
+ * · `'hit'` —— 拿到了,端可以落状态。
27
+ * · `'skipped'` —— 无转录锚 / 已有在飞 ⇒ 端什么都别做(不是失败)。
28
+ * · `'unreadable'` —— 压根没读到(无 wire / 构造失败 / throw)⇒ **记号已撤**,下次可重试。
29
+ * · `'absent'` —— 读成功但转录里挑不出委派 user 消息 ⇒ **定论**,记号保留,不再重复打读面。
30
+ *
31
+ * 🔴 `unreadable` 与 `absent` 分开是复审穷举第 5 条:混成一个会把「一次性取件」变成
32
+ * 「每次进查看态都打一发 `GET /v1/sessions/:id`」。
33
+ */
34
+ outcome: 'hit' | 'skipped' | 'unreadable' | 'absent';
35
+ }
36
+ /**
37
+ * 取一个后台子代行的**委派 prompt**(判定半场:防抖 + 取锚 + 读转录 + 挑正文)。
38
+ * 幂等、fail-soft、绝不 throw。**不碰任何端状态** —— 落状态是端的事(见文件头拆缝说明)。
39
+ *
40
+ * 端侧用法(壳 `hydrateEngineAgentPrompt` shim):先自己判「这行是不是 local_agent、prompt 是不是
41
+ * 已经有了」,是才调本函数;拿到 `outcome==='hit'` 再 `setState`。
42
+ */
43
+ export declare function fetchDelegatedPrompt(taskId: string): Promise<DelegatedPromptFetch>;
44
+ export {};
@@ -0,0 +1,205 @@
1
+ /**
2
+ * ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6 + §2.5 拆缝表):cli `src/sema/engineDelegatedPrompt.ts` 的
3
+ * **判定半场** —— 转录锚台账(module 级)+ 委派 prompt 挑选规则 + 转录窗口读面 + 在飞防抖。
4
+ *
5
+ * 留在 TUI(**执行半场**):`hydrateEngineAgentPrompt` 里对 `AppState.tasks[taskId]` 的
6
+ * `isLocalAgentTask` 判别 / `store.setState` 落 `prompt` / 查看态转录补插首条 user 消息 ——
7
+ * 那三件读写的是**端的任务状态形状**(`LocalAgentTaskState`),web/桌面各有各的形。
8
+ * 壳侧 shim 保留原函数名与签名,内部改成「调本包 `fetchDelegatedPrompt()` → 拿到串再 setState」。
9
+ *
10
+ * 🔴 **单实例纪律(这条是本文件搬迁的真正理由,不是顺手)**:`transcriptIdByTask` 是 module 级
11
+ * Map,**写者 = `fleetLedger` 的行帧/通知帧消费臂**(`recordEngineTranscriptId`),
12
+ * **读者 = 取件腿**。fleetLedger 已在本包内 ⇒ 台账若留在壳里就是「包写壳读」两份实例,
13
+ * 取件腿恒读空、Prompt 段恒留白,而两边代码看着都对
14
+ * ([paired-mechanisms-must-share-premise] 同族失败形)。所以台账必须与写者同包。
15
+ *
16
+ * ── 以下为原文件的领域说明(逐字保留,它记着两条踩过的坑)────────────────────────────────────
17
+ *
18
+ * engineDelegatedPrompt — 后台子代**委派 prompt** 的取件腿(server 1.278.0 `transcriptId` 提货)。
19
+ *
20
+ * ── 缺陷形
21
+ * 后台子代行是行帧 lane(fleet stream)投影出来的,行帧上**没有** prompt —— 于是
22
+ * `AsyncAgentDetailDialog` 的 `Prompt` 段恒空(row.prompt = '')。前台子代的 prompt 由
23
+ * `task_progress` tick 的绑定臂晚绑,后台子代整跑零 tick(tick lane 根本没通道),所以那条腿
24
+ * 对它无效。
25
+ *
26
+ * ── 修法(不发明:server 1.278.0 在 fleet 行上加了 `transcriptId`)
27
+ * `transcriptId` = core 铸的**子代 childSessionId**(core subagent.js:`transcriptId: child.sessionId`)。
28
+ * 它是**取件锚不是内容** —— 端凭它去 session 读面(`GET /v1/sessions/:id`,SDK
29
+ * `sessions.get`)取转录,从里面挑出委派 prompt。
30
+ *
31
+ * 🔴 取哪一条 —— **最后一条非 tool_result 的 user 消息**,不是第一条(live 一手取证订正):
32
+ * `fork` 型子代的 `child.sessionId` 是**父会话的 fork**(core subagent.js:1749
33
+ * `forkTranscriptId = child.sessionId ?? forkedId`),转录里先是**父会话的全部历史**,委派
34
+ * prompt 追加在 fork 点。所以「第一条 user 消息」拿到的是**宿主自己的首轮提示**——第一版就是
35
+ * 这么写的,live pty 实测拉回 6869 字符(委派 prompt 只有 ~330),典型的「看起来很合理的错答案」。
36
+ * 反过来,全新子会话型子代的转录里委派 prompt 是唯一一条 user 文本消息 —— 两种形状都满足
37
+ * 「最后一条非 tool_result 的 user 消息」,所以倒扫是同时正确的那一条规则。
38
+ * (`tool_result` 型 user 条目 = 子代自己跑工具的回填,必须跳过。)
39
+ * 窗口取 `tail=N`(转录尾部)+ `blobCap`:委派 prompt 在尾部方向,而 `messages` 整段可以非常大。
40
+ * 诚实边界:子代被 steer 过(steerSubagent 注入 user 消息)时倒扫会拿到最新那条指令 —— 已记账,
41
+ * wire 上没有「哪条是委派」的标,端不猜。
42
+ *
43
+ * 诚实边界:拉不到 / 没有 transcriptId / 读面 501/404 ⇒ **什么都不写**,Prompt 段维持留白。
44
+ * 绝不用「子代的首条输出」或行描述冒充委派 prompt(那是把一个看起来合理的错答案摆上屏)。
45
+ * 一切失败 fail-soft,绝不 throw。
46
+ */
47
+ import { hostLog } from '../host.js';
48
+ import { makeEngineWireClient } from '../engineWireSdk.js';
49
+ import { engineWireTarget } from '../engineWireTarget.js';
50
+ /** 只拉转录**尾部**这么多条(委派 prompt 在尾部方向;见文件头的 fork 形状订正)。 */
51
+ const TAIL_WINDOW = 200;
52
+ /** 单条正文的上限:详情页自己还会截到 300 字符,这里只防拉回一个巨型 blob。 */
53
+ const BLOB_CAP = 8 * 1024;
54
+ // ── 转录锚台账(module-level:footer/REPL 谁重挂都不丢;写者=行帧消费臂)───────────────────────
55
+ const transcriptIdByTask = new Map();
56
+ /** 记一个子代行的转录锚(幂等;server <1.278 永不调用 = 台账恒空,取件腿整条静默不动)。 */
57
+ export function recordEngineTranscriptId(taskId, transcriptId) {
58
+ if (!taskId || !transcriptId)
59
+ return;
60
+ transcriptIdByTask.set(taskId, transcriptId);
61
+ }
62
+ /** 读面(取件腿 + 测试)。 */
63
+ export function getEngineTranscriptId(taskId) {
64
+ return transcriptIdByTask.get(taskId);
65
+ }
66
+ /** 测试钩:清台账 + 在飞记号。 */
67
+ export function __resetEngineDelegatedPromptForTests() {
68
+ transcriptIdByTask.clear();
69
+ promptHydrateInFlight.clear();
70
+ }
71
+ /** 每 task 一次在飞防抖;拉不到即撤记号,下次进入查看态可重试(副本重启/瞬态 404)。 */
72
+ const promptHydrateInFlight = new Set();
73
+ /** 一条转录条目的 role(permissive:`role` 或 CC jsonl 形的 `type`)。 */
74
+ function roleOf(entry) {
75
+ if (typeof entry !== 'object' || entry === null)
76
+ return undefined;
77
+ const e = entry;
78
+ if (typeof e.role === 'string')
79
+ return e.role;
80
+ if (typeof e.type === 'string')
81
+ return e.type;
82
+ // CC jsonl 形:{type:'user', message:{role:'user', content}}
83
+ if (typeof e.message === 'object' && e.message !== null) {
84
+ const m = e.message;
85
+ if (typeof m.role === 'string')
86
+ return m.role;
87
+ }
88
+ return undefined;
89
+ }
90
+ /** 一条转录条目的正文 content(permissive:直挂 或 嵌在 `message` 里)。 */
91
+ function contentOf(entry) {
92
+ if (typeof entry !== 'object' || entry === null)
93
+ return undefined;
94
+ const e = entry;
95
+ if (e.content !== undefined)
96
+ return e.content;
97
+ if (typeof e.message === 'object' && e.message !== null) {
98
+ return e.message.content;
99
+ }
100
+ return undefined;
101
+ }
102
+ /**
103
+ * content → 纯文本。tool_result 型正文返回 undefined —— 那是子代自己跑工具的回填,**不是**委派
104
+ * prompt(这条判别就是「别把子代自己的消息当委派 prompt」的落点)。
105
+ */
106
+ export function delegatedPromptText(content) {
107
+ if (typeof content === 'string')
108
+ return content.trim() || undefined;
109
+ if (!Array.isArray(content))
110
+ return undefined;
111
+ const parts = [];
112
+ for (const block of content) {
113
+ if (typeof block === 'string') {
114
+ parts.push(block);
115
+ continue;
116
+ }
117
+ if (typeof block !== 'object' || block === null)
118
+ continue;
119
+ const b = block;
120
+ if (b.type === 'tool_result')
121
+ return undefined; // 子代自己的工具回填,不是委派 prompt
122
+ if (b.type === 'text' && typeof b.text === 'string')
123
+ parts.push(b.text);
124
+ }
125
+ const text = parts.join('\n').trim();
126
+ return text || undefined;
127
+ }
128
+ /**
129
+ * 从转录里挑出委派 prompt = **最后一条非 tool_result 的 user 消息**(倒扫)。
130
+ * 🔴 倒扫不是随手选的:fork 型子代的转录以**父会话历史**开头,正扫会拿到宿主自己的首轮提示
131
+ * (live pty 实测的错答案);全新子会话型只有一条 user 文本消息,倒扫结果与正扫相同。
132
+ */
133
+ export function pickDelegatedPrompt(messages) {
134
+ for (let i = messages.length - 1; i >= 0; i--) {
135
+ const entry = messages[i];
136
+ if (roleOf(entry) !== 'user')
137
+ continue;
138
+ const text = delegatedPromptText(contentOf(entry));
139
+ if (text)
140
+ return text;
141
+ }
142
+ return undefined;
143
+ }
144
+ let transcriptWindowReader = null;
145
+ export function __setTranscriptWindowReaderForTests(r) {
146
+ transcriptWindowReader = r;
147
+ }
148
+ async function readTranscriptWindow(transcriptId) {
149
+ if (transcriptWindowReader)
150
+ return transcriptWindowReader(transcriptId);
151
+ // 🔴 B6:六份手抄 `wireConfig()` 收成 `engineWireTarget()`(env 读经 hostEnv,本包零 process)。
152
+ const cfg = engineWireTarget();
153
+ if (!cfg)
154
+ return null;
155
+ const client = makeEngineWireClient({
156
+ baseUrl: cfg.baseUrl,
157
+ ...(cfg.token ? { token: cfg.token } : {}),
158
+ principal: cfg.principal,
159
+ });
160
+ if (!client)
161
+ return null;
162
+ // 窗口读:`tail=N` = 转录**尾部** N 条(server ≥1.162)。老引擎忽略 query 返回全量 ——
163
+ // 倒扫规则对全量同样正确,只是多传了点字节(行为等价)。
164
+ const rec = await client.sessions.get(transcriptId, {
165
+ tail: TAIL_WINDOW,
166
+ blobCap: BLOB_CAP,
167
+ });
168
+ const msgs = rec.messages;
169
+ return Array.isArray(msgs) ? msgs : null;
170
+ }
171
+ /**
172
+ * 取一个后台子代行的**委派 prompt**(判定半场:防抖 + 取锚 + 读转录 + 挑正文)。
173
+ * 幂等、fail-soft、绝不 throw。**不碰任何端状态** —— 落状态是端的事(见文件头拆缝说明)。
174
+ *
175
+ * 端侧用法(壳 `hydrateEngineAgentPrompt` shim):先自己判「这行是不是 local_agent、prompt 是不是
176
+ * 已经有了」,是才调本函数;拿到 `outcome==='hit'` 再 `setState`。
177
+ */
178
+ export async function fetchDelegatedPrompt(taskId) {
179
+ if (promptHydrateInFlight.has(taskId))
180
+ return { outcome: 'skipped' };
181
+ const transcriptId = transcriptIdByTask.get(taskId);
182
+ if (!transcriptId)
183
+ return { outcome: 'skipped' };
184
+ promptHydrateInFlight.add(taskId);
185
+ let msgs = null;
186
+ try {
187
+ msgs = await readTranscriptWindow(transcriptId);
188
+ }
189
+ catch (e) {
190
+ promptHydrateInFlight.delete(taskId);
191
+ hostLog('debug', `[delegated-prompt] ${taskId} transcript ${transcriptId} read failed: ${String(e).slice(0, 160)}`);
192
+ return { outcome: 'unreadable' };
193
+ }
194
+ const prompt = msgs ? pickDelegatedPrompt(msgs) : undefined;
195
+ if (!prompt) {
196
+ if (msgs === null)
197
+ promptHydrateInFlight.delete(taskId);
198
+ hostLog('debug', `[delegated-prompt] ${taskId} transcript ${transcriptId}: ${msgs === null ? 'transcript unreadable (retryable)' : 'no delegated user message (final)'}`);
199
+ return { outcome: msgs === null ? 'unreadable' : 'absent' };
200
+ }
201
+ // 诊断锚(SEMA_DEBUG 才落盘):字符数 + 头 60 字符 —— 「拿到的到底是委派 prompt 还是
202
+ // fork 转录里宿主自己的首轮提示」只有看得见正文头才分得开(第一版倒在这上面)。
203
+ hostLog('debug', `[delegated-prompt] ${taskId} <- transcript ${transcriptId} (${prompt.length} chars) head=${JSON.stringify(prompt.slice(0, 60))}`);
204
+ return { prompt, outcome: 'hit' };
205
+ }
@@ -0,0 +1,18 @@
1
+ /** 引擎 bridge 从 `task_progress` tick / fleet 行帧建的行(module-level:footer 重挂不丢归属)。 */
2
+ export declare const engineOwnedRowIds: Set<string>;
3
+ /** fleet 树 enter 时为「面板无行的 fleet 行」stub 出来的纯查看载体行。 */
4
+ export declare const fleetViewStubRowIds: Set<string>;
5
+ /** 这一行的真身在引擎里(本地 abortController 管不到它)。 */
6
+ export declare function isEngineViewRow(taskId: string): boolean;
7
+ /** 能力门同步读口(liveClient 构造期已 kick /v1/capabilities 进 engineCapsCache)。 */
8
+ export declare function engineTaskHandlesCapable(): boolean;
9
+ /**
10
+ * 停这一行**必须先拿到引擎回执**吗?
11
+ *
12
+ * true ⇒ 行的执行者是引擎,端写 `killed` 是一句它没证实过的断言 —— 必须 `POST …/stop` 拿到 200
13
+ * 才翻;409 `stop.not_landed` / `stop.not_local` / `stop.parked` 一律保持原状(引擎里
14
+ * 可能还在跑),下一拍由行帧/`stopped-by` 归因 settle(黑板 [1754]③)。
15
+ * false ⇒ 行跑在本进程(CC 逐字形态),`abortController.abort()` 就是执行本身,端写 `killed`
16
+ * 按构造恒真 ⇒ 同步翻,与 CC 2.1.220 `UPd`(pretty220.js:507299)一字不差。
17
+ */
18
+ export declare function engineRowNeedsStopConfirm(taskId: string): boolean;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):cli `src/sema/engineRowStopGate.ts` 整搬。
3
+ * 唯一差分 = `process.env.SEMA_LIVE_BASEURL` → `engineWireTarget()?.baseUrl`
4
+ * (本包零 `process`;Node 宿主下 `hostEnv()` 返回的就是 `process.env` 同一对象 ⇒ 取值相同。
5
+ * **超集**:非 Node 宿主可用 `installEngineWireTarget()` 显式装,此前那条腿恒 false)。
6
+ *
7
+ * 🔴 单实例纪律(本文件两个 module 级 Set 都是判据真源):
8
+ * `engineOwnedRowIds` 写者 = 引擎 bridge 的 `task_progress` tick / fleet 行帧臂;
9
+ * `fleetViewStubRowIds` 写者 = fleet 树 enter 的 stub 造行处;
10
+ * 读者 = 批量停/单行停的同步分诊。两份实例 ⇒ 分诊恒判「纯本地行」⇒ 引擎侧行被壳直接写
11
+ * `killed` 而引擎里还在跑 = [1754]③ 那类「壳说了一句它没证实过的话」。
12
+ *
13
+ * ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
14
+ *
15
+ * engineRowStopGate — 「这一行是引擎侧的吗 / 停它要不要等引擎回执」的**同步**判别叶模块(#98)。
16
+ *
17
+ * 为什么单独一个叶模块:批量停(`ctrl+x ctrl+k` → `killAllRunningAgentTasks`)必须在**同步**
18
+ * 那一拍就把行分成两半 —— 纯本地行照 CC 逐字立刻翻 `killed`(CC 的 markNotified→kill 顺序不容
19
+ * 插入一个 await,否则 runner 自己的完成通知会抢在 markNotified 前面发出去),引擎侧行才走
20
+ * 「等 200 再翻」。而原先这两个判据分别住在 `engineAgentView`(拉 React/ink/transcript 全图)和
21
+ * `engineTaskHandleWire`(拉 SDK),`LocalAgentTask` 静态 import 任一都是循环依赖风险 —— 所以此前
22
+ * 只能 `await import(...)`,分类天然异步。把判据本身下沉成零依赖叶(只依赖同样零依赖的
23
+ * `engineCapsCache`),两边改成 re-export,判据单一真源不变,调用方拿到同步口。
24
+ *
25
+ * 🔴 语义与单行 `x`(BackgroundTasksDialog.killEngineAwareAgentTask)必须逐字一致:
26
+ * 引擎视图行 ∧ `capabilities.taskHandles` 在场 → 发真停止动词、**只有 200 才翻行**;
27
+ * 缺任一(老引擎 / 无 wire / 纯本地 AgentTool 行) → 回落旧行为(本地 abort 即真停,立即翻)。
28
+ */
29
+ import { engineCapTrue } from '../engineCapsCache.js';
30
+ import { engineWireTarget } from '../engineWireTarget.js';
31
+ /** 引擎 bridge 从 `task_progress` tick / fleet 行帧建的行(module-level:footer 重挂不丢归属)。 */
32
+ export const engineOwnedRowIds = new Set();
33
+ /** fleet 树 enter 时为「面板无行的 fleet 行」stub 出来的纯查看载体行。 */
34
+ export const fleetViewStubRowIds = new Set();
35
+ /** 这一行的真身在引擎里(本地 abortController 管不到它)。 */
36
+ export function isEngineViewRow(taskId) {
37
+ return engineOwnedRowIds.has(taskId) || fleetViewStubRowIds.has(taskId);
38
+ }
39
+ /** 能力门同步读口(liveClient 构造期已 kick /v1/capabilities 进 engineCapsCache)。 */
40
+ export function engineTaskHandlesCapable() {
41
+ return engineCapTrue(engineWireTarget()?.baseUrl, 'taskHandles');
42
+ }
43
+ /**
44
+ * 停这一行**必须先拿到引擎回执**吗?
45
+ *
46
+ * true ⇒ 行的执行者是引擎,端写 `killed` 是一句它没证实过的断言 —— 必须 `POST …/stop` 拿到 200
47
+ * 才翻;409 `stop.not_landed` / `stop.not_local` / `stop.parked` 一律保持原状(引擎里
48
+ * 可能还在跑),下一拍由行帧/`stopped-by` 归因 settle(黑板 [1754]③)。
49
+ * false ⇒ 行跑在本进程(CC 逐字形态),`abortController.abort()` 就是执行本身,端写 `killed`
50
+ * 按构造恒真 ⇒ 同步翻,与 CC 2.1.220 `UPd`(pretty220.js:507299)一字不差。
51
+ */
52
+ export function engineRowNeedsStopConfirm(taskId) {
53
+ return isEngineViewRow(taskId) && engineTaskHandlesCapable();
54
+ }
@@ -0,0 +1,15 @@
1
+ /** 测试钩:清 caps 缓存。 */
2
+ export declare function __resetSubagentOutputCapCacheForTests(): void;
3
+ export interface EngineSubagentReport {
4
+ /** 子代最终 assistant 正文(server 对 running 态给状态文本——由 status 区分)。 */
5
+ content: string;
6
+ /** output.status 原样(completed/failed/killed/running…;undefined = wire 未带)。 */
7
+ status?: string;
8
+ }
9
+ /**
10
+ * 拉取一个后台子代的终报正文。null = 读不到(无 wire / caps 缺 / 404 域外或副本重启 / 网错)——
11
+ * 调用方回落 bg 事实渲染。绝不 throw。
12
+ */
13
+ export declare function fetchEngineSubagentReport(taskId: string, opts?: {
14
+ signal?: AbortSignal;
15
+ }): Promise<EngineSubagentReport | null>;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * ⇄ B6 批搬迁(2026-07-27,设计稿 §3 B6):cli `src/sema/engineSubagentOutput.ts` 整搬。
3
+ *
4
+ * 🔴 搬迁三处差分(全部是「端资产上移到 port」,行为一字节不变):
5
+ * · `wireConfig()`(六份手抄之一)→ `engineWireTarget()`;
6
+ * · `getActiveEngineTaskId()`(engineToolDetach,pty 绑定的活跃 run)→ `activeEngineRunId()`
7
+ * (SessionPort.activeRunId);
8
+ * · `logForDebugging` → `hostLog('debug', …)`。
9
+ *
10
+ * ── 以下为原文件的领域说明(逐字保留)────────────────────────────────────────────────────────
11
+ *
12
+ * engineSubagentOutput — 后台子代终报读面消费(server 1.244 / SDK 0.0.65 提货,提货单 [1488]③(b))。
13
+ *
14
+ * Wire: `GET /v1/runs/:runId/subagents/:handle/output` → {taskId, target, content, output:{status,…}}
15
+ * · content = 子代最终 assistant 正文(终态)或诚实 running 状态文本;
16
+ * · runId = spawn 该子代的 run(registry access 从 run 行推导——寻址映射由 fleet 行 parentId /
17
+ * bg_notification.parentTaskId 喂 subagentContentStore.recordBgParentRun);
18
+ * · a… 域(background_agent)专用——wa… 观测行恒 404(workflow journal 是那边的读面),调用方
19
+ * 按 null 回落 bg 事实渲染,fail-soft;
20
+ * · caps 探测 capabilities.subagentOutput(selfKnowledge caps 同款纪律:true 固化/false TTL/
21
+ * 探测失败不缓存)。老引擎(<1.244)= caps 无该键 → 恒走回落,version-safe。
22
+ *
23
+ * 副本域注意([1491]④):TaskRegistry 是进程内存态——引擎重启后 handle 消失(404),按 null 回落,
24
+ * 绝不把 404 当异常上抛。
25
+ *
26
+ * session 作用域([1498]③,server 1.245.0 / SDK 0.0.66):session-bound run 的 subagentOutput
27
+ * 必须带匹配的 `?session=`,否则 404 fail-closed(这个读面正是 [1493]② 双开串扰的读侧入口,
28
+ * server 侧已把它闩死)。端无条件带 session 即双版本兼容——1.244 忽略该参数,1.245 起必需。
29
+ * 🔴 端漏装 SessionPort ⇒ `engineSessionParam()` 恒 undefined ⇒ 本读面对 session-bound run
30
+ * 一律 404 ⇒ 整条子代终报静默哑掉(所以 `hostSession()` 计 miss,见 host.ts)。
31
+ */
32
+ import { hostLog } from '../host.js';
33
+ import { makeEngineWireClient } from '../engineWireSdk.js';
34
+ import { engineWireTarget } from '../engineWireTarget.js';
35
+ import { getBgParentRun } from '../subagentContentStore.js';
36
+ import { activeEngineRunId, engineSessionParam } from '../engineSessionParam.js';
37
+ // ── caps 探测(true 固化 / false TTL / 失败不缓存)────────────────────────────────────────────
38
+ const capByBase = new Map();
39
+ const CAP_FALSE_RETRY_TTL_MS = 5 * 60_000;
40
+ async function subagentOutputCapable(client, baseUrl, signal) {
41
+ const hit = capByBase.get(baseUrl);
42
+ if (hit && (hit.ok || Date.now() - hit.at < CAP_FALSE_RETRY_TTL_MS))
43
+ return hit.ok;
44
+ try {
45
+ // SDK 0.0.77 起 capabilities() 有真型(Capabilities 开集)——宽形 cast 已撤;键仍防御读。
46
+ const caps = await client.capabilities({ ...(signal ? { signal } : {}) });
47
+ const ok = caps.subagentOutput === true;
48
+ capByBase.set(baseUrl, { ok, at: Date.now() });
49
+ return ok;
50
+ }
51
+ catch {
52
+ return false; // 探测失败不缓存(引擎瞬断):本次回落,下次再判
53
+ }
54
+ }
55
+ /** 测试钩:清 caps 缓存。 */
56
+ export function __resetSubagentOutputCapCacheForTests() {
57
+ capByBase.clear();
58
+ }
59
+ /**
60
+ * 拉取一个后台子代的终报正文。null = 读不到(无 wire / caps 缺 / 404 域外或副本重启 / 网错)——
61
+ * 调用方回落 bg 事实渲染。绝不 throw。
62
+ */
63
+ export async function fetchEngineSubagentReport(taskId, opts) {
64
+ const cfg = engineWireTarget();
65
+ if (!cfg)
66
+ return null;
67
+ const runId = getBgParentRun(taskId) ?? activeEngineRunId();
68
+ if (!runId)
69
+ return null;
70
+ const client = makeEngineWireClient({
71
+ baseUrl: cfg.baseUrl,
72
+ ...(cfg.token ? { token: cfg.token } : {}),
73
+ principal: cfg.principal,
74
+ });
75
+ if (!client)
76
+ return null;
77
+ if (!(await subagentOutputCapable(client, cfg.baseUrl, opts?.signal)))
78
+ return null;
79
+ try {
80
+ // [1498]③:无条件带 session(1.244 忽略/1.245 必需,双版本兼容);缺席时(端未装 SessionPort /
81
+ // 端自己还没有值)不带——1.245 对 session-bound run 会 404,按既有 null 回落,下次重试自然带上。
82
+ const session = engineSessionParam();
83
+ const r = await client.runs.subagentOutput(runId, taskId, {
84
+ ...(opts?.signal ? { signal: opts.signal } : {}),
85
+ ...(session ? { session } : {}),
86
+ });
87
+ const content = typeof r?.content === 'string' ? r.content : '';
88
+ if (!content.trim())
89
+ return null;
90
+ const status = typeof r?.output?.status === 'string' ? r.output.status : undefined;
91
+ return { content, ...(status !== undefined ? { status } : {}) };
92
+ }
93
+ catch (e) {
94
+ const status = e?.status;
95
+ hostLog('debug', `[subagent-output] ${taskId} via run ${runId}: ${typeof status === 'number' ? status : String(e).slice(0, 160)}`);
96
+ return null;
97
+ }
98
+ }
@@ -0,0 +1,10 @@
1
+ export type SubagentSteerOutcome = {
2
+ ok: true;
3
+ receipt: string;
4
+ } | {
5
+ ok: false;
6
+ reason: 'no-wire' | 'no-run' | 'not-running' | 'error';
7
+ detail?: string;
8
+ };
9
+ /** Steer a running engine subagent (target = its parentToolCallId, or agentName). */
10
+ export declare function steerEngineSubagent(target: string, text: string): Promise<SubagentSteerOutcome>;