@sema-agent/client-core 0.29.0 → 0.30.1

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 (44) hide show
  1. package/CHANGELOG.md +455 -0
  2. package/README.md +19 -3
  3. package/dist/adapt/arms.js +24 -1
  4. package/dist/adapt/wireShapes.d.ts +7 -0
  5. package/dist/adapt/wireShapes.js +7 -0
  6. package/dist/adapter/activeRunSelfHeal.d.ts +285 -48
  7. package/dist/adapter/activeRunSelfHeal.js +553 -19
  8. package/dist/adapter/runStream.js +13 -3
  9. package/dist/engineWireSdk.d.ts +10 -2
  10. package/dist/engineWireSdk.js +7 -3
  11. package/dist/hitl/approvalDecisionNoteAudit.d.ts +58 -0
  12. package/dist/hitl/approvalDecisionNoteAudit.js +91 -0
  13. package/dist/hitl/armedGateRegistry.d.ts +62 -4
  14. package/dist/hitl/armedGateRegistry.js +226 -14
  15. package/dist/hitl/askParkRowRouting.d.ts +150 -0
  16. package/dist/hitl/askParkRowRouting.js +183 -0
  17. package/dist/hitl/gateIdentity.d.ts +8 -0
  18. package/dist/hitl/gateIdentity.js +8 -0
  19. package/dist/hitl/hitlBridge.d.ts +7 -0
  20. package/dist/hitl/hitlBridge.js +11 -2
  21. package/dist/hitl/parkOwnership.d.ts +2 -1
  22. package/dist/hitl/parkOwnership.js +11 -3
  23. package/dist/hitl/parkRowBirthWait.d.ts +63 -0
  24. package/dist/hitl/parkRowBirthWait.js +192 -0
  25. package/dist/hitl/planReviewWire.d.ts +31 -1
  26. package/dist/hitl/planReviewWire.js +69 -30
  27. package/dist/hitl/resumeRunningCard.d.ts +134 -0
  28. package/dist/hitl/resumeRunningCard.js +177 -0
  29. package/dist/hitl/toolApprovalWire.d.ts +49 -9
  30. package/dist/hitl/toolApprovalWire.js +9 -0
  31. package/dist/index.d.ts +5 -0
  32. package/dist/index.js +14 -0
  33. package/dist/seatContract.d.ts +27 -0
  34. package/dist/seatContract.js +42 -0
  35. package/dist/subagent/engineSubagentTail.d.ts +0 -2
  36. package/dist/subagent/engineSubagentTail.js +7 -15
  37. package/dist/subagentContentStore.d.ts +58 -2
  38. package/dist/subagentContentStore.js +95 -6
  39. package/dist/toolResult.d.ts +26 -0
  40. package/dist/toolResult.js +38 -6
  41. package/dist/workflowClient.d.ts +6 -1
  42. package/docs/INTEGRATION-CLIENTS.md +844 -0
  43. package/docs/REFACTOR-LEDGER.md +392 -0
  44. package/package.json +7 -4
@@ -10,7 +10,10 @@
10
10
  *
11
11
  * WIRE FACTS: with `forwardSubagentEvents: true` on the task request, the live stream carries the
