@sema-agent/client-core 0.2.0 → 0.4.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 (80) hide show
  1. package/README.md +23 -1
  2. package/dist/agentsWireCaps.d.ts +112 -0
  3. package/dist/agentsWireCaps.js +319 -0
  4. package/dist/attachmentsWireCaps.d.ts +47 -0
  5. package/dist/attachmentsWireCaps.js +46 -0
  6. package/dist/classifierVerdictWire.d.ts +45 -0
  7. package/dist/classifierVerdictWire.js +76 -0
  8. package/dist/clientContextWireCaps.d.ts +46 -0
  9. package/dist/clientContextWireCaps.js +48 -0
  10. package/dist/cloudConfigWireCaps.d.ts +110 -0
  11. package/dist/cloudConfigWireCaps.js +228 -0
  12. package/dist/controlRouter.d.ts +191 -0
  13. package/dist/controlRouter.js +244 -0
  14. package/dist/effortWire.d.ts +34 -0
  15. package/dist/effortWire.js +38 -0
  16. package/dist/engineAgentPanelStore.d.ts +111 -0
  17. package/dist/engineAgentPanelStore.js +93 -0
  18. package/dist/engineCapsCache.d.ts +32 -0
  19. package/dist/engineCapsCache.js +64 -0
  20. package/dist/engineInlineTaskStats.d.ts +66 -0
  21. package/dist/engineInlineTaskStats.js +97 -0
  22. package/dist/engineToolLabelStore.d.ts +32 -0
  23. package/dist/engineToolLabelStore.js +55 -0
  24. package/dist/engineWireSdk.d.ts +49 -0
  25. package/dist/engineWireSdk.js +61 -0
  26. package/dist/fleetAgentPanelProjection.d.ts +31 -0
  27. package/dist/fleetAgentPanelProjection.js +0 -0
  28. package/dist/fleetTaskDesc.d.ts +20 -0
  29. package/dist/fleetTaskDesc.js +35 -0
  30. package/dist/forkWireCaps.d.ts +25 -0
  31. package/dist/forkWireCaps.js +42 -0
  32. package/dist/hostEnv.d.ts +16 -0
  33. package/dist/hostEnv.js +17 -0
  34. package/dist/imagesWireCaps.d.ts +28 -0
  35. package/dist/imagesWireCaps.js +49 -0
  36. package/dist/index.d.ts +56 -1
  37. package/dist/index.js +62 -1
  38. package/dist/liveModelCatalog.d.ts +62 -0
  39. package/dist/liveModelCatalog.js +79 -0
  40. package/dist/liveQuestionStore.d.ts +101 -0
  41. package/dist/liveQuestionStore.js +109 -0
  42. package/dist/mcpWireCaps.d.ts +41 -0
  43. package/dist/mcpWireCaps.js +51 -0
  44. package/dist/modelBudgetRule.d.ts +44 -0
  45. package/dist/modelBudgetRule.js +73 -0
  46. package/dist/modelWireCaps.d.ts +17 -0
  47. package/dist/modelWireCaps.js +16 -0
  48. package/dist/notifications.d.ts +150 -4
  49. package/dist/notifications.js +472 -0
  50. package/dist/permissionWireCaps.d.ts +37 -0
  51. package/dist/permissionWireCaps.js +46 -0
  52. package/dist/printToolResultFrame.d.ts +59 -0
  53. package/dist/printToolResultFrame.js +65 -0
  54. package/dist/promptProfileWireCaps.d.ts +17 -0
  55. package/dist/promptProfileWireCaps.js +20 -0
  56. package/dist/retainBackgroundWireCaps.d.ts +50 -0
  57. package/dist/retainBackgroundWireCaps.js +58 -0
  58. package/dist/rewindWireCaps.d.ts +36 -0
  59. package/dist/rewindWireCaps.js +41 -0
  60. package/dist/selfOrchestrationWireCaps.d.ts +40 -0
  61. package/dist/selfOrchestrationWireCaps.js +48 -0
  62. package/dist/sessionModelLatch.d.ts +35 -0
  63. package/dist/sessionModelLatch.js +63 -0
  64. package/dist/skillsWireCaps.d.ts +47 -0
  65. package/dist/skillsWireCaps.js +41 -0
  66. package/dist/sseIdleTriage.d.ts +94 -0
  67. package/dist/sseIdleTriage.js +142 -0
  68. package/dist/steering.d.ts +4 -0
  69. package/dist/steering.js +4 -0
  70. package/dist/subagentContentStore.d.ts +131 -0
  71. package/dist/subagentContentStore.js +322 -0
  72. package/dist/types/engineState.d.ts +116 -0
  73. package/dist/types/engineState.js +17 -0
  74. package/dist/ultracodeWireCaps.d.ts +90 -0
  75. package/dist/ultracodeWireCaps.js +159 -0
  76. package/dist/webSearchWireCaps.d.ts +54 -0
  77. package/dist/webSearchWireCaps.js +76 -0
  78. package/dist/workflow.d.ts +40 -8
  79. package/dist/workflow.js +69 -11
  80. package/package.json +1 -1
