@sema-agent/client-core 0.5.0 → 0.7.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 (39) hide show
  1. package/dist/adapt.d.ts +17 -3
  2. package/dist/adapt.js +555 -36
  3. package/dist/clientSlice.d.ts +169 -0
  4. package/dist/clientSlice.js +62 -0
  5. package/dist/diff/patch.d.ts +29 -0
  6. package/dist/diff/patch.js +45 -0
  7. package/dist/engineSessionParam.d.ts +7 -0
  8. package/dist/engineSessionParam.js +35 -0
  9. package/dist/engineWireSdk.d.ts +25 -1
  10. package/dist/engineWireSdk.js +30 -4
  11. package/dist/finalVerifyWire.d.ts +74 -0
  12. package/dist/finalVerifyWire.js +63 -0
  13. package/dist/headlessPermissionModeWire.d.ts +55 -0
  14. package/dist/headlessPermissionModeWire.js +111 -0
  15. package/dist/headlessReconnectWire.d.ts +96 -0
  16. package/dist/headlessReconnectWire.js +141 -0
  17. package/dist/host.d.ts +90 -0
  18. package/dist/host.js +84 -0
  19. package/dist/index.d.ts +36 -1
  20. package/dist/index.js +48 -3
  21. package/dist/interactiveToolsWire.d.ts +48 -0
  22. package/dist/interactiveToolsWire.js +86 -0
  23. package/dist/limitsWire.d.ts +89 -0
  24. package/dist/limitsWire.js +225 -0
  25. package/dist/notifications.d.ts +7 -0
  26. package/dist/notifications.js +32 -0
  27. package/dist/request/taskRequest.d.ts +135 -0
  28. package/dist/request/taskRequest.js +176 -0
  29. package/dist/sandboxWire.d.ts +75 -0
  30. package/dist/sandboxWire.js +138 -0
  31. package/dist/scenarioWire.d.ts +62 -0
  32. package/dist/scenarioWire.js +115 -0
  33. package/dist/scratchpadWireCaps.d.ts +16 -0
  34. package/dist/scratchpadWireCaps.js +65 -0
  35. package/dist/seam.d.ts +114 -11
  36. package/dist/seam.js +31 -0
  37. package/dist/toolResult.d.ts +118 -0
  38. package/dist/toolResult.js +774 -0
  39. package/package.json +4 -2