12
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)
13
+ * (§E1 redact upstream; §E2 identity). 🔴 EventIdentity 的键是 `eventId` / `parentToolCallId`
14
+ * (+ LIVE 白名单四臂上的 `sourceTaskId` / `bgAgentId`)—— **没有 `taskId`**(sdk `events.d.ts` 的
15
+ * `interface EventIdentity` 直证;`taskId` 只长在 `meta` 首帧上)。所以内容帧到不了「自带引擎
16
+ * taskId」那一步,归账键靠 `parentToTask`,见 `canonicalKey`。The seam adapter (runStream.ts)
14
17
  * DIVERTS those events here — they must never reach the main-transcript pipeline (a subagent's tokens
15
18
  * rendering as the leader's text is the pollution failure mode).
16
19
  *
@@ -25,6 +28,64 @@
25
28
  * NOTIFY: coalesced (~250ms) per-task notify so a per-token delta storm never turns into per-token
26
29
  * AppState writes; consumers pull a snapshot on notify.
27
30
  */
31
+ /**
32
+ * `tool_end.output` → 本 store 的 `output` 位(**唯一**字符串化口)。
33
+ *
34
+ * 为什么住在这里(#158 移交① / [3674](d) 姊妹病,2026-08-12 搬入):这个函数原本长在
35
+ * `subagent/engineSubagentTail.ts`(tail 腿)里。而喂本 store 的是**两条**腿 —— tail 腿与
36
+ * seam 适配器的 C1 分流臂(`adapter/runStream.ts`)—— C1 那条当时自己写了
37
+ * `typeof sub.output === 'string' ? sub.output : undefined`,于是同一个 wire 形经不同腿进同一个
38
+ * 账本得到两种结果(块数组走 C1 恒空)。字符串化口跟着**它服务的那个位**走,两条腿一律 import
39
+ * 这一份:第二份实现在类型面就没有立足点了。
40
+ * 🔴 住这里的另一个硬理由:tail 腿的传递闭包里有 SDK/host(engineWireSdk 等),而 C1 所在的
41
+ * `runStream.ts` 有一道**可移植性闭包门**(零 Node 内建 / 零 react / 文件数棘轮 ≤7)——
42
+ * 从 tail 腿 import 会当场把整条 wire 依赖拖进内核。本 store 已在那 7 件之内且零 import,
43
+ * 所以搬到这里的代价是 0 个新闭包文件。
44
+ *
45
+ * 语义:`undefined`/`null` ⇒ `undefined`(诚实缺席,不铸空串);`string` ⇒ 原样直通;
46
+ * **§E1 的 `(TextContent|ImageContent)[]` 块数组 ⇒ 规范展平**(text 块贡献 `.text`、image 块贡献
47
+ * 中性占位 `[image]`);其余(server `redactDeep` 之后原型不保的对象形等)⇒ `JSON.stringify`,
48
+ * 序列化失败退 `String(v)`。
49
+ *
50
+ * 🔴 块数组这一支为什么不是 `JSON.stringify`(0.30.0 发包扫描修,2026-08-14):`output` 是**展示位**
51
+ * (投进 {@link planSubagentViewSlots} 的结果栏),而同一个 wire 形在主 lane 走的是
52
+ * `adapt/wireShapes.ts` 的 `flattenWireOutput`(工具卡体 / print 帧 / 面板 settle 三处)。修前这里
53
+ * 出的是 `[{"type":"text","text":"…"}]` 这种机器形 —— 同一份素材主 lane 渲人话、子代查看态渲转义
54
+ * JSON;带 image 块时整段 base64(events.d.ts 的 `totalChars` 注释逐字:image/document = base64 真实
55
+ * 体量)会被原样灌进内容账本并上屏(item 数有 200 上限、**单 item 体量无上限**)。
56
+ * 🔴 为什么这里还有第二份展平实现、而不是 import 那一份:本 store 是 `runStream.ts` 内核可移植闭包
57
+ * (`run-client-core-portability-test.mjs` 的 `MAX_CLOSURE_FILES`,只许降)里的一件,且本文件头注
58
+ * 三处以「dep-free / 零 import」为立身之本;从 `adapt/wireShapes.ts` 拉一条边会同时破这两条。
59
+ * ⇒ 两份实现由 **pure 门 ⑨b 段的等值断言**逐形对拍钉住(字符串 / 纯 text 块 / 含 image 块 / 混合 /
60
+ * 空数组 / 未知块形六形,`coerceOutput(x) === flattenWireOutput(x)` + 逐字面值),任一侧改语义当场红。
61
+ * 动本函数的块数组分支 ⇒ 同批看那条断言。
62
+ */
63
+ export function coerceOutput(v) {
64
+ if (v === undefined || v === null)
65
+ return undefined;
66
+ if (typeof v === 'string')
67
+ return v;
68
+ // §E1 块数组 ⇒ 规范展平(与 adapt/wireShapes.ts 的 flattenWireOutput 逐字同语义;等值由 pure ⑨b 钉)
69
+ if (Array.isArray(v)) {
70
+ const parts = [];
71
+ for (const block of v) {
72
+ if (block && typeof block === 'object') {
73
+ const b = block;
74
+ if (b.type === 'text' && typeof b.text === 'string')
75
+ parts.push(b.text);
76
+ else if (b.type === 'image')
77
+ parts.push('[image]');
78
+ }
79
+ }
80
+ return parts.join('');
81
+ }
82
+ try {
83
+ return JSON.stringify(v);
84
+ }
85
+ catch {
86
+ return String(v);
87
+ }
88
+ }
28
89
  const MAX_ITEMS_PER_TASK = 200;
29
90
  const MAX_TASKS = 32;
30
91
  const NOTIFY_COALESCE_MS = 250;
@@ -105,13 +166,31 @@ function scheduleNotify(taskId) {
105
166
  }
106
167
  }, NOTIFY_COALESCE_MS);