@@ -0,0 +1,322 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/subagentContentStore.ts 逐字搬入(零依赖;壳侧改 re-export)。
3
+ * 🔴 module 级台账(tasks/parentToTask/ownEngineRuns/bgFacts)= 单实例语义:写方(seam adapter
4
+ * runStream 的子代内容分流 + liveClient 的 run_started own 台账)与读方(查看态/帧级 own 判别)
5
+ * 必须走**同一个模块实例**,否则 own 判别恒 false(帧级校验静默失效)。
6
+ */
7
+ /**
8
+ * subagentContentStore — dep-free side-channel store for ENGINE subagent CONTENT events (C1 consumption,
9
+ * service 1.89 `forwardSubagentEvents` + core 1.219; blackboard [381]⑤/[383]③).
10
+ *
11
+ * WIRE FACTS: with `forwardSubagentEvents: true` on the task request, the live stream carries the
12
+ * subagent's text_delta / reasoning_delta / tool_start / tool_end stamped with EventIdentity
13
+ * {taskId, parentToolCallId} (§E1 redact upstream; §E2 identity). The seam adapter (runStream.ts)
14
+ * DIVERTS those events here — they must never reach the main-transcript pipeline (a subagent's tokens
15
+ * rendering as the leader's text is the pollution failure mode).
16
+ *
17
+ * WHAT IT KEEPS (bounded, per task):
18
+ * - a NEUTRAL item log: {kind:'text'|'thinking',text} | {kind:'tool',name,input,output,isError,id}
19
+ * (ring-capped) — the 查看态 transcript backlog, so entering a running agent mid-flight still shows
20
+ * history. Message SHAPES are built by the consumers (engineAgentView/chrome-agentprogress own the
21
+ * CC message creators; this store stays dep-free like engineAgentPanelStore).
22
+ * - live accumulation buffers (streaming text/thinking not yet segment-closed).
23
+ * - taskId → parentToolCallId (the C2 steer target — service routes subagent steer by parentToolCallId).
24
+ *
25
+ * NOTIFY: coalesced (~250ms) per-task notify so a per-token delta storm never turns into per-token
26
+ * AppState writes; consumers pull a snapshot on notify.
27
+ */
28
+ const MAX_ITEMS_PER_TASK = 200;
29
+ const MAX_TASKS = 32;
30
+ const NOTIFY_COALESCE_MS = 250;
31
+ const tasks = new Map();
32
+ // parentToolCallId ↔ engine taskId aliasing: CONTENT events carry ONLY {eventId, parentToolCallId}
33
+ // (EventIdentity — no taskId on the wire), while the panel rows / 查看态 lookups key by the ENGINE task id
34
+ // (task_progress carries BOTH). Content arriving before the first tick parks under the parent key and is
35
+ // RE-KEYED when the alias registers.
36
+ const parentToTask = new Map();
37
+ const taskToParent = new Map();
38
+ let notifyListener = null;
39
+ const pendingNotify = new Set();
40
+ let notifyTimer = null;
41
+ /** LRU touch:Map 迭代序=插入序,容量淘汰取「最老」——不刷新的话是纯 FIFO,长会话里第 33 个
42
+ * 子代会把【仍在流式/正被查看】的最早行挤掉(扩散面复审 F11)。访问即 re-insert,活跃项恒新。 */
43
+ function touchLru(map, key) {
44
+ const v = map.get(key);
45
+ if (v !== undefined) {
46
+ map.delete(key);
47
+ map.set(key, v);
48
+ }
49
+ }
50
+ function stateFor(taskId, parentToolCallId) {
51
+ let s = tasks.get(taskId);
52
+ if (!s) {
53
+ // Bound total task footprint: evict the LEAST-RECENTLY-USED task's log (reads/writes touch below).
54
+ if (tasks.size >= MAX_TASKS) {
55
+ const oldest = tasks.keys().next().value;
56
+ if (oldest !== undefined)
57
+ tasks.delete(oldest);
58
+ }
59
+ s = { items: [], textBuf: '', thinkBuf: '', openTools: new Map(), parentToolCallId };
60
+ tasks.set(taskId, s);
61
+ }
62
+ else {
63
+ touchLru(tasks, taskId);
64
+ }
65
+ return s;
66
+ }
67
+ function pushItem(s, item) {
68
+ s.items.push(item);
69
+ if (s.items.length > MAX_ITEMS_PER_TASK) {
70
+ s.items.splice(0, s.items.length - MAX_ITEMS_PER_TASK);
71
+ // open-tool indexes shifted — rebuild from the surviving items
72
+ s.openTools.clear();
73
+ s.items.forEach((it, i) => {
74
+ if (it.kind === 'tool' && it.output === undefined)
75
+ s.openTools.set(it.id, i);
76
+ });
77
+ }
78
+ }
79
+ /** Close out streaming buffers into items (segment boundary: a tool starts, or the run settles). */
80
+ function flushBuffers(s) {
81
+ if (s.thinkBuf.trim())
82
+ pushItem(s, { kind: 'thinking', text: s.thinkBuf });
83
+ s.thinkBuf = '';
84
+ if (s.textBuf.trim())
85
+ pushItem(s, { kind: 'text', text: s.textBuf });
86
+ s.textBuf = '';
87
+ }
88
+ function scheduleNotify(taskId) {
89
+ pendingNotify.add(taskId);
90
+ if (notifyTimer)
91
+ return;
92
+ notifyTimer = setTimeout(() => {
93
+ notifyTimer = null;
94
+ const ids = [...pendingNotify];
95
+ pendingNotify.clear();
96
+ if (!notifyListener)
97
+ return;
98
+ for (const id of ids) {
99
+ try {
100
+ notifyListener(id);
101
+ }
102
+ catch {
103
+ // a consumer render error must never tear down the stream drain
104
+ }
105
+ }
106
+ }, NOTIFY_COALESCE_MS);
107
+ }
108
+ /** Register the parentToolCallId ↔ engine-taskId pair (from task_progress, which carries both).
109
+ * Content parked under the parent key migrates to the canonical task key. */
110
+ export function registerSubagentAlias(parentToolCallId, taskId) {
111
+ if (!parentToolCallId || !taskId || parentToolCallId === taskId)
112
+ return;
113
+ parentToTask.set(parentToolCallId, taskId);
114
+ taskToParent.set(taskId, parentToolCallId);
115
+ // a notify queued under the parent key before the alias landed must fire under the canonical key
116
+ if (pendingNotify.delete(parentToolCallId))
117
+ pendingNotify.add(taskId);
118
+ const parked = tasks.get(parentToolCallId);
119
+ if (parked && !tasks.has(taskId)) {
120
+ tasks.delete(parentToolCallId);
121
+ tasks.set(taskId, parked);
122
+ scheduleNotify(taskId);
123
+ }
124
+ }
125
+ function canonicalKey(ev) {
126
+ // the wire's content events carry no taskId — runStream passes parentToolCallId in both slots
127
+ if (ev.taskId !== ev.parentToolCallId)
128
+ return ev.taskId;
129
+ return parentToTask.get(ev.parentToolCallId) ?? ev.parentToolCallId;
130
+ }
131
+ export function publishSubagentContentEvent(ev) {
132
+ const key = canonicalKey(ev);
133
+ const s = stateFor(key, ev.parentToolCallId);
134
+ switch (ev.type) {
135
+ case 'text_delta':
136
+ if (typeof ev.delta === 'string')
137
+ s.textBuf += ev.delta;
138
+ break;
139
+ case 'reasoning_delta':
140
+ if (typeof ev.delta === 'string')
141
+ s.thinkBuf += ev.delta;
142
+ break;
143
+ case 'tool_start': {
144
+ flushBuffers(s);
145
+ const id = ev.toolCallId ?? `${ev.taskId}-tool-${s.items.length}`;
146
+ pushItem(s, { kind: 'tool', id, name: ev.toolName ?? 'Tool', input: ev.args ?? {} });
147
+ s.openTools.set(id, s.items.length - 1);
148
+ break;
149
+ }
150
+ case 'tool_end': {
151
+ const id = ev.toolCallId;
152
+ const idx = id !== undefined ? s.openTools.get(id) : undefined;
153
+ if (idx !== undefined && s.items[idx]?.kind === 'tool') {
154
+ const it = s.items[idx];
155
+ it.output = ev.output ?? '';
156
+ it.isError = ev.isError === true;
157
+ if (id !== undefined)
158
+ s.openTools.delete(id);
159
+ }
160
+ else {
161
+ // orphan close (start lost to the ring / arrived first) — synthesize a resolved card
162
+ pushItem(s, {
163
+ kind: 'tool',
164
+ id: id ?? `${ev.taskId}-tool-${s.items.length}`,
165
+ name: ev.toolName ?? 'Tool',
166
+ input: ev.args ?? {},
167
+ output: ev.output ?? '',
168
+ isError: ev.isError === true,
169
+ });
170
+ }
171
+ break;
172
+ }
173
+ }
174
+ scheduleNotify(key);
175
+ }
176
+ /** 查看态 composer echo (C2 steer optimistic display) — segment-closes the buffers first so the echo
177
+ * lands AFTER the text streamed before it. taskId may be unseen (row entered before any content):
178
+ * parentToolCallId may then be unknown — pass '' and the steer path resolves the target separately. */
179
+ export function pushSubagentLocalEcho(taskId, text) {
180
+ const s = stateFor(taskId, tasks.get(taskId)?.parentToolCallId ?? '');
181
+ flushBuffers(s);
182
+ pushItem(s, { kind: 'echo', text });
183
+ scheduleNotify(taskId);
184
+ }
185
+ export function getSubagentContentSnapshot(taskId) {
186
+ const key = tasks.has(taskId) ? taskId : (taskToParent.get(taskId) ?? '');
187
+ const s = tasks.get(key);
188
+ if (!s)
189
+ return null;
190
+ touchLru(tasks, key); // 正被查看的转录不因容量淘汰当场清空(F11)
191
+ return { items: [...s.items], textBuf: s.textBuf, thinkBuf: s.thinkBuf };
192
+ }
193
+ /**
194
+ * 把一个子代的内容账本投影成查看态渲染计划。
195
+ *
196
+ * [1568] 之前的形(壳侧 engineAgentView 里)有两处「等段闭合」:①遇到第一个未闭合工具就 break;
197
+ * ②textBuf/thinkBuf 完全不投影。实测(engine 1.275.0 真跑):数据面 ~23 帧/s 的 delta 连续到壳,
198
+ * 查看态却只在 tool 边界更新(进查看态后 6.0s 全空,之后每 5–15s 才动一次)。现形:在飞工具照投,
199
+ * 流式尾巴作为 live 槽投 —— 屏面粒度 = wire 粒度(store notify 已按 250ms 合并)。
200
+ */
201
+ export function planSubagentViewSlots(taskId) {
202
+ const snap = getSubagentContentSnapshot(taskId);
203
+ if (!snap)
204
+ return [];
205
+ const out = [];
206
+ snap.items.forEach((it, i) => {
207
+ if (it.kind === 'tool') {
208
+ out.push({
209
+ kind: 'tool',
210
+ slot: `u${i}`,
211
+ id: it.id,
212
+ name: it.name,
213
+ input: it.input,
214
+ ...(it.output !== undefined ? { output: it.output } : {}),
215
+ ...(it.isError === true ? { isError: true } : {}),
216
+ });
217
+ }
218
+ else if (it.kind === 'text') {
219
+ out.push({ kind: 'text', slot: `t${i}`, text: it.text });
220
+ }
221
+ else if (it.kind === 'thinking') {
222
+ out.push({ kind: 'thinking', slot: `k${i}`, text: it.text });
223
+ }
224
+ else {
225
+ out.push({ kind: 'echo', slot: `e${i}`, text: it.text });
226
+ }
227
+ });
228
+ // 尾巴顺序与 flushBuffers 落项顺序一致(thinking 先、text 后)——段闭合时屏面内容连续不跳。
229
+ if (snap.thinkBuf.trim()) {
230
+ out.push({ kind: 'thinking', slot: 'live-think', text: snap.thinkBuf, live: true });
231
+ }
232
+ if (snap.textBuf.trim()) {
233
+ out.push({ kind: 'text', slot: 'live-text', text: snap.textBuf, live: true });
234
+ }
235
+ return out;
236
+ }
237
+ /** C2 steer target: the service routes subagent steer by the delegating tool call's id. */
238
+ export function parentToolCallIdOf(taskId) {
239
+ return taskToParent.get(taskId) ?? tasks.get(taskId)?.parentToolCallId;
240
+ }
241
+ /** Settle a task's streaming buffers (turn ended / run settled) so the snapshot is complete. */
242
+ export function settleSubagentContent(taskId) {
243
+ const s = tasks.get(taskId);
244
+ if (!s)
245
+ return;
246
+ flushBuffers(s);
247
+ scheduleNotify(taskId);
248
+ }
249
+ export function clearSubagentContent(taskId) {
250
+ tasks.delete(taskId);
251
+ }
252
+ const bgFacts = new Map();
253
+ export function recordBgTerminalFacts(taskId, facts) {
254
+ if (!taskId)
255
+ return;
256
+ if (bgFacts.size >= MAX_TASKS && !bgFacts.has(taskId)) {
257
+ const oldest = bgFacts.keys().next().value;
258
+ if (oldest !== undefined)
259
+ bgFacts.delete(oldest);
260
+ }
261
+ bgFacts.delete(taskId); // LRU touch(F11):重录/更新即刷新插入序
262
+ bgFacts.set(taskId, facts);
263
+ scheduleNotify(taskId); // 打开中的查看态借既有 notify 链重建消息
264
+ }
265
+ export function getBgTerminalFacts(taskId) {
266
+ const key = bgFacts.has(taskId) ? taskId : (taskToParent.get(taskId) ?? '');
267
+ touchLru(bgFacts, key);
268
+ return bgFacts.get(key);
269
+ }
270
+ // ── bg 子代 → spawn 它的 leader run(server 1.244 subagentOutput 读面的寻址元组)──────────────
271
+ // GET /v1/runs/:runId/subagents/:handle/output 的 runId = spawn 该子代的 run(registry access 从
272
+ // run 行推导,[1491]①)。来源两路:fleet 行 parentId(出生帧即带)+ bg_notification.parentTaskId。
273
+ const bgParentRun = new Map();
274
+ export function recordBgParentRun(taskId, runId) {
275
+ if (!taskId || !runId || taskId === runId)
276
+ return;
277
+ if (bgParentRun.size >= MAX_TASKS * 2 && !bgParentRun.has(taskId)) {
278
+ const oldest = bgParentRun.keys().next().value;
279
+ if (oldest !== undefined)
280
+ bgParentRun.delete(oldest);
281
+ }
282
+ bgParentRun.delete(taskId); // LRU touch(F11):活跃映射不被容量淘汰挤掉 → 读面退错 runId
283
+ bgParentRun.set(taskId, runId);
284
+ }
285
+ export function getBgParentRun(taskId) {
286
+ touchLru(bgParentRun, taskId);
287
+ return bgParentRun.get(taskId);
288
+ }
289
+ // ── 本壳自己发起过的引擎 run 台账([1498]④ 帧级校验换锚)────────────────────────────────────
290
+ // 喂给:engineToolDetach.setActiveEngineTaskId(liveClient 从 run_started 帧绑定,本壳每个交互
291
+ // turn 的 run 必经)。与 bgParentRun 的关键区别:bgParentRun 由 fleet 行/通知帧喂(≤1.244 事实
292
+ // 广播下可能混入别的壳的行),这份台账**只**记本进程亲手驱动的 run,是帧级 own/foreign 判别的
293
+ // 唯一可信锚。进程内存态,壳重启即空(跨壳 resume 的历史 run 判不了 own——诚实边界,见
294
+ // fleetClient 帧级校验注释)。
295
+ const ownEngineRuns = new Set();
296
+ export function recordOwnEngineRun(runId) {
297
+ if (!runId)
298
+ return;
299
+ // 防御性上限:一个壳进程生命周期内 turn 数远小于此;溢出丢最老(Set 迭代序=插入序)。
300
+ if (ownEngineRuns.size >= 8192 && !ownEngineRuns.has(runId)) {
301
+ const oldest = ownEngineRuns.values().next().value;
302
+ if (oldest !== undefined)
303
+ ownEngineRuns.delete(oldest);
304
+ }
305
+ ownEngineRuns.add(runId);
306
+ }
307
+ export function isOwnEngineRun(runId) {
308
+ return ownEngineRuns.has(runId);
309
+ }
310
+ /** Single consumer (the panel hook). Returns an unsubscribe. */
311
+ export function subscribeSubagentContent(fn) {
312
+ notifyListener = fn;
313
+ // replay: anything already accumulated gets one notify so a late-mounting consumer catches up
314
+ for (const id of tasks.keys())
315
+ pendingNotify.add(id);
316
+ if (pendingNotify.size > 0)
317
+ scheduleNotify([...pendingNotify][0]);
318
+ return () => {
319
+ if (notifyListener === fn)
320
+ notifyListener = null;
321
+ };
322
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/types/engineState.ts 逐字搬入(纯类型,零运行时;壳侧改 re-export)。
3
+ * ⚠️ 归属注记:`EngineInstance`/`TOCConfig` 描述的是**宿主**职责(spawn/端口/日志路径),逻辑本体
4
+ * (engineLifecycleManager)按设计稿 §2.4.2-A 留 TUI;搬进本包的只是**形状**,让 desktop 宿主与
5
+ * TUI 宿主共用同一套 /health 与 engine.port 契约类型,不代表生命周期逻辑下沉。
6
+ */
7
+ /**
8
+ * TOC 一体化 (TP-A-P0-IMPL Step 5) — Engine Lifecycle 状态类型。
9
+ *
10
+ * 壳一体化起本地引擎(ai-agent-service file 模式),对用户透明,体验等同 CC 单进程。
11
+ * 本文件只定义跨 lifecycle 流转的纯数据形状,不含逻辑(逻辑在 engineLifecycleManager.ts)。
12
+ *
13
+ * 真相源契约:engine.port 文件是壳↔引擎的「当前活引擎」单一发现锚点(flock 串行化写,
14
+ * write-temp+fsync+mv 原子落盘)。HealthResponse 字段与引擎 server.ts:645-660 /health 逐字对齐
15
+ * (W2 同约定实现):instanceId/dataRoot/configHash/pid —— 壳据此判断「连对引擎」防陈旧 port 连错。
16
+ */
17
+ /** 引擎 GET /health 返回体(与 ai-agent-service server.ts:645-660 对齐;字段大多 additive 可缺)。 */
18
+ export interface HealthResponse {
19
+ /** 恒为 'ok';非 200 / 非此值 = 不健康。 */
20
+ status: string;
21
+ /** 当前模型 id(configHash 的一部分,信息性)。 */
22
+ model?: string;
23
+ /** 会话存储后端('file' for TOC local)。 */
24
+ sessionBackend?: string;
25
+ /** 活跃 session 数(信息性)。 */
26
+ sessions?: number;
27
+ /** 引擎实例身份(uuidv7,每次 boot 唯一)。陈旧 port 连错引擎时此值会变 → 拒连。 */
28
+ instanceId?: string;
29
+ /** 引擎 localDataRoot 解析结果。壳校验 === assembleChildEnv 注入的 LOCAL_DATA_ROOT。 */
30
+ dataRoot?: string;
31
+ /** restart-relevant config 的稳定指纹(无 secret)。drift 检测:configHash 变 → 拒连旧引擎。 */
32
+ configHash?: string;
33
+ /** 引擎进程 pid。orphan reap 的 kill-0 目标 + heartbeat 死活判定。 */
34
+ pid?: number;
35
+ /** 引擎监听端口。 */
36
+ port?: number;
37
+ /** 引擎请求重启(center restart 信号);壳侧信息性。 */
38
+ restartRequired?: boolean;
39
+ /** 引擎 build 版本自述(service 1.96+,[392]ask②)。复用时与本地 dist 版本比对,不一致=陈旧代码拒复用。 */
40
+ version?: string;
41
+ }
42
+ /**
43
+ * engine.port 文件落盘的 JSON 形状(write-temp+fsync+mv 原子写)。
44
+ * 壳启动时读此文件做引擎发现:存在且 kill-0 活 + /health 身份匹配 → 复用;否则 reap + spawn。
45
+ * 🔴 secrets 边界:绝不写入任何明文 token/secret(只透传到 child env)。grep engine.port 不得见 secret。
46
+ */
47
+ export interface EnginePortFile {
48
+ /** 引擎监听端口。 */
49
+ port: number;
50
+ /** spawn 出的引擎子进程 pid(kill-0 存活检测 / orphan reap 目标)。 */
51
+ pid: number;
52
+ /** 引擎实例身份(uuidv7);连接前用 /health 复验,不匹配 = 陈旧 port。 */
53
+ instanceId?: string;
54
+ /** 引擎 localDataRoot;身份校验用。 */
55
+ dataRoot?: string;
56
+ /** boot 时记录的 configHash;drift 检测基线。 */
57
+ configHash?: string;
58
+ /** 引擎日志文件路径(<configHome>/engine-<pid>.log),壳可 tail 诊断。 */
59
+ logPath: string;
60
+ /** 落盘时间戳(epoch ms);信息性 / 调试用。 */
61
+ startedAt: number;
62
+ /** 写文件的格式版本(向后兼容);当前 1。 */
63
+ version: number;
64
+ /**
65
+ * P2 云配置整体开关:spawn 该引擎时生效的配置源标签('local' | 'cloud:<profile>')。
66
+ * ADDITIVE 可缺(老文件缺 = 'local')。boot 复用检查:与当前 desiredEngineConfigSource() 不一致
67
+ * → 视同配置漂移 reap+respawn(models 域走 CONFIG_LOCAL_DIR shadow,restart-to-apply)。
68
+ */
69
+ configSource?: string;
70
+ }
71
+ /**
72
+ * 内存中持有的「已托管引擎实例」句柄。lifecycle 全程持有,shutdown 时据此 SIGTERM/SIGKILL。
73
+ */
74
+ export interface EngineInstance {
75
+ /** 监听端口。 */
76
+ port: number;
77
+ /** 子进程 pid。 */
78
+ pid: number;
79
+ /** baseUrl = http://127.0.0.1:<port>,壳设到 SEMA_LIVE_BASEURL。 */
80
+ baseUrl: string;
81
+ /** /health 校验拿到的身份(复用既有引擎时可能来自 probe,而非自己 spawn)。 */
82
+ instanceId?: string;
83
+ /** 引擎 dataRoot。 */
84
+ dataRoot?: string;
85
+ /** boot configHash 基线(heartbeat drift 检测)。 */
86
+ configHash?: string;
87
+ /** 日志文件路径(自己 spawn 的才有;复用既有引擎为 undefined)。 */
88
+ logPath?: string;
89
+ /** true = 本壳 spawn 出来的(shutdown 时负责回收);false = 复用既有引擎(不杀)。 */
90
+ ownedByThisShell: boolean;
91
+ /** node ChildProcess 句柄(仅自己 spawn 时有;复用既有为 undefined)。as unknown 避免类型耦合 child_process。 */
92
+ child?: unknown;
93
+ }
94
+ /**
95
+ * 壳注入给引擎子进程的 TOC env pack(§3.1)。引擎默认偏云(PORT=8090/DB_BACKEND=tidb/
96
+ * CONFIG_PROVIDER=undefined),壳必须显式覆盖为 local 单用户模式,绝不依赖引擎默认。
97
+ * secrets(ANTHROPIC_API_KEY/E2B_API_KEY 等)仅在父 env 存在时透传到 child,不落任何文件。
98
+ */
99
+ export interface TOCConfig {
100
+ /** 选定端口(显式 pin SEMA_ENGINE_PORT 优先;无 pin 按 config dir 派生 + 空闲探测,P0-2)。 */
101
+ port: number;
102
+ /** 引擎数据根 = <configHome>/engine-data;引擎 LOCAL_DATA_ROOT 优先级链最高项。 */
103
+ localDataRoot: string;
104
+ /** 本地 config.d 目录 = <configHome>/config.d(引擎 CONFIG_LOCAL_DIR)。 */
105
+ configLocalDir: string;
106
+ /** 壳配置主目录(getClaudeConfigHomeDir());也作 SEMA_CONFIG_DIR 注入对齐路径。 */
107
+ configHome: string;
108
+ /** per-spawn 随机服务令牌(uuid),SERVICE_AUTH_TOKENS="<uuid>=system:shell-local";不落 .env。 */
109
+ serviceAuthToken: string;
110
+ /** 引擎入口文件绝对路径(ai-agent-service/dist/main.js,缺则回退 tsx src/main.ts)。 */
111
+ engineEntry: string;
112
+ /** 引擎入口的工作目录(ai-agent-service 根)。 */
113
+ engineCwd: string;
114
+ /** 引擎日志输出文件(<configHome>/engine-<pid>.log;pid spawn 后回填,先用占位)。 */
115
+ logPath: string;
116
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * ⇄ B1 批搬迁(2026-07-27,多端改造设计稿 §2.4.1-B):cli src/sema/types/engineState.ts 逐字搬入(纯类型,零运行时;壳侧改 re-export)。
3
+ * ⚠️ 归属注记:`EngineInstance`/`TOCConfig` 描述的是**宿主**职责(spawn/端口/日志路径),逻辑本体
4
+ * (engineLifecycleManager)按设计稿 §2.4.2-A 留 TUI;搬进本包的只是**形状**,让 desktop 宿主与
5
+ * TUI 宿主共用同一套 /health 与 engine.port 契约类型,不代表生命周期逻辑下沉。
6
+ */
7
+ /**
8
+ * TOC 一体化 (TP-A-P0-IMPL Step 5) — Engine Lifecycle 状态类型。
9
+ *
10
+ * 壳一体化起本地引擎(ai-agent-service file 模式),对用户透明,体验等同 CC 单进程。
11
+ * 本文件只定义跨 lifecycle 流转的纯数据形状,不含逻辑(逻辑在 engineLifecycleManager.ts)。
12
+ *
13
+ * 真相源契约:engine.port 文件是壳↔引擎的「当前活引擎」单一发现锚点(flock 串行化写,
14
+ * write-temp+fsync+mv 原子落盘)。HealthResponse 字段与引擎 server.ts:645-660 /health 逐字对齐
15
+ * (W2 同约定实现):instanceId/dataRoot/configHash/pid —— 壳据此判断「连对引擎」防陈旧 port 连错。
16
+ */
17
+ export {};
@@ -0,0 +1,90 @@
1
+ /**
2
+ * ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2):cli src/sema/ultracodeWireCaps.ts 逐字搬入(零依赖;🔴 模块级 ultracodePreset 是 sticky 单实例态,写口 setUltracodePreset 与读口 ultracodeForWire 必须同实例)。
3
+ */
4
+ /**
5
+ * src/sema/ultracodeWireCaps.ts — the shell's TWO opt-in sources for the CC-parity `settings.ultracode` PRESET,
6
+ * projected onto `TaskRequest.settings.ultracode` (a typed boolean the SERVICE — ai-agent-service 1.48.0 — reads
7
+ * and EXPANDS to `thinking:"max"` + `selfOrchestration:true`; both core's existing orthogonal axes, fail-closed
8
+ * gates unchanged). design/111 (core/search AI 2026-06-30): ultracode is a STANDALONE settings boolean folding
9
+ * "max thinking × workflow orchestration", surfaced in sema as the top notch of the `/effort` dial.
10
+ *
11
+ * Two sources, ONE structured-spec output (`settings.ultracode:true` — never prompt text):
12
+ *
13
+ * ① L2 dial preset (STICKY): the `/effort ultracode` pick. The effort dial COERCES the reasoning axis to `xhigh`
14
+ * (overrides/effort.tsx executeEffortSema — "ultracode runs at xhigh effort"), which would LOSE the
15
+ * orchestration axis. So the dial separately latches this module-level preset: ON when `ultracode` is picked,
16
+ * OFF when any OTHER effort level is picked (a sticky session setting, like CC's settings.ultracode boolean).
17
+ *
18
+ * ② L3 keyword (PER-TURN): a client-side scan of the REAL human turn input for the literal word "ultracode".
19
+ * 🔴 RED LINE (design/111 L3, the ONLY injection risk): scan ONLY the human's current-turn submission, BEFORE
20
+ * any untrusted content (tool results / file reads / web / subagent / MCP) is concatenated. NEVER scan the
21
+ * assembled prompt server-side — a malicious file containing "ultracode" would be a privilege-escalation
22
+ * injection. The keyword is a UX convenience switch, NOT a security decision point: privilege STILL passes the
23
+ * service's fail-closed per-principal `allowWorkflows` gate (core deliberately refuses prompt scanning,
24
+ * prepare-task.ts:1612 — that boundary is correct). The output is a STRUCTURED spec (the boolean), never
25
+ * injected prompt text.
26
+ *
27
+ * Both sources are OR'd live-side by `toTaskRequest`; the SAME `SEMA_LIVE_BASEURL` gate the other capability wires
28
+ * use keeps the mock request shape unchanged (the stamp IS the opt-in; absent ⇒ the engine never expands ultracode).
29
+ *
30
+ * The PRESET state is module-level (mirrors seamQuery's seam* injectors): the dial (a React override) latches it,
31
+ * the seam reads it. The KEYWORD scan is a PURE function (caller passes the human input) so it bundles + unit-tests
32
+ * without React/AppState.
33
+ */
34
+ import { type EnvLike } from './hostEnv.js';
35
+ /** Latch the dial preset. `effort.tsx` calls this whenever an effort level is APPLIED: `true` for the `ultracode`
36
+ * notch, `false` for every other level (so switching the dial OFF ultracode clears the workflow-orchestration axis
37
+ * while the reasoning axis follows the picked level). */
38
+ export declare function setUltracodePreset(on: boolean): void;
39
+ /** Read the sticky dial preset (the L2 source). */
40
+ export declare function getUltracodePreset(): boolean;
41
+ /** Test/boot reset — clears the latched preset (e.g. a fresh session). */
42
+ export declare function _resetUltracodePreset(): void;
43
+ /**
44
+ * L3 keyword source: `true` iff the REAL human turn input contains the "ultracode" keyword, else `undefined`.
45
+ * 🔴 The caller MUST pass ONLY the human's current-turn submission (seamQuery's `objective`, derived from the
46
+ * projectable user message) — NOT any assembled/untrusted content. Returns `true | undefined` (never `false`) to
47
+ * mirror the strict additive-stamp discipline of the other wire caps (a stamped `false` would needlessly change
48
+ * the default request shape).
49
+ */
50
+ export declare function ultracodeFromInput(humanInput: string | undefined): true | undefined;
51
+ /**
52
+ * Combine the two sources into the structured spec the seam stamps. `true` when EITHER the sticky dial preset is on
53
+ * OR the human's current-turn input contains the keyword; otherwise `undefined` (⇒ no stamp ⇒ default behaviour).
54
+ * Returns `true | undefined` so the seam can spread `...(ultracode ? { ultracode } : {})` without changing the
55
+ * default request shape. The CALLER gates this to LIVE mode (mock parity).
56
+ */
57
+ export declare function ultracodeForRequest(humanInput: string | undefined): true | undefined;
58
+ export declare function workflowFromInput(humanInput: string | undefined): true | undefined;
59
+ /** Env escape hatch for the deferred-Workflow factory default: `SEMA_WORKFLOW_DEFER=0/false/no/off` restores the
60
+ * pre-[1052] always-exposed shape (schema bytes back in the prefix). Anything else (incl. unset) = defer ON. */
61
+ export declare const WORKFLOW_DEFER_ENV: "SEMA_WORKFLOW_DEFER";
62
+ /**
63
+ * [1052]① 收官形(clay 裁 B 的 core deferred 机制件,cli 接线半场):Workflow「全局默认开但不暴露」——
64
+ * 未命中激活源的 TaskRequest 带 `deferTools:["Workflow"]`(core 1.314 对已挂载工具做 design/36 延迟披露:
65
+ * 占位符上 wire、schema 字节撤出缓存前缀、ToolSearch 可激活=中途激活通路);命中任一激活源 ⇒ `undefined`
66
+ * (不 defer=名字+schema 全量上 wire):
67
+ * a) 用户当轮说 "workflow"(workflowFromInput,只扫真人输入);
68
+ * b) "ultracode" 关键词(L3,同一红线);
69
+ * c) `/effort ultracode` sticky dial(L2 preset)。
70
+ * CALLER 前置两门:live 门(mock 形不变)+ selfOrchestration stamp 存在(opt-out 时 Workflow 根本不挂载,
71
+ * defer 无意义)。前缀缓存注记:defer↔不 defer 的翻转只发生在真人激活当轮,与 CC 209 ToolSearch materialize
72
+ * 同一形——非激活轮的前缀指纹稳定([1044]② clay 硬约束 ii 的解)。
73
+ */
74
+ export declare function workflowDeferForRequest(humanInput: string | undefined, env?: EnvLike): ['Workflow'] | undefined;
75
+ /** [1077]④ 低频工具惰性注入:`SEMA_DEFER_TOOLS=<csv>`(wire 名)追加进 deferTools(core 1.314
76
+ * 延迟披露腿)。**缺省翻转已裁(clay [1212]⑤,2026-07-19)**:九件低频工具缺省 defer——判决数=
77
+ * -2,484 tok/首调用+多轮 gate 零行为退化;defer≠禁用(占位符在,模型 ToolSearch 随取)。
78
+ * 逃生门:`SEMA_DEFER_LOWFREQ=0/false/no/off` 关缺省名单(九件恢复全量上前缀)。 */
79
+ export declare const DEFER_TOOLS_ENV: "SEMA_DEFER_TOOLS";
80
+ export declare const DEFER_LOWFREQ_ENV: "SEMA_DEFER_LOWFREQ";
81
+ /** clay 裁定的缺省惰性名单([1076]④ 评测九件原单,[1090]② 判决数)。 */
82
+ export declare const DEFAULT_LOWFREQ_DEFER: readonly string[];
83
+ /**
84
+ * The FULL deferTools list for a request: the Workflow default-defer lane (activation sources, see
85
+ * {@link workflowDeferForRequest}) + the DEFAULT low-frequency list (clay [1212]⑤, opt-out via
86
+ * SEMA_DEFER_LOWFREQ) + the SEMA_DEFER_TOOLS extra list, deduped. An activation hit removes ONLY
87
+ * Workflow (the user asked for it by name); the other lanes are source-independent. Empty ⇒
88
+ * `undefined` (no stamp, request shape unchanged).
89
+ */
90
+ export declare function deferToolsForRequest(humanInput: string | undefined, env?: EnvLike): string[] | undefined;