@sema-agent/client-core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * ⇄ B3 批搬迁(2026-07-27,多端改造设计稿 §3 B3 / §2.4.1-C):cli `src/seam/adapter/types.ts` 的
3
+ * **运行时半场**搬入(`eventSeq` / `uuid` / `stamp` / `EmitContext`),外加 B3 的两个注入位。
4
+ *
5
+ * 🔴 **这个文件是「拆」不是「整搬」**,拆缝写明(设计稿 §2.5 同款口径):
6
+ * · 搬进来的 = 三个纯函数 + EmitContext(零依赖,浏览器可跑);
7
+ * · **留在壳里的 = 那 8 行 zod 推断型别名**(`z.infer<ReturnType<typeof SDKMessageSchema>>` …,
8
+ * 真源 = 壳内 CC 自带的 `src/entrypoints/sdk/coreSchemas.ts`)。
9
+ * 为什么不搬:① 搬它就得把 coreSchemas.ts(1,996 行 CC 自带 zod)一起 vendor 进本包并新增
10
+ * `zod` 运行时依赖 —— 本包至今 `dependencies: {}`;② 那是 CC 自带文件,本包是**公开 npm 包**;
11
+ * ③ 本仓既定词表来源是 `@sema-agent/agent-types`(全量镜像,非 curation)。
12
+ * ⇒ 壳侧 shim 里 `export type SDKMessage = z.infer<…>` 这几行**原样保留**,壳的消费面看到的
13
+ * 仍然是 zod 推断真身,一个消费点都不换型。本包内部用 agent-types 的同名镜像。
14
+ *
15
+ * 🔴 **两个 `SDKMessage` 不是同一个 symbol**(B1 发现#4,printToolResultFrame.ts 同款记账):
16
+ * 两者都镜像 CC 的同一个 union,但是两条独立的类型来源。本包搬入的三个文件里 `SDKMessage`
17
+ * **只当不透明返回型用**(每个构造点都走 `as unknown as SDKMessage`),所以这个选择在本包内
18
+ * 零后果;真正的接触点在壳侧 shim 那一刻的 tsc —— 报错就是真信号,不许 any 压。
19
+ *
20
+ * ── 以下为原文件头(逐字保留)────────────────────────────────────────────────────────────────
21
+ * Adapter-internal shared types + id-stamp / emit / uuid helpers (T21).
22
+ *
23
+ * The seam runs ONE direction here: downstream `@sema-ai/sdk` `AgentEvent`s
24
+ * (events.ts:14-41) are projected into Claude Code `SDKMessage` arms
25
+ * (coreSchemas.ts:1854 union) that the verbatim-copied REPL renders.
26
+ *
27
+ * Two laws this file pins (contract 02 §0-§1, 08 branding/altitude invariants):
28
+ * - contract-sovereign: the WIRE shape is `AgentEvent`; CC's `SDKMessage` is
29
+ * the *reference shape we reproduce* for the renderer. We DOWN-map (drop
30
+ * fields), never demand the SDK grow.
31
+ * - provider-agnostic: NO Anthropic symbol crosses this seam — no raw
32
+ * `APIAssistantMessage` block array, no `RawMessageStreamEvent` wire, no
33
+ * rate-limit window taxonomy, no `effort`/`fast_mode` vocabulary
34
+ * (events.ts:6-10; contract 02 §5). Those fields are simply never populated.
35
+ */
36
+ import type { AgentEvent } from '@sema-agent/sdk';
37
+ import type { ModelUsage, SDKMessage } from '@sema-agent/agent-types';
38
+ import type { ChromeEvent } from '../seam.js';
39
+ export type { ModelUsage, SDKMessage };
40
+ export type StampedAgentEvent = AgentEvent & {
41
+ id?: string;
42
+ };
43
+ /** Read the stamped durable seq off an AgentEvent, if present. */
44
+ export declare function eventSeq(ev: AgentEvent): string | undefined;
45
+ export declare function uuid(): string;
46
+ /**
47
+ * Per-stream identity context. `session_id` threads every emitted arm; it is the
48
+ * service-minted sessionId (TaskResult.sessionId / RunReceipt.sessionId) once
49
+ * known, falling back to a locally-minted placeholder before the first reply.
50
+ */
51
+ export interface EmitContext {
52
+ sessionId: string;
53
+ /** Wall-clock ms when the stream started draining (runStream stamps it once) — the terminal projector
54
+ * derives the CC `duration_ms` from it (P1-5: the wire carries no duration; 0 was a fake constant). */
55
+ startedAtMs?: number;
56
+ /**
57
+ * B3 新增(设计稿 §5.2 两条外向边的落点)—— chrome 事件下泄口。runStream 里那两条
58
+ * `import('../../sema/…')` 动态边改走这里:宿主注入一个 sink,库侧只发事件。
59
+ * 缺席 = 该宿主不消费(**语义 = 那个 UI 面在这个宿主上是哑的**,不是报错)。
60
+ *
61
+ * 返回 `Promise` 时 runStream 只在 **cli 原本 await 的那一处** await(plan_review park),
62
+ * 其余点 fire-and-forget —— 与搬迁前的时序逐点对齐,别顺手改成全 await。
63
+ */
64
+ emitChrome?(event: ChromeEvent): void | Promise<void>;
65
+ }
66
+ /** Stamp `uuid` + `session_id` onto a freshly-built arm body. */
67
+ export declare function stamp<T extends {
68
+ uuid?: string;
69
+ session_id?: string;
70
+ }>(ctx: EmitContext, body: T): T & {
71
+ uuid: string;
72
+ session_id: string;
73
+ };
@@ -0,0 +1,23 @@
1
+ /** Read the stamped durable seq off an AgentEvent, if present. */
2
+ export function eventSeq(ev) {
3
+ return ev.id;
4
+ }
5
+ // ── identity mock-fill (contract 02 §4: AgentEvents are anonymous) ────────────
6
+ // CC SDKMessage arms require `uuid` + `session_id`. The wire drops them, so the
7
+ // adapter mints them client-side. A v4-shaped uuid is sufficient for the
8
+ // renderer's keying; it is NEVER smuggled onto the wire.
9
+ export function uuid() {
10
+ // RFC4122-v4 shape; crypto.randomUUID when available, else a deterministic-enough fallback.
11
+ const g = typeof globalThis !== 'undefined' ? globalThis.crypto : undefined;
12
+ if (g && typeof g.randomUUID === 'function')
13
+ return g.randomUUID();
14
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
15
+ const r = (Math.random() * 16) | 0;
16
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
17
+ return v.toString(16);
18
+ });
19
+ }
20
+ /** Stamp `uuid` + `session_id` onto a freshly-built arm body. */
21
+ export function stamp(ctx, body) {
22
+ return { ...body, uuid: body.uuid ?? uuid(), session_id: ctx.sessionId };
23
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * clientSlice.ts — **verb 门面接口定型**(B4,设计稿 §8-1 裁决:「要,但只定接口不定实现」)。
3
+ *
4
+ * ## 为什么要有这一层
5
+ *
6
+ * §6.1.4 解包 CC 桌面端量到 **854 条 IPC 通道**(`ns_$_Service_$_method`),其中会话相关的
7
+ * `LocalSessions` 一族就有 212 个 verb。那一层客观存在而且很大 —— **不定接口,每端会各长一套**,
8
+ * 于是 HITL 权威登记、通知直结这些「必须只有一个落点」的东西会被重写三遍。
9
+ *
10
+ * ## 但它**只是接口**
11
+ *
12
+ * 🔴 本文件不导出任何 IPC / HTTP / SDK 实现,一行运行时代码都没有(编译产物只有类型)。
13
+ * 依据公理一:CC 的皮肤形状不进 wire ⇒ **IPC 的形状同样不该进 wire**。谁来实现:
14
+ * · TUI = 直接把 `@sema-agent/sdk` 的 AgentClient 方法接上;
15
+ * · web = BFF 转发(逐请求注入 principal);
16
+ * · 桌面 = 主进程 IPC(CC 同款 `ns_$_Service_$_method` 形)。
17
+ *
18
+ * ## 判据:什么进这个接口
19
+ *
20
+ * **会话运行时自己会调的 verb 才进**。纯 UI 触发、端各自决定的动作(开面板、切主题、选文件)
21
+ * 不进 —— 那些是端的事。今天这张表按 cli 现役会话链路归纳,`additive` 演进(只增不改),
22
+ * 与 `CHROME_ARMS` 一样做成可断言的数据出口。
23
+ */
24
+ /** 一个 verb 的登记项(覆盖率断言用;与 `CHROME_ARMS` 同形)。 */
25
+ export interface ClientVerbSpec {
26
+ /** `ClientSliceLike` 上的方法名。 */
27
+ verb: string;
28
+ /** 缺它会让**已发生的行为丢失**(而不是少个装饰)⇒ required。 */
29
+ required: boolean;
30
+ duty: string;
31
+ }
32
+ /**
33
+ * 会话运行时需要宿主提供的 verb 集合(**结构型**:实现方不必 implements,形状对上即可)。
34
+ *
35
+ * 全部返回 Promise —— 三端里有两端是跨进程/跨网络的,同步形会逼 web/桌面造假。
36
+ */
37
+ export interface ClientSliceLike {
38
+ /** 发起一个 turn,拿到 wire 帧流(喂给 `adapt()`)。 */
39
+ streamTask(req: unknown, opts?: {
40
+ signal?: {
41
+ readonly aborted: boolean;
42
+ };
43
+ }): AsyncIterable<unknown>;
44
+ /** 取消在跑的 run(Esc / `-p` 的信号兜底)。 */
45
+ cancelRun(runId: string): Promise<void>;
46
+ /** 工具审批决断。`boundCallId` / `boundInputHash` **必须原样回显**,一个字节不改。 */
47
+ respondApproval(input: {
48
+ approvalId: string;
49
+ decision: 'allow' | 'deny';
50
+ boundCallId: string;
51
+ boundInputHash: string;
52
+ }): Promise<void>;
53
+ /** AskUserQuestion 应答。 */
54
+ respondQuestion(input: {
55
+ questionId: string;
56
+ answer: unknown;
57
+ }): Promise<void>;
58
+ /** plan-review 决断(`done{status:"needs_review"}` + gate `plan_review` 的 resume 腿)。 */
59
+ respondPlanReview(input: {
60
+ taskId: string;
61
+ approved: boolean;
62
+ feedback?: string;
63
+ }): Promise<void>;
64
+ /** 后台子代的实时转录尾巴(查看态)。 */
65
+ tailSubagent(taskId: string): AsyncIterable<unknown>;
66
+ /** 给在跑的子代插话(steer)。 */
67
+ steerSubagent(input: {
68
+ taskId: string;
69
+ text: string;
70
+ }): Promise<void>;
71
+ /** 唤醒已停机的保留子代(design/144 `retainSubagentSessions` 的兑现面)。 */
72
+ wakeSubagent(input: {
73
+ taskId: string;
74
+ message: string;
75
+ }): Promise<void>;
76
+ /** fleet 行帧流(常开;后台子代唯一在场的 lane)。 */
77
+ streamFleet(): AsyncIterable<unknown>;
78
+ /** 列会话(resume 选单)。 */
79
+ listSessions(input?: {
80
+ limit?: number;
81
+ }): Promise<unknown>;
82
+ /** 读一个会话的转录(桌面与 TUI 共享 `~/.claude/projects/**` 的先例,§8-11)。 */
83
+ readSession(sessionId: string): Promise<unknown>;
84
+ /** 引擎 caps(壳侧所有 `engineCapTrue` 门的数据源)。 */
85
+ capabilities(): Promise<unknown>;
86
+ /** 健康探测(🔴 语义差见 `probeHealth` 记账:SDK 要求 `status==='ok'`,壳现实现只要 `res.ok`)。 */
87
+ probeHealth(): Promise<{
88
+ ok: boolean;
89
+ version?: string;
90
+ }>;
91
+ }
92
+ /**
93
+ * verb 覆盖率断言出口(§8-1 的另一半)。
94
+ *
95
+ * 宿主自检遍历本表,对 `required:true` 的逐条断言"我实现了"。不实现 ≠ 报错,是**那个面在该端是哑的**
96
+ * —— 与 `CHROME_ARMS` 同一套纪律:把义务做成数据,而不是散落在注释里。
97
+ */
98
+ export declare const CLIENT_VERBS: readonly [{
99
+ readonly verb: "streamTask";
100
+ readonly required: true;
101
+ readonly duty: "发起 turn 并回 wire 帧流(会话运行时的入口)";
102
+ }, {
103
+ readonly verb: "cancelRun";
104
+ readonly required: true;
105
+ readonly duty: "取消在跑的 run(不实现 = Esc 只停渲染,引擎照跑)";
106
+ }, {
107
+ readonly verb: "respondApproval";
108
+ readonly required: true;
109
+ readonly duty: "工具审批;两元组 verbatim 回显(安全不变量)";
110
+ }, {
111
+ readonly verb: "respondQuestion";
112
+ readonly required: true;
113
+ readonly duty: "AskUserQuestion 应答(不实现 = 问了没法答,turn 挂死)";
114
+ }, {
115
+ readonly verb: "respondPlanReview";
116
+ readonly required: false;
117
+ readonly duty: "plan-review 决断(fail-soft:不实现只是少一张审批卡)";
118
+ }, {
119
+ readonly verb: "tailSubagent";
120
+ readonly required: false;
121
+ readonly duty: "后台子代查看态实时转录";
122
+ }, {
123
+ readonly verb: "steerSubagent";
124
+ readonly required: false;
125
+ readonly duty: "给在跑的子代插话";
126
+ }, {
127
+ readonly verb: "wakeSubagent";
128
+ readonly required: false;
129
+ readonly duty: "唤醒保留子代(SendMessage 语义)";
130
+ }, {
131
+ readonly verb: "streamFleet";
132
+ readonly required: false;
133
+ readonly duty: "fleet 行帧流(后台子代面板行的唯一来源)";
134
+ }, {
135
+ readonly verb: "listSessions";
136
+ readonly required: false;
137
+ readonly duty: "resume 选单";
138
+ }, {
139
+ readonly verb: "readSession";
140
+ readonly required: false;
141
+ readonly duty: "读历史转录";
142
+ }, {
143
+ readonly verb: "capabilities";
144
+ readonly required: true;
145
+ readonly duty: "引擎 caps(所有能力门的数据源;不实现 = 门全判假)";
146
+ }, {
147
+ readonly verb: "probeHealth";
148
+ readonly required: false;
149
+ readonly duty: "健康探测";
150
+ }];
151
+ type _MissingFromTable = Exclude<keyof ClientSliceLike, (typeof CLIENT_VERBS)[number]['verb']>;
152
+ type _ExtraInTable = Exclude<(typeof CLIENT_VERBS)[number]['verb'], keyof ClientSliceLike>;
153
+ type _MustBeNever<T extends never> = T;
154
+ export type _VerbTableClosure = [_MustBeNever<_MissingFromTable>, _MustBeNever<_ExtraInTable>];
155
+ /**
156
+ * 🔴 `probeHealth` 的**行为差记账**(B4,设计稿 §3 B4 第四件明确要求先保旧行为)。
157
+ *
158
+ * 壳里两处手拼 `/health`(`S/limitsWire.ts:225`、`S/detachWire.ts:82`)判的是 **`res.ok`**
159
+ * (HTTP 2xx 即算活);SDK 的 `probeHealth` 额外要求 **`body.status === 'ok'`**。
160
+ * 差异面:引擎起来了但自报 degraded(2xx + `status!=='ok'`)时 —— 手拼判**活**,SDK 判**死**。
161
+ *
162
+ * B4 的处置:**先保旧行为**(不换,两处仍判 `res.ok`),差异记在这里候裁。
163
+ * 换的话是行为改动,要按宪法三问单独立项:
164
+ * · 谁需要:degraded 引擎上 `--max-*` 版本门与 detach 头版本门本就该保守拒发 ⇒ SDK 语义更对;
165
+ * · 谁受害:自报 degraded 但功能其实在线的引擎,`-p` 会少发 detach 头(断连即杀 run)⇒ 真回归;
166
+ * · 补偿:换之前先量一次"degraded 但可用"在真部署里出不出现。
167
+ */
168
+ export declare const PROBE_HEALTH_DIVERGENCE: string;
169
+ export {};
@@ -0,0 +1,62 @@
1
+ /**
2
+ * clientSlice.ts — **verb 门面接口定型**(B4,设计稿 §8-1 裁决:「要,但只定接口不定实现」)。
3
+ *
4
+ * ## 为什么要有这一层
5
+ *
6
+ * §6.1.4 解包 CC 桌面端量到 **854 条 IPC 通道**(`ns_$_Service_$_method`),其中会话相关的
7
+ * `LocalSessions` 一族就有 212 个 verb。那一层客观存在而且很大 —— **不定接口,每端会各长一套**,
8
+ * 于是 HITL 权威登记、通知直结这些「必须只有一个落点」的东西会被重写三遍。
9
+ *
10
+ * ## 但它**只是接口**
11
+ *
12
+ * 🔴 本文件不导出任何 IPC / HTTP / SDK 实现,一行运行时代码都没有(编译产物只有类型)。
13
+ * 依据公理一:CC 的皮肤形状不进 wire ⇒ **IPC 的形状同样不该进 wire**。谁来实现:
14
+ * · TUI = 直接把 `@sema-agent/sdk` 的 AgentClient 方法接上;
15
+ * · web = BFF 转发(逐请求注入 principal);
16
+ * · 桌面 = 主进程 IPC(CC 同款 `ns_$_Service_$_method` 形)。
17
+ *
18
+ * ## 判据:什么进这个接口
19
+ *
20
+ * **会话运行时自己会调的 verb 才进**。纯 UI 触发、端各自决定的动作(开面板、切主题、选文件)
21
+ * 不进 —— 那些是端的事。今天这张表按 cli 现役会话链路归纳,`additive` 演进(只增不改),
22
+ * 与 `CHROME_ARMS` 一样做成可断言的数据出口。
23
+ */
24
+ /**
25
+ * verb 覆盖率断言出口(§8-1 的另一半)。
26
+ *
27
+ * 宿主自检遍历本表,对 `required:true` 的逐条断言"我实现了"。不实现 ≠ 报错,是**那个面在该端是哑的**
28
+ * —— 与 `CHROME_ARMS` 同一套纪律:把义务做成数据,而不是散落在注释里。
29
+ */
30
+ // 🔴 **不要**给这个 const 加 `: readonly ClientVerbSpec[]` 注解 —— 注解会把 `verb` 拓宽成 `string`,
31
+ // 下面那道编译期双向闭合门就变成恒真的空转(B4 首写时正是这么写的,加注解后 `Exclude` 恒为 `string`
32
+ // ⇒ 门当场红且删表项也红,红得毫无判别力)。用 `as const satisfies` 既约束形状又保住字面量类型。
33
+ export const CLIENT_VERBS = [
34
+ { verb: 'streamTask', required: true, duty: '发起 turn 并回 wire 帧流(会话运行时的入口)' },
35
+ { verb: 'cancelRun', required: true, duty: '取消在跑的 run(不实现 = Esc 只停渲染,引擎照跑)' },
36
+ { verb: 'respondApproval', required: true, duty: '工具审批;两元组 verbatim 回显(安全不变量)' },
37
+ { verb: 'respondQuestion', required: true, duty: 'AskUserQuestion 应答(不实现 = 问了没法答,turn 挂死)' },
38
+ { verb: 'respondPlanReview', required: false, duty: 'plan-review 决断(fail-soft:不实现只是少一张审批卡)' },
39
+ { verb: 'tailSubagent', required: false, duty: '后台子代查看态实时转录' },
40
+ { verb: 'steerSubagent', required: false, duty: '给在跑的子代插话' },
41
+ { verb: 'wakeSubagent', required: false, duty: '唤醒保留子代(SendMessage 语义)' },
42
+ { verb: 'streamFleet', required: false, duty: 'fleet 行帧流(后台子代面板行的唯一来源)' },
43
+ { verb: 'listSessions', required: false, duty: 'resume 选单' },
44
+ { verb: 'readSession', required: false, duty: '读历史转录' },
45
+ { verb: 'capabilities', required: true, duty: '引擎 caps(所有能力门的数据源;不实现 = 门全判假)' },
46
+ { verb: 'probeHealth', required: false, duty: '健康探测' },
47
+ ];
48
+ /**
49
+ * 🔴 `probeHealth` 的**行为差记账**(B4,设计稿 §3 B4 第四件明确要求先保旧行为)。
50
+ *
51
+ * 壳里两处手拼 `/health`(`S/limitsWire.ts:225`、`S/detachWire.ts:82`)判的是 **`res.ok`**
52
+ * (HTTP 2xx 即算活);SDK 的 `probeHealth` 额外要求 **`body.status === 'ok'`**。
53
+ * 差异面:引擎起来了但自报 degraded(2xx + `status!=='ok'`)时 —— 手拼判**活**,SDK 判**死**。
54
+ *
55
+ * B4 的处置:**先保旧行为**(不换,两处仍判 `res.ok`),差异记在这里候裁。
56
+ * 换的话是行为改动,要按宪法三问单独立项:
57
+ * · 谁需要:degraded 引擎上 `--max-*` 版本门与 detach 头版本门本就该保守拒发 ⇒ SDK 语义更对;
58
+ * · 谁受害:自报 degraded 但功能其实在线的引擎,`-p` 会少发 detach 头(断连即杀 run)⇒ 真回归;
59
+ * · 补偿:换之前先量一次"degraded 但可用"在真部署里出不出现。
60
+ */
61
+ export const PROBE_HEALTH_DIVERGENCE = 'probeHealth: SDK 要求 status==="ok",壳侧 limitsWire:225 / detachWire:82 手拼只要 res.ok。' +
62
+ 'B4 保旧行为(res.ok),差异候裁 —— 换成 SDK 语义会让"自报 degraded 但可用"的引擎少发 detach 头。';
@@ -27,7 +27,31 @@ export type WireAuthToken = string | {
27
27
  };
28
28
  /** loopback 判定(engineTarget.isLoopbackEngineUrl 逐字镜像;解析失败=非 loopback)。 */
29
29
  export declare function isLoopbackWireUrl(url: string): boolean;
30
- /** #118 三态凭证解析(resolveLiveAuthToken 纯半场镜像)。 */
30
+ /**
31
+ * §8-2 定谳的**唯一**凭证解析口(B4 收编)—— 11 处构造点的公共分母。
32
+ *
33
+ * 背景(设计稿 §8-2 + B4 实测):壳里今天是 **2 份三态解析**(`engineTarget.resolveLiveAuthToken`
34
+ * 产品半场 + 本包 `wireAuthTokenFor` 纯半场,后者头注自述是前者的「逐字镜像」⇒ 两份必然漂移)
35
+ * 外加 **11 处 `new AgentClient` 直造**,其中 7 处根本不走任何解析口,直接手写
36
+ * `token ?? { mode:'loopback-unauthed' }` —— 那一形**丢了 'anon' 那一态**:非 loopback 且无 token
37
+ * 时它给的是 loopback-unauthed(出站零 Authorization 头),而三态解析给 'anon'。对 fail-closed
38
+ * worker 两者都是 401,但对 **owner-gated 路由**不是同一回事,所以这是真分歧不是等价写法。
39
+ *
40
+ * 🔴 **env 缺省留在端上**:`resolveLiveAuthToken` 的第二参缺省是 `process.env.SEMA_LIVE_TOKEN`,
41
+ * 那一半**绝不进包**(本包零 `process.env` —— portability 门盯着)。壳侧 shim 保留缺省参即可:
42
+ * `resolveLiveAuthToken(u, t = process.env.SEMA_LIVE_TOKEN) => resolveWireAuth(u, t)`。
43
+ *
44
+ * 三态(#118):token 有 ⇒ 原样 Bearer;无 token ∧ loopback ⇒ `{mode:'loopback-unauthed'}`
45
+ * (出站零 Authorization 头,绝不伪造);无 token ∧ 非 loopback ⇒ `'anon'`(fail-closed 换诚实 401)。
46
+ * `baseUrl` 允许 undefined(= `resolveLiveAuthToken` 的签名超集,本地自 spawn 态)⇒ 'anon'。
47
+ */
48
+ export declare function resolveWireAuth(baseUrl: string | undefined, token?: string): WireAuthToken;
49
+ /**
50
+ * #118 三态凭证解析(旧名,B2 引入)。
51
+ * B4 起是 `resolveWireAuth` 的**别名**(baseUrl 必填的窄签名);两者行为逐字相同,保留是为了
52
+ * 壳侧既有 import 零改动 —— 不设代际让位([clean-cut-no-legacy-compat] 只约束**行为面**硬切,
53
+ * 同一函数的两个名字不是两套行为)。新代码一律用 `resolveWireAuth`。
54
+ */
31
55
  export declare function wireAuthTokenFor(baseUrl: string, token?: string): WireAuthToken;
32
56
  export type EngineWireClientConfig = {
33
57
  baseUrl: string;
@@ -32,14 +32,40 @@ export function isLoopbackWireUrl(url) {
32
32
  return false;
33
33
  }
34
34
  }
35
- /** #118 三态凭证解析(resolveLiveAuthToken 纯半场镜像)。 */
36
- export function wireAuthTokenFor(baseUrl, token) {
35
+ /**
36
+ * §8-2 定谳的**唯一**凭证解析口(B4 收编)—— 11 处构造点的公共分母。
37
+ *
38
+ * 背景(设计稿 §8-2 + B4 实测):壳里今天是 **2 份三态解析**(`engineTarget.resolveLiveAuthToken`
39
+ * 产品半场 + 本包 `wireAuthTokenFor` 纯半场,后者头注自述是前者的「逐字镜像」⇒ 两份必然漂移)
40
+ * 外加 **11 处 `new AgentClient` 直造**,其中 7 处根本不走任何解析口,直接手写
41
+ * `token ?? { mode:'loopback-unauthed' }` —— 那一形**丢了 'anon' 那一态**:非 loopback 且无 token
42
+ * 时它给的是 loopback-unauthed(出站零 Authorization 头),而三态解析给 'anon'。对 fail-closed
43
+ * worker 两者都是 401,但对 **owner-gated 路由**不是同一回事,所以这是真分歧不是等价写法。
44
+ *
45
+ * 🔴 **env 缺省留在端上**:`resolveLiveAuthToken` 的第二参缺省是 `process.env.SEMA_LIVE_TOKEN`,
46
+ * 那一半**绝不进包**(本包零 `process.env` —— portability 门盯着)。壳侧 shim 保留缺省参即可:
47
+ * `resolveLiveAuthToken(u, t = process.env.SEMA_LIVE_TOKEN) => resolveWireAuth(u, t)`。
48
+ *
49
+ * 三态(#118):token 有 ⇒ 原样 Bearer;无 token ∧ loopback ⇒ `{mode:'loopback-unauthed'}`
50
+ * (出站零 Authorization 头,绝不伪造);无 token ∧ 非 loopback ⇒ `'anon'`(fail-closed 换诚实 401)。
51
+ * `baseUrl` 允许 undefined(= `resolveLiveAuthToken` 的签名超集,本地自 spawn 态)⇒ 'anon'。
52
+ */
53
+ export function resolveWireAuth(baseUrl, token) {
37
54
  if (token)
38
55
  return token;
39
- if (isLoopbackWireUrl(baseUrl))
56
+ if (baseUrl !== undefined && isLoopbackWireUrl(baseUrl))
40
57
  return { mode: 'loopback-unauthed' };
41
58
  return 'anon';
42
59
  }
60
+ /**
61
+ * #118 三态凭证解析(旧名,B2 引入)。
62
+ * B4 起是 `resolveWireAuth` 的**别名**(baseUrl 必填的窄签名);两者行为逐字相同,保留是为了
63
+ * 壳侧既有 import 零改动 —— 不设代际让位([clean-cut-no-legacy-compat] 只约束**行为面**硬切,
64
+ * 同一函数的两个名字不是两套行为)。新代码一律用 `resolveWireAuth`。
65
+ */
66
+ export function wireAuthTokenFor(baseUrl, token) {
67
+ return resolveWireAuth(baseUrl, token);
68
+ }
43
69
  /**
44
70
  * 构造一个引擎 wire 用 AgentClient。null = 构造失败(SDK fail-closed 拒绝——不该发生:
45
71
  * wireAuthTokenFor 恒给合法形;防御性 fail-soft,调用方按各自离线/探测失败路径处理)。
@@ -48,7 +74,7 @@ export function makeEngineWireClient(cfg) {
48
74
  try {
49
75
  return new AgentClient({
50
76
  baseUrl: cfg.baseUrl,
51
- authToken: wireAuthTokenFor(cfg.baseUrl, cfg.token),
77
+ authToken: resolveWireAuth(cfg.baseUrl, cfg.token),
52
78
  principal: cfg.principal ?? 'anon:shell-live',
53
79
  ...(cfg.timeoutMs !== undefined ? { timeoutMs: cfg.timeoutMs } : {}),
54
80
  maxRetries: cfg.maxRetries ?? 0,
package/dist/host.d.ts ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * host.ts — **包级宿主装配**(B4;B3 交接报告的建议件 + 设计稿 §5.4 「web 宿主的最小接入面」)。
3
+ *
4
+ * ## 两种口,别混
5
+ *
6
+ * · `AdapterContext`(seam.ts)= **per-turn 切片** —— 时钟、铸号、signal、定时器、攒批节拍。
7
+ * 它随每个 `adapt()` 调用传进来,因为这些东西**每个 turn 可以不一样**(比如这一 turn 可中断)。
8
+ * · `installHost()`(本文件)= **进程/端级装配** —— settings 读取、fs、通知队列。
9
+ * 它们在会话开始之前就该在位,而且被**请求构造期**的代码调用(`hooksForWire()` /
10
+ * `scratchpadDirField()`),那时候根本没有 `ctx` 可传。
11
+ *
12
+ * ⇒ 判据:**这个能力是不是每 turn 会变?** 会 ⇒ ctx;不会 ⇒ installHost。
13
+ *
14
+ * ## 缺席语义(与 AdapterContext 同一条纪律)
15
+ *
16
+ * 🔴 缺席 = **该能力不启用**,不是报错、更不是「换个方式偷偷做」。库绝不 `require('fs')`、
17
+ * 绝不 `console.log`、绝不读 `process.env` 兜底 —— 那正是搬迁要切掉的东西。
18
+ * 但「静默不启用」有个已知的失败形:**宿主漏装了,而症状是某个面悄悄空掉**。所以每个口都带
19
+ * miss 计数(`hostPortMisses()`),宿主自检断言它恒为 0 —— 与 `notificationQueuePortMisses()`
20
+ * 同一套姿势([probe-must-prove-it-speaks]:不能让「什么都没有」看起来像「一切正常」)。
21
+ */
22
+ import type { AdapterLogLevel } from './seam.js';
23
+ import { type NotificationQueuePort } from './notifications.js';
24
+ /**
25
+ * settings 口 —— 用户 settings 文件的读取面(壳=三层合并后的文件读;web=center 下发;桌面=主进程)。
26
+ *
27
+ * 🔴 `shouldSkipHookDueToTrust` 是**安全门**不是取值:未受信目录的 hook 绝不上 wire。
28
+ * 端必须真实现它;不实现(端返回恒 false)= 把信任门拆了。所以它在 required 清单里。
29
+ */
30
+ export interface SettingsPort {
31
+ /** 按来源取 settings(cli `utils/settings/settings.ts` 的 `getSettingsForSource`)。 */
32
+ getSettingsForSource(source: string): unknown;
33
+ /** 信任门:该 hook 是否因来源不受信而跳过。**返回 true = 跳过**。 */
34
+ shouldSkipHookDueToTrust(hook: unknown): boolean;
35
+ }
36
+ /**
37
+ * fs 口 —— scratchpad 目录族(cli `utils/permissions/filesystem.ts`)。
38
+ * `ensureScratchpadDir` 是**真 IO**(mkdir);浏览器宿主整个口省略即可(⇒ 不 stamp scratchpadDir,
39
+ * additive 字段缺席对引擎无害)。
40
+ */
41
+ export interface FsPort {
42
+ getScratchpadDir(): string | undefined;
43
+ isScratchpadEnabled(): boolean;
44
+ /** 🔴 壳侧真身是 **async**(mkdir 0o700);端可返 void 或 Promise,库两种都吞(fire-and-forget)。 */
45
+ ensureScratchpadDir(): void | Promise<void>;
46
+ }
47
+ /** 定时器口(进程级默认;`ctx.setTimer` 仍可按 turn 覆盖)。 */
48
+ export interface TimersPort {
49
+ setTimer(ms: number, fn: () => void): unknown;
50
+ clearTimer(handle: unknown): void;
51
+ }
52
+ /** `installHost` 的入参 —— 全部可选,缺席即该能力不启用。 */
53
+ export interface HostPorts {
54
+ log?(level: AdapterLogLevel, msg: string, data?: unknown): void;
55
+ probe?(channel: string, line: string): void;
56
+ /** 通知队列(B2 引入)。经本口装 == 直接调 `installNotificationQueuePort`,单一真源不分裂。 */
57
+ queue?: NotificationQueuePort | null;
58
+ timers?: TimersPort;
59
+ settings?: SettingsPort;
60
+ fs?: FsPort;
61
+ }
62
+ /**
63
+ * 装配宿主口。**可多次调用做增量装配**(只覆盖本次给出的键),便于分模块装。
64
+ * 传 `queue` 会直通 `installNotificationQueuePort` —— 队列的真源仍在 notifications.ts,
65
+ * 本文件不另存一份([paired-mechanisms-must-share-premise]:两处各存一份 = 装了 A 读了 B)。
66
+ * 返回一个还原函数(测试/热重载用)。
67
+ */
68
+ export declare function installHost(ports: HostPorts): () => void;
69
+ /** 日志(缺席 = 静默,**不计 miss**:诊断面本就是可选的)。 */
70
+ export declare function hostLog(level: AdapterLogLevel, msg: string, data?: unknown): void;
71
+ /** 行式探针(缺席 = 静默,不计 miss:同上)。 */
72
+ export declare function hostProbe(channel: string, line: string): void;
73
+ /** settings 口(缺席 = undefined + 计 miss:漏装会让 hooks 整条 wire 静默空掉)。 */
74
+ export declare function hostSettings(): SettingsPort | undefined;
75
+ /** fs 口(缺席 = undefined + 计 miss)。 */
76
+ export declare function hostFs(): FsPort | undefined;
77
+ /** 定时器口(缺席 = undefined,不计 miss:T40 竞速本就允许不启用)。 */
78
+ export declare function hostTimers(): TimersPort | undefined;
79
+ /**
80
+ * 🔴 宿主自检出口 —— 恒应为空对象。非空 = 有口漏装,**对应的面已经在静默失效**。
81
+ * 含通知队列的 miss(从 notifications.ts 汇过来,宿主只需看这一个数)。
82
+ */
83
+ export declare function hostPortMisses(): Record<string, number>;
84
+ /**
85
+ * 测试钩:清空装配与计数。
86
+ * 🔴 队列那一路必须走 `_resetNotificationQueuePortForTest()` 而不是 `installNotificationQueuePort(null)`
87
+ * —— 后者只卸口、**不清 miss 计数**,于是 `hostPortMisses()` 会带着上一段测试的历史 miss,
88
+ * 让「未装配时 misses 为空」这类断言恒假(B4 首写时就撞上了)。
89
+ */
90
+ export declare function _resetHostForTest(): void;
package/dist/host.js ADDED
@@ -0,0 +1,84 @@
1
+ import { _resetNotificationQueuePortForTest, installNotificationQueuePort, notificationQueuePortMisses, } from './notifications.js';
2
+ let host = {};
3
+ const misses = Object.create(null);
4
+ const warned = new Set();
5
+ function miss(name) {
6
+ misses[name] = (misses[name] ?? 0) + 1;
7
+ if (!warned.has(name)) {
8
+ warned.add(name);
9
+ // eslint-disable-next-line no-console
10
+ console.error(`[client-core] host port "${name}" NOT installed — the surface it drives is silently inert. ` +
11
+ 'Host must call installHost({ ' +
12
+ name +
13
+ ': … }) at startup (see host.ts).');
14
+ }
15
+ return undefined;
16
+ }
17
+ /**
18
+ * 装配宿主口。**可多次调用做增量装配**(只覆盖本次给出的键),便于分模块装。
19
+ * 传 `queue` 会直通 `installNotificationQueuePort` —— 队列的真源仍在 notifications.ts,
20
+ * 本文件不另存一份([paired-mechanisms-must-share-premise]:两处各存一份 = 装了 A 读了 B)。
21
+ * 返回一个还原函数(测试/热重载用)。
22
+ */
23
+ export function installHost(ports) {
24
+ const prev = { ...host };
25
+ if ('log' in ports)
26
+ host.log = ports.log;
27
+ if ('probe' in ports)
28
+ host.probe = ports.probe;
29
+ if ('timers' in ports)
30
+ host.timers = ports.timers;
31
+ if ('settings' in ports)
32
+ host.settings = ports.settings;
33
+ if ('fs' in ports)
34
+ host.fs = ports.fs;
35
+ if ('queue' in ports)
36
+ installNotificationQueuePort(ports.queue ?? null);
37
+ return () => {
38
+ host = prev;
39
+ };
40
+ }
41
+ /** 日志(缺席 = 静默,**不计 miss**:诊断面本就是可选的)。 */
42
+ export function hostLog(level, msg, data) {
43
+ host.log?.(level, msg, data);
44
+ }
45
+ /** 行式探针(缺席 = 静默,不计 miss:同上)。 */
46
+ export function hostProbe(channel, line) {
47
+ host.probe?.(channel, line);
48
+ }
49
+ /** settings 口(缺席 = undefined + 计 miss:漏装会让 hooks 整条 wire 静默空掉)。 */
50
+ export function hostSettings() {
51
+ return host.settings ?? miss('settings');
52
+ }
53
+ /** fs 口(缺席 = undefined + 计 miss)。 */
54
+ export function hostFs() {
55
+ return host.fs ?? miss('fs');
56
+ }
57
+ /** 定时器口(缺席 = undefined,不计 miss:T40 竞速本就允许不启用)。 */
58
+ export function hostTimers() {
59
+ return host.timers;
60
+ }
61
+ /**
62
+ * 🔴 宿主自检出口 —— 恒应为空对象。非空 = 有口漏装,**对应的面已经在静默失效**。
63
+ * 含通知队列的 miss(从 notifications.ts 汇过来,宿主只需看这一个数)。
64
+ */
65
+ export function hostPortMisses() {
66
+ const out = { ...misses };
67
+ const q = notificationQueuePortMisses();
68
+ if (q > 0)
69
+ out.queue = q;
70
+ return out;
71
+ }
72
+ /**
73
+ * 测试钩:清空装配与计数。
74
+ * 🔴 队列那一路必须走 `_resetNotificationQueuePortForTest()` 而不是 `installNotificationQueuePort(null)`
75
+ * —— 后者只卸口、**不清 miss 计数**,于是 `hostPortMisses()` 会带着上一段测试的历史 miss,
76
+ * 让「未装配时 misses 为空」这类断言恒假(B4 首写时就撞上了)。
77
+ */
78
+ export function _resetHostForTest() {
79
+ host = {};
80
+ for (const k of Object.keys(misses))
81
+ delete misses[k];
82
+ warned.clear();
83
+ _resetNotificationQueuePortForTest();
84
+ }
package/dist/index.d.ts CHANGED
@@ -5,6 +5,19 @@
5
5
  * 沿革:@sema-agent/wire-cc-adapter 0.1.0 = seam 类型 + id 确定性派生 + 首批踩坑纯函数;
6
6
  * 0.1.2(#52a)= adapt() 管线首批(纯投影臂全落 + 壳态耦合臂投影成 ChromeEvent + 差分守卫);
7
7
  * 0.2.0 = 迁入 sema-client-core 独立仓并改名(旧 npm 名 deprecate 指本包);
8
+ * 0.6.0 = **B4 批**(设计稿 §3 B4):A 层帧分派**收官**(第 14 臂 `task_progress` + 行绑定/
9
+ * inline stats/alias/#6 常驻台账/SubagentStart/workflow lane 三级门 + `settlePanelTasks` 三处 sweep
10
+ * 全接线)· P0-2 tool label 侧信道补漏(0.5.0 真缺口)· **请求面合一**(`request/taskRequest.ts`:
11
+ * 三个构造器的字段集与合并语义收成一份,车道差异做成 `REQUEST_FIELD_MATRIX` 数据表)·
12
+ * §8-2 构造点收编(`resolveWireAuth`)· §8-1 verb 门面接口定型(`ClientSliceLike` + `CLIENT_VERBS`,
13
+ * **编译期**双向闭合门)· `CHROME_ARMS` 覆盖率出口 · 包级宿主装配 `installHost()`(settings/fs/
14
+ * queue/timers 四口 + `hostPortMisses()` 自检)· `scratchpadWireCaps` 搬入(FsPort 端到端证明)。
15
+ * 0.5.0 = **B3 批**(设计稿 §3 B3):`AdapterContext` 扩容(log/probe/signal/setTimer/clearTimer/
16
+ * coalesceIntervalMs,**只加可选位 = minor 不 BREAKING**)· 适配内核搬入(`adapter/types` 运行时
17
+ * 半场 + `adapter/downstream` 三件 + `adapter/runStream`)· runStream **两条外向边切除**
18
+ * (lastTurnUsageStore / planReviewWire ⇒ `last_turn_usage` / `plan_review_park` 两个 chrome 臂)·
19
+ * T38 三清第三清 + T40 IDLE-FLUSH + T57 第五处断闸 + T58 iterator cleanup + T59 abort + T60 收尾腿。
20
+ * 🆕 第三道守卫 `scripts/run-client-core-portability-test.mjs`(内核闭包 1571 文件 → **6 文件**)。
8
21
  * 0.4.0 = **B2 批**(设计稿 §3 B2):通知族三文件合并进 notifications.ts(engineTaskNotification 全文 +
9
22
  * taskNotificationErrorSupplement 全文 + hookNoticeStore 判定半场)· 17 个 `*WireCaps` + engineWireSdk +
10
23
  * sseIdleTriage + classifierVerdictWire 判定半场 + controlRouter + 模型面纯逻辑 4 件 + 新写 hostEnv;
@@ -23,8 +36,20 @@
23
36
  * —— B2 新增:notifications.ts(11 个通知台账 + 队列 port + 两个 probe 槽)·
24
37
  * agentsWireCaps.ts(prepare 缓存/投影闭包/一次性告警集)· liveModelCatalog.ts(目录 + refresher)·
25
38
  * sessionModelLatch.ts(三个 latch)· ultracodeWireCaps.ts(sticky preset)。
26
- * 🔴 **宿主装配**(B2 起本包有必须由宿主注入才完整的口):`installNotificationQueuePort()` ——
39
+ * —— B4 新增:scratchpadWireCaps.ts(`ensured` 建目录闩)· host.ts(宿主口装配 + miss 计数);
40
+ * 适配器**实例级**(非 module 级)新增 `firedSubagentStartHookTaskIds`——cli 那份是 module 级,
41
+ * 本包放实例级是**有意的**(多 session 宿主上 module 形会让两个会话互吞 SubagentStart,见 DIVERGENCE-6)。
42
+ * —— B3 新增:adapter/runStream.ts(`inFlightTurns` 计数,读口 `isRunStreamActive()`;
43
+ * 两份实例 = 引擎滚动升级门恒读 false ⇒ **turn 中途热换引擎**,正是它当初要防的事)。
44
+ * 🔴 **宿主装配**——B4 起总入口是 `installHost({log,probe,queue,timers,settings,fs})`
45
+ * (`installNotificationQueuePort()` 仍是队列的真源出口,`installHost({queue})` 直通它,两者不分裂);
46
+ * 自检口 `hostPortMisses()` 恒应为空对象,非空 = 有口漏装、对应的面已在静默失效。
47
+ * —— B2 起的既有说明:`installNotificationQueuePort()` ——
27
48
  * 不装 = task-notification 投递静默丢失(`notificationQueuePortMisses()` 恒应为 0)。
49
+ * —— B3 新增(可选口,缺席 = 对应 UI 面在该宿主上是哑的,不是报错):`ctx.emitChrome`
50
+ * (runStream 的两条切边:statusline 真 usage / plan-review 审批卡)· `ctx.setTimer`
51
+ * (T40 IDLE-FLUSH,不给 = 长工具参数生成期已生成内容仍押到下一帧才上屏)· `ctx.signal`
52
+ * (Esc 中断)· `ctx.log` / `ctx.probe`(诊断面)。
28
53
  */
29
54
  export * from './seam.js';
30
55
  export * from './workflow.js';
@@ -68,3 +93,12 @@ export * from './liveModelCatalog.js';
68
93
  export * from './modelBudgetRule.js';
69
94
  export * from './sessionModelLatch.js';
70
95
  export * from './effortWire.js';
96
+ export * from './adapter/types.js';
97
+ export * from './adapter/downstream/turnUsageToModelUsage.js';
98
+ export * from './adapter/downstream/eventToSdkMessage.js';
99
+ export * from './adapter/downstream/terminalToSdkResult.js';
100
+ export * from './adapter/runStream.js';
101
+ export * from './request/taskRequest.js';
102
+ export * from './clientSlice.js';
103
+ export * from './host.js';
104
+ export * from './scratchpadWireCaps.js';