107
168
  }
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) {
169
+ /**
170
+ * **内容归账那一半**(`parentToolCallId taskId`,多对一):把停在 parent 键下的内容迁到 canonical
171
+ * task 键,并让后续内容帧的 `canonicalKey` 解析到 task。**不写** `taskToParent`。
172
+ *
173
+ * 为什么要与 {@link registerSubagentAlias} 分开(#158② 复审收货,2026-08-12):这张表其实承载
174
+ * **两件不同的事**,方向与基数都不同 ——
175
+ * · `parentToTask`:内容归账,**多对一**(同一个子代可能被多次调用宣告:SendMessage 复活会为
176
+ * 同一 taskId 再发一条 `async_launched` 回执,骑的是复活那次调用的 toolCallId);旧键不删,
177
+ * 所以两次宣告的内容都归到同一个 taskId,加一条只会更全。
178
+ * · `taskToParent`:**单值**,而它的读口 {@link parentToolCallIdOf} 是**对外**的 C2 steer 投递
179
+ * 目标(service 按「委派工具调用的 id」路由子代 steer)。这一位的**权威来源是 wire 的内容帧**
180
+ * (`publishSubagentContentEvent` 记在内容态上的 `parentToolCallId`,读口对它做兜底),
181
+ * 一条启动回执**不足以**改写它:复活回执带的是复活调用,拿它盖掉真正的委派调用 = 用户插话
182
+ * 投到错的目标,而「复活后该按哪个 target 投」要 server 语义作证,本包无从判定。
183
+ * ⇒ 回执类喂点只用本函数(只加内容归账,零 steer 面影响);`task_progress` tick 那条喂点两键都带、
184
+ * 且是引擎对「这条 tick 属谁」的当期陈述,继续用 {@link registerSubagentAlias}。
185
+ */
186
+ export function registerSubagentContentAlias(parentToolCallId, taskId) {
187
+ aliasContentKey(parentToolCallId, taskId);
188
+ }
189
+ /** 两个登记口共用的内容归账实现(单源)。返回 false = 退化对被拒(自指/空值),调用方别再往下写。 */
190
+ function aliasContentKey(parentToolCallId, taskId) {
111
191
  if (!parentToolCallId || !taskId || parentToolCallId === taskId)
112
- return;
192
+ return false;
113
193
  parentToTask.set(parentToolCallId, taskId);
114
- taskToParent.set(taskId, parentToolCallId);
115
194
  // a notify queued under the parent key before the alias landed must fire under the canonical key
116
195
  if (pendingNotify.delete(parentToolCallId))
117
196
  pendingNotify.add(taskId);
@@ -121,6 +200,16 @@ export function registerSubagentAlias(parentToolCallId, taskId) {
121
200
  tasks.set(taskId, parked);
122
201
  scheduleNotify(taskId);
123
202
  }
203
+ return true;
204
+ }
205
+ /** Register the parentToolCallId ↔ engine-taskId pair (from task_progress, which carries both).
206
+ * Content parked under the parent key migrates to the canonical task key.
207
+ * ⚠️ 这一口**同时**写单值的 `taskToParent`(= 对外 C2 steer 目标,见
208
+ * {@link registerSubagentContentAlias} 头注的两件事分家说明)。只想补内容归账的喂点用那一口。 */
209
+ export function registerSubagentAlias(parentToolCallId, taskId) {
210
+ if (!aliasContentKey(parentToolCallId, taskId))
211
+ return;
212
+ taskToParent.set(taskId, parentToolCallId);
124
213
  }
125
214
  function canonicalKey(ev) {
126
215
  // the wire's content events carry no taskId — runStream passes parentToolCallId in both slots
@@ -211,6 +211,32 @@ export declare function todoWriteToolUseResult(rawInput: unknown, oldTodos: unkn
211
211
  export declare function reportFindingsToolUseResult(structured: unknown, rawInput: unknown): {
212
212
  toolUseResult: unknown;
213
213
  } | null;
214
+ /**
215
+ * 后台(async)子代的**启动回执**上那两位「身份」—— `type:"agent"` 两形里 `async_launched` 那一形。
216
+ *
217
+ * 只带**身份**,不带可渲染性:`description`/`prompt` 是富卡的必填位(缺了要回落 text 路径),
218
+ * 与「这条回执宣告了哪个子代」无关,故不进本读口 —— 见 {@link structuredToToolUseResult} 的
219
+ * `case 'agent'`。
220
+ */
221
+ export interface AsyncLaunchedAgentReceipt {
222
+ /** 引擎子代任务 id(注意:**启动回执是 snake_case `task_id`**,完成卡是 camelCase `taskId`)。 */
223
+ readonly taskId: string;
224
+ /** bg 子代生命周期号(SendMessage 复活即 +1)。wire 今天未必带 ⇒ 缺席按首周期解释,绝不铸 0。 */
225
+ readonly seq?: number;
226
+ }
227
+ /**
228
+ * 把一张 `tool_end.structured` 窄化成 {@link AsyncLaunchedAgentReceipt} —— **仅当**它是
229
+ * `type:"agent"` 的 **`async_launched` 启动回执**臂时;完成着陆报告 / 别的族 / 缺身份 ⇒ `null`。
230
+ *
231
+ * 🔴 分臂判据 = `status`(core 原话:"Branch on `status`, never on which keys are present"),
232
+ * 与姊妹读口 {@link readCompletedAgentCard} 互为反面。本函数是这条纪律在**回执**侧的单一实现点:
233
+ * 本包现在有两个消费点 —— 富卡投影(`structuredToToolUseResult`)与 adapt 的 bg 子代 alias 登记
234
+ * (`adapt/arms.ts` 的 `tool_end_result` 臂,#158②)—— 两处不得各抄一遍键名与 `'async_launched'`。
235
+ * 🔴 `task_id` 要**非空**串:空串是「有这个键但没有身份」,拿它当 taskId 会在内容账本里铸一条
236
+ * 键为 `''` 的映射(读口那侧的 `?? ''` 兜底会当场把它认成命中)。诚实拒,不铸空 id ——
237
+ * 与 {@link readCompletedAgentCard} 对 `taskId` 的同款判据一致。
238
+ */
239
+ export declare function readAsyncLaunchedAgentReceipt(structured: unknown): AsyncLaunchedAgentReceipt | null;
214
240
  /**
215
241
  * 子代**完成着陆报告**卡(core `agents/subagent.ts#completedAgentCard` 的 wire 形)。
216
242
  *
@@ -732,17 +732,21 @@ modelText) {
732
732
  // 按宪法三问单独走。返回 null ⇒ 回落模型面 text 路径,与 5.10.0 之前逐字同形,零回归。
733
733
  // ⚠️ 完成卡本身**没有被吞**:`eventToSdkMessage` 的 `tool_end_result` 臂把 `structured`
734
734
  // 原样透传给宿主,typed 读口见本文件的 {@link readCompletedAgentCard}。
735
- if (s.status !== 'async_launched')
735
+ // 分臂 + 身份两位走 typed 读口 {@link readAsyncLaunchedAgentReceipt}(#158② 起本包有第二个
736
+ // 消费点 —— adapt 的 alias 登记 —— 判据必须只有一份;`status`/`task_id` 的读法不再散点)。
737
+ const receipt = readAsyncLaunchedAgentReceipt(s);
738
+ if (receipt === null)
736
739
  return null;
737
- const agentId = typeof s.task_id === 'string' ? s.task_id : undefined;
738
- // 三个都是 typed 形必填。缺 description/prompt 必须回落 **text 路径**而不是降级成 ''
739
- // ——富卡在空 prompt 时收起展开面 **且** 压过 text 兜底,'' 会两条 lane 都丢信息(对抗复审 M-1)。
740
- if (agentId === undefined || typeof s.description !== 'string' || typeof s.prompt !== 'string') {
740
+ const agentId = receipt.taskId;
741
+ // description/prompt 是**卡的可渲染性**要求(不是身份):缺它们必须回落 **text 路径**而不是
742
+ // 降级成 '' ——富卡在空 prompt 时收起展开面 **且** 压过 text 兜底,'' 会两条 lane 都丢信息
743
+ // (对抗复审 M-1)。故这两位刻意留在本臂,不进读口。
744
+ if (typeof s.description !== 'string' || typeof s.prompt !== 'string') {
741
745
  return null;
742
746
  }
743
747
  // seq = bg 子代生命周期号(SendMessage 复活即 +1)。wire 今天未必带它 —— 缺席 ⇒ 包内按
744
748
  // 首周期解释;带了就必须透传,否则复活周期的观察登记会与首周期同键而被幂等吞掉(notif-02)。
745
- registerOutstandingBgTask(agentId, s.description, s.prompt, typeof s.seq === 'number' ? s.seq : undefined);
749
+ registerOutstandingBgTask(agentId, s.description, s.prompt, receipt.seq);
746
750
  return {
747
751
  toolUseResult: {
748
752
  isAsync: true,
@@ -917,6 +921,34 @@ export function reportFindingsToolUseResult(structured, rawInput) {
917
921
  },
918
922
  };
919
923
  }
924
+ /**
925
+ * 把一张 `tool_end.structured` 窄化成 {@link AsyncLaunchedAgentReceipt} —— **仅当**它是
926
+ * `type:"agent"` 的 **`async_launched` 启动回执**臂时;完成着陆报告 / 别的族 / 缺身份 ⇒ `null`。
927
+ *
928
+ * 🔴 分臂判据 = `status`(core 原话:"Branch on `status`, never on which keys are present"),
929
+ * 与姊妹读口 {@link readCompletedAgentCard} 互为反面。本函数是这条纪律在**回执**侧的单一实现点:
930
+ * 本包现在有两个消费点 —— 富卡投影(`structuredToToolUseResult`)与 adapt 的 bg 子代 alias 登记
931
+ * (`adapt/arms.ts` 的 `tool_end_result` 臂,#158②)—— 两处不得各抄一遍键名与 `'async_launched'`。
932
+ * 🔴 `task_id` 要**非空**串:空串是「有这个键但没有身份」,拿它当 taskId 会在内容账本里铸一条
933
+ * 键为 `''` 的映射(读口那侧的 `?? ''` 兜底会当场把它认成命中)。诚实拒,不铸空 id ——
934
+ * 与 {@link readCompletedAgentCard} 对 `taskId` 的同款判据一致。
935
+ */
936
+ export function readAsyncLaunchedAgentReceipt(structured) {
937
+ if (structured === null || typeof structured !== 'object')
938
+ return null;
939
+ const s = structured;
940
+ if (s.type !== 'agent')
941
+ return null;
942
+ if (s.status !== 'async_launched')
943
+ return null;
944
+ const taskId = s.task_id;
945
+ if (typeof taskId !== 'string' || taskId.length === 0)
946
+ return null;
947
+ return {
948
+ taskId,
949
+ ...(typeof s.seq === 'number' && Number.isFinite(s.seq) ? { seq: s.seq } : {}),
950
+ };
951
+ }
920
952
  /**
921
953
  * 把一张 `tool_end.structured` 窄化成 {@link CompletedAgentCard} —— **仅当**它是 `type:"agent"` 的
922
954
  * **完成**臂时。不是 agent 卡 / 是 `async_launched` 回执 / 缺 `status` 或 `taskId` ⇒ `null`。
@@ -40,7 +40,12 @@ export interface LiveWorkflowController {
40
40
  }
41
41
  export interface LiveWorkflowConfig {
42
42
  baseUrl: string;
43
- authToken: string;
43
+ /** Bearer 串,或 `{ mode: 'same-origin-relay' }`([C175],0.29.0)—— 浏览器同源宿主形:凭证由
44
+ * 同源反代承载,`baseUrl` 该形下可为相对路径;直传 {@link makeEngineWireClient} 的同名放宽位
45
+ * (唯一构造点纪律不破:relay 形也走那一个铸口)。串形语义与 0.28.x 字节不变。 */
46
+ authToken: string | {
47
+ mode: 'same-origin-relay';
48
+ };
44
49
  /** SDK Principal(workflows are owner-scoped by principal)。缺席=不发 x-agent-principal 头
45
50
  * (owner-null,F-011 停发;两侧同缺席仍对齐)。 */
46
51
  principal: string | undefined;