@@ -0,0 +1,115 @@
1
+ /**
2
+ * src/sema/scenarioWire.ts — `--scenario <name>` wire (clay 拍, 2026-07-15): expose the engine's
3
+ * REQUEST-LEVEL scenario routing axis on the headless `-p` path. The server has shipped scenario
4
+ * routing since day one (ai-agent-service capabilities/scenarios.ts — `TaskRequest.scenario` picks the
5
+ * tool roster + prompt provider per request: default / code-review / scan / oa / team + center-declared
6
+ * overlays); the shell simply never exposed the field to users. This module is the CLI face.
7
+ *
8
+ * PRECEDENCE (flag > settings > default):
9
+ * a) `--scenario <name>` (explicit flag) → stamp `TaskRequest.scenario`.
10
+ * b) `SEMA_HEADLESS_SCENARIO` env — the settings lane: → stamp when no flag. Set it in the user
11
+ * settings.json `env` block (seeded into process.env by the SAME 1a-envseed both the REPL boot and
12
+ * printModeEngine run), the lane every other wire config rides (SEMA_SELF_ORCHESTRATION,
13
+ * SEMA_WEBSEARCH_*, SEMA_ENABLE_FORK). One mechanism serves both "env var" and "settings file".
14
+ * c) neither → NO stamp = today's behaviour, verbatim.
15
+ *
16
+ * VOCABULARY = the server's scenario registry (builtin + center-declared). The shell does NOT
17
+ * pre-validate against it (the registry is deployment-side truth): an UNKNOWN name falls back to the
18
+ * `default` scenario server-side (scenarios.ts selectScenario — safe), and a center allowlist violation
19
+ * comes back as an honest 400 `scenario_not_allowed` (already rendered by scenarioNotAllowedCopy.ts).
20
+ * We only mirror the server's NAME-SHAPE rule (scenarios.ts:318 SCENARIO_NAME_RE) locally — same rule,
21
+ * earlier and cheaper, and it keeps prompt-looking values (whitespace etc.) from being eaten as a name.
22
+ *
23
+ * NOTE (mock-key shadow): the seam also uses `scenario` as the MOCK transcript selector
24
+ * (seamQuery.selectScenario / SEMA_SCENARIO). liveClient strips those mock keys before a live forward;
25
+ * a `--scenario` value that collides with a mock key (e.g. `bash`) is therefore equivalent to `default`
26
+ * on the live engine — which is exactly what the server would resolve for it anyway (no such scenario).
27
+ *
28
+ * SEMA_SCENARIO (the mock harness opt-in — "never spawn a real engine", printModeEngine.ts) is
29
+ * deliberately NOT reused here: it selects offline mock transcripts, not live engine routing.
30
+ */
31
+ import { hostEnv } from './hostEnv.js';
32
+ /** Mirror of the server's scenario NAME shape (ai-agent-service scenarios.ts:318). */
33
+ export const SCENARIO_NAME_RE = /^[a-z][a-z0-9-]{1,31}$/;
34
+ /** Settings-lane env knob (settings.json `env` block → 1a-envseed → process.env). */
35
+ export const HEADLESS_SCENARIO_ENV = 'SEMA_HEADLESS_SCENARIO';
36
+ const USAGE_HINT = "sema: --scenario expects a scenario name (lowercase letters/digits/hyphens, starting with a letter, ≤32 chars).\n" +
37
+ ' --scenario <name> route this run through the named engine scenario (deployment-defined tool\n' +
38
+ ' roster + system prompt; unknown names fall back to the default scenario)\n' +
39
+ ' (no flag) the default scenario — today\'s behaviour';
40
+ /**
41
+ * Parse `--scenario` out of an argv slice (everything before a bare `--`; both `--scenario <v>` and
42
+ * `--scenario=<v>` forms — the sandboxWire idiom). No flag ⇒ `{ok:true}` with no scenario. Repeated
43
+ * flag ⇒ LAST wins (commander convention). Missing value / flag-shaped value / name-shape violation ⇒
44
+ * fail-LOUD parse error (never a silent default — a mis-typed routing choice must not quietly run on
45
+ * the wrong tool surface; same stance as `--sandbox`).
46
+ */
47
+ export function parseScenarioArgv(argv) {
48
+ let scenario;
49
+ for (let i = 0; i < argv.length; i++) {
50
+ const a = argv[i];
51
+ if (a === '--')
52
+ break; // positionals — never flags
53
+ let raw;
54
+ if (a === '--scenario') {
55
+ const nxt = argv[i + 1];
56
+ if (nxt === undefined || nxt.startsWith('-')) {
57
+ return { ok: false, error: USAGE_HINT };
58
+ }
59
+ raw = nxt;
60
+ i++; // consume the value token
61
+ }
62
+ else if (a.startsWith('--scenario=')) {
63
+ raw = a.slice('--scenario='.length);
64
+ }
65
+ else {
66
+ continue;
67
+ }
68
+ const v = raw.trim();
69
+ // sandboxWire audit #17 lesson: a bare `--scenario` EATS the following positional — a value with
70
+ // whitespace is almost certainly the user's prompt; say exactly that instead of mis-parsing.
71
+ if (/\s/.test(v)) {
72
+ return {
73
+ ok: false,
74
+ error: `sema: --scenario got "${v.length > 48 ? `${v.slice(0, 48)}…` : v}" — that looks like your prompt, not a scenario name.\n` +
75
+ 'Put the flag AFTER the value form (--scenario=<name>) or keep the name and prompt as separate tokens.\n' +
76
+ USAGE_HINT,
77
+ };
78
+ }
79
+ if (!SCENARIO_NAME_RE.test(v)) {
80
+ return { ok: false, error: USAGE_HINT };
81
+ }
82
+ scenario = v;
83
+ }
84
+ return { ok: true, ...(scenario ? { scenario } : {}) };
85
+ }
86
+ /**
87
+ * The settings-lane default (`SEMA_HEADLESS_SCENARIO`). FAIL-SOFT on a bad value (stderr warning +
88
+ * no stamp): a settings-file typo must not hard-brick every headless run the way an explicit flag
89
+ * typo fails the one run it was typed on.
90
+ */
91
+ export function headlessScenarioFromEnv(env = hostEnv()) {
92
+ const v = env[HEADLESS_SCENARIO_ENV]?.trim();
93
+ if (!v)
94
+ return undefined;
95
+ if (!SCENARIO_NAME_RE.test(v)) {
96
+ // eslint-disable-next-line no-console
97
+ console.error(`[sema] ${HEADLESS_SCENARIO_ENV}="${v}" is not a valid scenario name (want ${String(SCENARIO_NAME_RE)}) — ignoring the settings default`);
98
+ return undefined;
99
+ }
100
+ return v;
101
+ }
102
+ /**
103
+ * Resolve the scenario for a headless `-p` submit: explicit `--scenario` flag > `SEMA_HEADLESS_SCENARIO`
104
+ * settings default > none (= the engine's default scenario, today's behaviour). Flag parse errors
105
+ * propagate fail-loud; the caller exits.
106
+ */
107
+ export function scenarioForPrint(argv, env = hostEnv()) {
108
+ const parsed = parseScenarioArgv(argv);
109
+ if (!parsed.ok)
110
+ return parsed;
111
+ if (parsed.scenario)
112
+ return parsed;
113
+ const fromEnv = headlessScenarioFromEnv(env);
114
+ return { ok: true, ...(fromEnv ? { scenario: fromEnv } : {}) };
115
+ }
@@ -0,0 +1,16 @@
1
+ export interface ScratchpadDirField {
2
+ scratchpadDir?: string;
3
+ }
4
+ /**
5
+ * Per-turn scratchpadDir stamp。gate off(tengu_scratch)⇒ 空对象(请求形状不变,mock parity)。
6
+ * 路径 per-session 稳定(sessionId 定 getScratchpadDir),重复调用幂等。
7
+ *
8
+ * 与 cli 的差分记账:
9
+ * · `ensureScratchpadDir` 在壳里是 async(mkdir 0o700),这里 **两种返回都吞**(端可给同步实现);
10
+ * 建失败只影响引擎首写时自建(server 半场也建),**不阻 turn** —— 与 cli 逐字同语义。
11
+ * · fs 口缺席(浏览器宿主)⇒ 返回空对象 = 不 stamp。additive 字段缺席对引擎无害,
12
+ * 但会计一次 `hostPortMisses().fs`,让「Node 宿主漏装」与「浏览器本就没有」在自检里可区分。
13
+ */
14
+ export declare function scratchpadDirField(): ScratchpadDirField;
15
+ /** 测试钩:复位 module 级建目录闩。 */
16
+ export declare function _resetScratchpadEnsuredForTest(): void;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * ⇄ B4 批搬迁(2026-07-27,多端改造设计稿 §3 B4 第五件):cli `src/sema/scratchpadWireCaps.ts` 搬入。
3
+ * 拆缝 = 两条外向边换 host port:`utils/permissions/filesystem`(getScratchpadDir /
4
+ * isScratchpadEnabled / ensureScratchpadDir)→ `hostFs()`;`utils/debug.logForDebugging` → `hostLog()`。
5
+ * 本文件是 B2 判给 B4 的两件「宿主耦合 WireCaps」里的**小的那件**,同时充当 `FsPort` 的端到端证明。
6
+ */
7
+ /**
8
+ * scratchpad 域的 wire 半场([816] 壳侧承诺③ / [819]④ / [820]③,2026-07-14)。
9
+ *
10
+ * 链路:core `TaskSpec.envFacts.scratchpadDir` 字段 + 提示词消费链(buildScratchpadSection)1.257.3
11
+ * 起全在;server 半场 = buildEnvFacts 填字段 + 建目录 + 塞进 createFsWriteGatePolicy 的 exemptDirs。
12
+ * 客户端把 getScratchpadDir()(CC 同型:`/tmp/claude-{uid}/{sanitized-cwd}/{sessionId}/scratchpad`)
13
+ * stamp 到 `TaskRequest.scratchpadDir` —— server 半场没到时字段发了引擎忽略 = 无害(additive 铁律);
14
+ * 到货后 server 以客户端带的路径为准(本机 TOC 引擎与壳同 fs,壳造的目录引擎直接可写),或自造(TOB 远端)。
15
+ *
16
+ * 🔴 [1840] 勘误:G3 `scratchpadDir` **不是死键** —— server 已把「死键」的说法收回改接单
17
+ * ([816]③/[820]③ additive 预挂)。别再按「没人读」把这条 stamp 删掉。
18
+ *
19
+ * 🔴 单实例纪律:本模块持 module 级 `ensured` 闩(整进程只 fire-and-forget 建一次目录)。
20
+ * 两份实例 = 建两次(幂等无害),但与本包其余台账同理,壳侧必须只解析到一份。
21
+ */
22
+ import { hostFs, hostLog } from './host.js';
23
+ let ensured = false;
24
+ /**
25
+ * Per-turn scratchpadDir stamp。gate off(tengu_scratch)⇒ 空对象(请求形状不变,mock parity)。
26
+ * 路径 per-session 稳定(sessionId 定 getScratchpadDir),重复调用幂等。
27
+ *
28
+ * 与 cli 的差分记账:
29
+ * · `ensureScratchpadDir` 在壳里是 async(mkdir 0o700),这里 **两种返回都吞**(端可给同步实现);
30
+ * 建失败只影响引擎首写时自建(server 半场也建),**不阻 turn** —— 与 cli 逐字同语义。
31
+ * · fs 口缺席(浏览器宿主)⇒ 返回空对象 = 不 stamp。additive 字段缺席对引擎无害,
32
+ * 但会计一次 `hostPortMisses().fs`,让「Node 宿主漏装」与「浏览器本就没有」在自检里可区分。
33
+ */
34
+ export function scratchpadDirField() {
35
+ try {
36
+ const fs = hostFs();
37
+ if (fs === undefined)
38
+ return {};
39
+ if (!fs.isScratchpadEnabled())
40
+ return {};
41
+ const dir = fs.getScratchpadDir();
42
+ if (!ensured) {
43
+ ensured = true;
44
+ try {
45
+ const r = fs.ensureScratchpadDir();
46
+ if (r !== undefined && typeof r.catch === 'function') {
47
+ void r.catch((e) => {
48
+ hostLog('debug', `scratchpadWireCaps: ensureScratchpadDir failed (non-fatal): ${String(e)}`);
49
+ });
50
+ }
51
+ }
52
+ catch (e) {
53
+ hostLog('debug', `scratchpadWireCaps: ensureScratchpadDir threw (non-fatal): ${String(e)}`);
54
+ }
55
+ }
56
+ return dir !== undefined ? { scratchpadDir: dir } : {};
57
+ }
58
+ catch {
59
+ return {};
60
+ }
61
+ }
62
+ /** 测试钩:复位 module 级建目录闩。 */
63
+ export function _resetScratchpadEnsuredForTest() {
64
+ ensured = false;
65
+ }
package/dist/seam.d.ts CHANGED
@@ -80,7 +80,15 @@ export type ChromeEvent = {
80
80
  kind: 'retry_status';
81
81
  laneProof: LaneProof;
82
82
  status: unknown;
83
- } | {
83
+ }
84
+ /**
85
+ * footer 面板行事件(载荷 = 本包 `EngineAgentPanelEvent`,B1 已搬入)。
86
+ * 宿主消费义务:原样 `publishEngineAgentPanelEvent(event)`。
87
+ * 🔴 **session 常驻台账(`markEnginePanelTaskResident`/`clearEnginePanelTaskResident`)不在宿主义务里**
88
+ * —— B4 起由适配器**整臂自持**(读者 `isEnginePanelTaskResident` 也在本包,写读同包 = R4 纪律)。
89
+ * 宿主再写一遍不会更对,只会两臂打架。
90
+ */
91
+ | {
84
92
  kind: 'panel_task';
85
93
  laneProof: LaneProof;
86
94
  event: unknown;
@@ -90,6 +98,20 @@ export type ChromeEvent = {
90
98
  taskId: string;
91
99
  status: string;
92
100
  outputPath?: string;
101
+ }
102
+ /**
103
+ * B5 新臂(#117a,E 层 Bash 分臂的**执行半场**)—— 模型发起的后台 Bash 回执被认出来了。
104
+ * 判定在库(`detectEngineBgShellReceipt`,纯文案锚定 + `run_in_background` 门);执行留宿主:
105
+ * 宿主消费义务 = ① 把 registration 发给自己的后台 shell 面板 store(壳 =
106
+ * `publishEngineBgShellPanelEvent`)② 登记「句柄 → 宿主 run」映射(壳 = `recordBgParentRun(
107
+ * taskId, getActiveEngineTaskId())`,[1501] A/B 寻址的硬证据)。
108
+ * 缺席 = ctrl+B 面板看不到这条后台命令(不是报错,是这一面在该宿主上哑掉)。
109
+ */
110
+ | {
111
+ kind: 'bgshell_register';
112
+ laneProof: LaneProof;
113
+ taskId: string;
114
+ registration: unknown;
93
115
  } | {
94
116
  kind: 'tasks_expand';
95
117
  laneProof: LaneProof;
@@ -149,12 +171,19 @@ export type ChromeEvent = {
149
171
  }
150
172
  /**
151
173
  * 一条终态 task_notification 抵达(引擎已 server-side steer-inject 过,壳侧只做记账)。
152
- * 宿主消费义务(cli 现役三件,缺一即回归):
153
- * ① drop 壳队列里同 run 的 Path B 条目(workflow_complete/probe feeder),否则 idle
154
- * auto-submit 会把同一完成再喂模型一遍(跨进程双投的壳侧半场);
155
- * ② background_bash:清 resident 台账(面板行终态由本帧落,不由 turn sweep 假结)。
156
- * 跨通道去重(「补发通道不得再注入模型一遍」)由**适配器自持台账**负责,不摊给宿主——
157
- * 见 adapt.ts 的 notifiedRuns / workflow_notification_enqueue 臂的发射门。
174
+ * 宿主消费义务(cli 现役三件里 **B4 起只剩零件**——见下,本臂现在纯属「告知」):
175
+ * ① ~~drop 壳队列里同 run 的 Path B 条目~~ B2 起 `dropQueuedNotificationsForRun` 已在本包,
176
+ * B4 起由适配器自己调(宿主只需在启动时 `installNotificationQueuePort`);
177
+ * ② ~~清 resident 台账~~ B4 起由适配器自己调(`clearEnginePanelTaskResident` 在本包)。
178
+ * 宿主对本臂的义务 = **可选的 UI 提示**(如"后台任务完成"toast)。<br>
179
+ * 🔴 配对纪律([paired-mechanisms-must-share-premise]):这两件从「宿主义务」改成「库自持」是
180
+ * **整臂让位**,不是两边都做——宿主若仍照旧实现,两次 drop/clear 幂等无害,但**记号必须删**,
181
+ * 否则下一棒会以为库没做。
182
+ * 跨通道去重(「补发通道不得再注入模型一遍」)由**库自持**,不摊给宿主 —— B5 起台账回到
183
+ * `notifications.ts` 的 **module 级** `notifiedRunIds` / `cardEnqueuedRunIds`(0.6.0 曾放在适配器
184
+ * 实例上,与清账/记账的另一半不共享前提 = [1857] 那个缺口)。
185
+ * 🔴 宿主**不要**再在本臂上调 `markEngineWorkflowNotified` —— 库已在同一位置调过(幂等,
186
+ * 宿主照调无害,但记号该删:整臂让位,不是两边都做)。
158
187
  * 注:面板行 end 事件本身走 panel_task 臂、SubagentStop 走 subagent_lifecycle 臂,不重复。
159
188
  */
160
189
  | {
@@ -166,10 +195,14 @@ export type ChromeEvent = {
166
195
  }
167
196
  /**
168
197
  * 子代生命周期 hook 触发点(cli fireSubagentStartHook / fireSubagentStopHook)。
169
- * `guard:'if-started'` = 宿主必须先查自己的「已 fire Start」台账,没 fire 过 Start 的
170
- * taskId(workflow runId / 外来 id)绝不 fire Stop。
171
- * 宿主消费义务:fire SubagentStart/SubagentStop hooks(observe-only,fail-soft,永不阻塞流);
172
- * Start 对一个 taskId 一生只许一次(进程级台账去重,frame-lane-matrix #4)。
198
+ * 宿主消费义务:fire SubagentStart/SubagentStop hooks(observe-only,fail-soft,永不阻塞流)。
199
+ *
200
+ * 🔴 **B4 起「一生只许一次」的台账由适配器自持**(实例级 = 一个 session 一份,cli 的 module 级
201
+ * 等价物):`phase:'start'` 对同一 taskId **本臂只会发一次**,宿主收到就 fire,不必再去重。
202
+ * `guard:'if-started'` 同理已由适配器解掉——**只有真 fire 过 Start 的 taskId 才会收到带 guard 的
203
+ * stop**(cli `task_notification` 臂 `firedSubagentStartHookTaskIds.has()` 的等价面)。guard 字段
204
+ * 保留只为让宿主能识别这条 stop 的来源臂,**不是**要宿主再判一次(判据锚在能决定结果的量上:
205
+ * 台账在库里,宿主手上根本没有那个量)。
173
206
  */
174
207
  | {
175
208
  kind: 'subagent_lifecycle';
@@ -179,6 +212,44 @@ export type ChromeEvent = {
179
212
  agentType?: string;
180
213
  guard?: 'if-started';
181
214
  }
215
+ /**
216
+ * B4 新臂 ①(A 层 `task_progress` 搬入)—— **inline 群组行**的累计统计(cli
217
+ * `engineInlineTaskStats` 的三个写口:`publishEngineInlineTaskTick` / `settleEngineInlineTaskStats`
218
+ * / `settleAllEngineInlineTaskStats`)。转录里那条 "Running N agents… ├ … 12 tool uses" 的数字面。
219
+ *
220
+ * 为什么与 `panel_task` 分臂:两者键不同 —— 本臂按**委派卡 id**(`cardId` = tool_use id,渲染面
221
+ * 的 AgentProgressLine 只有卡 id 在手),`panel_task` 按 **taskId**(footer 面板行)。同一 tick
222
+ * 同时喂两面是 cli 现役行为,合臂会逼消费端反解键。
223
+ *
224
+ * 宿主消费义务(三个 op 逐一,缺一即回归):
225
+ * · `op:'tick'` → `publishEngineInlineTaskTick(cardId, stats)`(已 settle 的卡是 no-op);
226
+ * · `op:'settle'` → `settleEngineInlineTaskStats(cardId, isError)`(冻结终值,幂等);
227
+ * · `op:'settle_all'` → `settleAllEngineInlineTaskStats()`(turn 末/abort 的防御 sweep 孪生腿)。
228
+ * 三个函数本包已导出(B1 搬入),宿主直接转发即可,不必自己实现语义。
229
+ */
230
+ | {
231
+ kind: 'inline_task_stats';
232
+ laneProof: LaneProof;
233
+ op: 'tick';
234
+ /** 委派 tool_use id(渲染面订阅键)。 */
235
+ cardId: string;
236
+ stats: {
237
+ toolUses: number;
238
+ totalTokens: number;
239
+ durationMs: number;
240
+ currentAction?: string;
241
+ };
242
+ } | {
243
+ kind: 'inline_task_stats';
244
+ laneProof: LaneProof;
245
+ op: 'settle';
246
+ cardId: string;
247
+ isError: boolean;
248
+ } | {
249
+ kind: 'inline_task_stats';
250
+ laneProof: LaneProof;
251
+ op: 'settle_all';
252
+ }
182
253
  /**
183
254
  * 带外后台 workflow 完成推送(service 1.75 Path B:引擎**没有** server-side 注入,壳负责喂模型)。
184
255
  * 宿主消费义务:把 `message`(模型面 `<task-notification>` 正文,适配器已按 core 同形铸好)
@@ -207,6 +278,20 @@ export type ChromeEvent = {
207
278
  toolUseId: string;
208
279
  input: unknown;
209
280
  }
281
+ /**
282
+ * B5 —— 同一台账的**第二个来源**:core 1.217([379])的 `task` / `task-list` structured 权威全量态。
283
+ * `source:'tool_use'` 那条是旧引擎兜底(从 input 重建),本条是引擎真实 id 的精确同步。
284
+ * 宿主消费义务:壳 = `handleEngineTaskStructured(structured)`(两路经 engineToShellTaskId 收敛)。
285
+ * 🔴 两条**不是**二选一 —— 旧引擎只有前者、新引擎两者都来且后者更准,宿主按幂等处理即可。
286
+ */
287
+ | {
288
+ kind: 'task_ledger_sync';
289
+ laneProof: LaneProof;
290
+ source: 'structured';
291
+ /** `'task' | 'task-list'`(白名单已在库内判过)。 */
292
+ structuredType: string;
293
+ structured: unknown;
294
+ }
210
295
  /**
211
296
  * B3 新臂 ①(T38 三清的第三清 + T57 五处断闸的第五处)—— E12「下一步可问什么」建议**批**。
212
297
  * `suggestions: null` = 作废(turn 开场必发一次:上一轮的建议对新上下文是噪声,挂着不掉会让人
@@ -246,6 +331,24 @@ export type ChromeEvent = {
246
331
  laneProof: LaneProof;
247
332
  result: unknown;
248
333
  };
334
+ /** `ChromeEvent` 的判别键(臂名)—— 覆盖率断言与宿主装配自检都锚在它上。 */
335
+ export type ChromeArmKind = ChromeEvent['kind'];
336
+ /**
337
+ * chrome 臂**覆盖率断言出口**(§8-1 裁决的一半:client-core 定接口不定实现)。
338
+ *
339
+ * 用途:任何宿主(TUI / web / 桌面)在自检里遍历本表,对 `required:true` 的臂断言"我实现了消费口"。
340
+ * 不实现 = 该 UI 面在那个宿主上是**哑的**(不是报错,是静默没有)—— 这正是多端最容易漏的一类,
341
+ * 所以把义务做成**数据**而不是散落在注释里。
342
+ *
343
+ * `required` 判据:**不实现会让已发生的行为丢失**(转录/模型输入/hook/去重)⇒ true;
344
+ * 只影响可选装饰(spinner 计量、活体预览、chips)⇒ false。
345
+ * `duty` = 一句话义务(完整义务在上方每臂的注释里,那里是真源)。
346
+ */
347
+ export declare const CHROME_ARMS: readonly {
348
+ kind: ChromeArmKind;
349
+ required: boolean;
350
+ duty: string;
351
+ }[];
249
352
  /** 车道纪律一等公民([1617] 家族固化):chrome 事件构造必须携判别证明。 */
250
353
  export type LaneProof = {
251
354
  lane: 'main';
package/dist/seam.js CHANGED
@@ -1,3 +1,34 @@
1
+ /**
2
+ * chrome 臂**覆盖率断言出口**(§8-1 裁决的一半:client-core 定接口不定实现)。
3
+ *
4
+ * 用途:任何宿主(TUI / web / 桌面)在自检里遍历本表,对 `required:true` 的臂断言"我实现了消费口"。
5
+ * 不实现 = 该 UI 面在那个宿主上是**哑的**(不是报错,是静默没有)—— 这正是多端最容易漏的一类,
6
+ * 所以把义务做成**数据**而不是散落在注释里。
7
+ *
8
+ * `required` 判据:**不实现会让已发生的行为丢失**(转录/模型输入/hook/去重)⇒ true;
9
+ * 只影响可选装饰(spinner 计量、活体预览、chips)⇒ false。
10
+ * `duty` = 一句话义务(完整义务在上方每臂的注释里,那里是真源)。
11
+ */
12
+ export const CHROME_ARMS = [
13
+ { kind: 'retry_status', required: false, duty: '渲/清 spinner 的重试覆盖层' },
14
+ { kind: 'panel_task', required: true, duty: 'publishEngineAgentPanelEvent(event) 原样转发' },
15
+ { kind: 'bgshell_settle', required: true, duty: '后台 shell 面板行落终态(含 outputPath)' },
16
+ { kind: 'bgshell_register', required: true, duty: '后台 shell 面板行建行 + 登记「句柄→宿主 run」' },
17
+ { kind: 'tasks_expand', required: false, duty: '展开 ctrl+t 任务面板' },
18
+ { kind: 'thinking_activity', required: false, duty: '开/收活体 "∴ Thinking…" 行' },
19
+ { kind: 'request_start', required: false, duty: 'spinner 置 requesting 态' },
20
+ { kind: 'stream_delta', required: false, duty: '渲活体预览 + 推进 responseLength' },
21
+ { kind: 'response_metrics', required: false, duty: '驱动 responseLength reducer(ttft/对账)' },
22
+ { kind: 'attachment', required: false, duty: '渲 attachment 行(必须带 default 臂)' },
23
+ { kind: 'notification_terminal', required: false, duty: '可选 UI 提示(记账已由库自持)' },
24
+ { kind: 'subagent_lifecycle', required: true, duty: 'fire SubagentStart/Stop hooks(去重已由库解)' },
25
+ { kind: 'workflow_notification_enqueue', required: true, duty: 'message 入完成通知队列,idle 时喂模型' },
26
+ { kind: 'task_ledger_sync', required: true, duty: '按 toolUseId 幂等落 TaskCreate/TaskUpdate' },
27
+ { kind: 'inline_task_stats', required: true, duty: 'tick/settle/settle_all 转发给 engineInlineTaskStats' },
28
+ { kind: 'prompt_suggestions', required: false, duty: '推 composer 上方 chips(绝不回喂模型)' },
29
+ { kind: 'last_turn_usage', required: true, duty: '落最近一次 turn 真 usage(statusline 回落源)' },
30
+ { kind: 'plan_review_park', required: true, duty: '弹 plan 审批卡(fail-soft,不挡后续终态帧)' },
31
+ ];
1
32
  /**
2
33
  * transcript id 确定性派生([1653] 定案):稳定键优先序 = 帧 id > seq > toolCallId;
3
34
  * 全缺才用 ctx.uuid()。同流重放 ⇒ 同 id 序列(往返守卫钉此不变量)。
@@ -0,0 +1,118 @@
1
+ /**
2
+ * 引擎会在 `tool_end.structured` 顶层 `type` 上发的**全部**取值。
3
+ * 用途**不是**「只处理这些」——下面的 switch 只认得其中一部分,认不得的照旧走 text 回落;
4
+ * 用途是「**structured 在场**」这个判据本身:顶层 type 命中白名单 ⇒ 引擎这一版是发结构化的,
5
+ * 于是模型面正文的**正则反解**(T11 bash / T13+T24 task-output / T14 后台回执三代文案)退位。
6
+ * 反过来,一个 `{type:'something-else'}` 或裸对象**不算** structured 在场 —— 宁可回落到正则,
7
+ * 也不许把「有个对象」当成「引擎发了结构化」(判据锚在决定结果的量上:决定的是「该不该信正则」)。
8
+ * ⚠️ 与设计稿的口径差:任务书写「29 项」,[1840] 清单逐条数是 **30 项**(见交接报告「设计稿错漏」)。
9
+ */
10
+ export declare const STRUCTURED_DETAIL_TYPES: ReadonlySet<string>;
11
+ /** structured 在场判别:顶层 `type` ∈ 白名单 ⇒ 返回该 type,否则 undefined(= 不在场)。 */
12
+ export declare function structuredDetailType(structured: unknown): string | undefined;
13
+ export declare function modelFacingParseCalls(): number;
14
+ export declare function _resetModelFacingParseCountForTest(): void;
15
+ export declare function parseModelFacingBash(text: string): {
16
+ stdout: string;
17
+ stderr: string;
18
+ exitCode: number | null;
19
+ } | null;
20
+ /**
21
+ * T12 —— 剥引擎的 `delimitUntrusted` 围栏(core untrusted-text.ts:118)**仅供渲染**。
22
+ * 围栏是给模型看的注入防护,人类转录卡显示正文(clay 07-03 审计 #1/#9 视觉 bug 的修)。
23
+ */
24
+ export declare function stripUntrustedFence(text: string): {
25
+ label: string;
26
+ body: string;
27
+ } | null;
28
+ /**
29
+ * T13 回落 —— 引擎 TaskOutput 的模型面正文(core task-registry.ts 的 bash/agent 两形,整体
30
+ * delimitUntrusted 包裹、label = `TaskOutput <id>`)反解回卡片槽位。`kind` 报哪一形命中,
31
+ * 'unknown' ⇒ 调用方回落原文。
32
+ */
33
+ export declare function parseModelFacingTaskOutput(text: string): {
34
+ kind: 'bash' | 'agent' | 'unknown';
35
+ taskId?: string;
36
+ status?: string;
37
+ output?: string;
38
+ };
39
+ /**
40
+ * T14 回落 —— 后台回执**三代文案**正则(#117a:core ≥1.283 的 `Command running in background` 是
41
+ * 第三代)。structured bash 在场时由 `background`/`task_id` 位取代(见 `wireOutputToBody`)。
42
+ */
43
+ export declare function parseBackgroundReceipt(text: string): {
44
+ taskId: string;
45
+ } | null;
46
+ /**
47
+ * T8 —— 压平非均匀 §E1 `output`(`string` | `(Text|Image)[]`)。
48
+ * ⚠️ 真源在 adapt.ts(A 层 settle 报告也用同一份),本文件只 import,别再写第二份。
49
+ */
50
+ export type FlattenWireOutput = (output: unknown) => string;
51
+ /** wire 没带 output 时的降级原因码(UI 显示决策留端;客户端绝不编造执行结果)。 */
52
+ export declare const TOOL_RESULT_DEGRADED_WIRE_NO_OUTPUT: "wire_carried_no_output";
53
+ export interface ToolResultBody {
54
+ content: unknown;
55
+ isError: boolean;
56
+ toolUseResult?: unknown;
57
+ /** 只在「wire 没带正文」时出现;在场 = 这张卡**没有真实执行结果**,不是空结果。 */
58
+ degraded?: typeof TOOL_RESULT_DEGRADED_WIRE_NO_OUTPUT;
59
+ }
60
+ /**
61
+ * T7 —— `tool_end.output === undefined`(events.d.ts:79 文档化的「工具没有正文」情形)时的卡体。
62
+ *
63
+ * cli 旧形:调 mock 注册表 `synthesizeToolIO(name, input)` **反造**一个结果体(stdout/hunks/匹配列表),
64
+ * 卡片于是显示「看起来像真的、但根本不是这次执行产物」的内容。§8-4 按宪法「诚实优先于产出」裁定
65
+ * 搬的时候就改掉:**输出诚实缺席 + degraded 标记**,由 UI 决定显示「结果不可用」。
66
+ * 🔴 `content` 保持**字符串**(件1:tool_result block content 恒字符串,裸对象经 /compact 直发
67
+ * provider = 400 invalid_request);空串 = 无正文,`degraded` 才是判别位。
68
+ */
69
+ export declare function degradedToolResultBody(isError: boolean): ToolResultBody;
70
+ /**
71
+ * 真实 §E1 `tool_end.output` → 卡片读的 typed 正文。
72
+ *
73
+ * B5 新增第 5 个入参 `structured`:**structured 在场 ⇒ 正则退位**(①)。
74
+ * 三臂:`bash`(T11/T14)· `taskoutput|task_output`(T13,无 structured 的老腿)· 泛化兜底。
75
+ * 🔴 `truncated` 只做中性提示,截断正文**绝不**回喂模型(events.d.ts:80-85)。
76
+ */
77
+ export declare function wireOutputToBody(toolName: string, output: unknown, isError: boolean, truncated: boolean, flatten: FlattenWireOutput, structured?: unknown): ToolResultBody;
78
+ /**
79
+ * design/64 CC-parity `toolUseResult`:引擎结构化细节 → 各卡 outputSchema **精确** typed 形
80
+ * (safeParse 过了才渲富卡,不过就回落 12 行文本兜底)。
81
+ * `structuredPatch` 在**这里**算(T20,core 裁定:引擎发 originalFile、客户端算 hunks)。
82
+ * 全部**防御读**:形状不认得 ⇒ null ⇒ 调用方回落 text 路径(降保真,绝不炸)。
83
+ */
84
+ export declare function structuredToToolUseResult(structured: unknown,
85
+ /** 压平后的模型面正文 —— 有些形(task-output)只带 identity/status,正文仍在模型面。 */
86
+ modelText?: string): {
87
+ toolUseResult: unknown;
88
+ isError?: boolean;
89
+ } | null;
90
+ export interface EngineBgShellRegistration {
91
+ kind: 'register';
92
+ /** 回执上的**引擎** task id(`task_id=…`,TaskOutput/TaskStop 的句柄)。 */
93
+ taskId: string;
94
+ /** Bash tool_use input 的 command(面板行的命令行)。 */
95
+ command: string;
96
+ /** 模型给了 description 才有。 */
97
+ description?: string;
98
+ /** 引擎模型面回执原文,逐字 —— 详情视图显示它(里面的 output-file 路径因此可见可复制)。 */
99
+ receipt: string;
100
+ }
101
+ export declare function detectEngineBgShellReceipt(rawInput: unknown, outputText: string | undefined): EngineBgShellRegistration | null;
102
+ /**
103
+ * TodoWrite 清单卡:引擎不发 todo structured,全量新表骑在 tool_use INPUT 上 ⇒ 从它合成 typed 结果。
104
+ * `oldTodos` = 上一次的活体清单(调用方持:一个 session 一份,正是划删线 diff 要的粒度)。
105
+ */
106
+ export declare function todoWriteToolUseResult(rawInput: unknown, oldTodos: unknown[], isError: boolean): {
107
+ toolUseResult: unknown;
108
+ isError: boolean;
109
+ newTodos: unknown[];
110
+ } | null;
111
+ /**
112
+ * ReportFindings(core 1.288 [811]b,引擎侧执行的合成回声工具):details 是 `{count, level?, findings}`
113
+ * **没有 type 判别位**(core synthetic-tools.js)⇒ B 层的 type-keyed switch 认领不了,按**工具名**认。
114
+ * 双腿:优先 wire structured;引擎没发就从 tool_use INPUT 合成(回声工具:input.findings ≡ output.findings)。
115
+ */
116
+ export declare function reportFindingsToolUseResult(structured: unknown, rawInput: unknown): {
117
+ toolUseResult: unknown;
118
+ } | null;