@sema-agent/client-core 0.30.4 → 0.30.5

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.
package/CHANGELOG.md CHANGED
@@ -16,6 +16,41 @@
16
16
  > 🔴 **互链**(web [C166]⑦):各版「已知局限」段只记**该版新增**;接入面已知局限的完整台账在
17
17
  > `docs/INTEGRATION-CLIENTS.md` §6e/§7 —— **只读其一会漏**,两处都过。
18
18
 
19
+ ## 0.30.5
20
+
21
+ - **#244 F4 族D:配置/模型表单源化(A-028.17/.18/.20 三件单源上收 + A-028.19 对账钉,additive,
22
+ 零 BREAKING)**(2026-08-15;公开导出 707 → 724,+17 全 additive;常驻钉 = pure 门 F4D 段 35
23
+ checks + F4D-seat-catalog-pin 条件腿 4 checks(红先绿后:壳钉落地前该腿以断言红留证;codex
24
+ 复审两处 [medium] 采纳 —— 腿剥注释锚初始化式防「删钉留注释」假绿、钉补精确等型轴防
25
+ readonly/any 漂移,后者以 d.ts 变异 readonly 化 → 壳 typecheck 红实证)):
26
+ - **件1 `model/providerCatalog.ts`(A-028.17)**:46 家 provider 表的**规范折表层** ——
27
+ `ProviderCatalogRow`(cli DirectoryRow 与 web 折表字段的严格超集)+ `providerCatalogRows` /
28
+ `providerCatalogCount` / `providerCatalogRowDetail` / `providerPresetById` / `providerApiShortName`。
29
+ 此前这张表被 cli/web 折了三次(cli providerDirectory + web catalog.ts + web 降级表映射),
30
+ 「host 怎么取 / 零模型家怎么说 / 顺序动不动」的判定各持一份靠人工对齐。#175 三条设计约束
31
+ 逐字随迁(单一真源现算 / 全表不重排不过滤 / 诚实缺席「model id typed in」不编「0 models」)。
32
+ cli 已换装 re-export;web 第二份折表退役归 web 半场(发布帖点名)。
33
+ - **件2 `model/tierVocabulary.ts`(A-028.18)**:档位五档词表 + 派生 + 校验单源 —— `Tier` /
34
+ `TIER_ORDER` / `SURFACED_TIERS` / `CC_TIER_ALIASES` / `DEFAULT_TIER` / `TIER_BLURBS` /
35
+ `TierGroupDef` / `isTier` / `resolveTierBinding`(fail-open 沿链向下,镜像 core expandTiers)。
36
+ 此前跨两仓共三份(cli tierCore + web tiers-page 词表 + web wording.ts 词表+派生)。settings
37
+ 读写/归一/投影那半场仍归宿主(cli tierCore 只留存储)。
38
+ - **件3 `env/localeTag.ts` + `env/uiLanguage.ts`(A-028.20)**:locale tag **纯手术**单源
39
+ (`parseLocaleTag`:剥 codeset/modifier、中性 locale 认定、子标签切分)+ UI 语言词表与判定
40
+ (`UiLanguage` / `UI_LANGUAGES` / `UI_LANGUAGE_ENDONYMS` / `isUiLanguage` / `pickUiLanguage`,
41
+ 自 cli i18n/core 整体迁入)。🔴 **双出口成文**(输出域裁定):同一台手术,两个判定各答各的
42
+ 题 —— `pickUiLanguage` 答「界面用哪种文字」(en/zh,未知回落可读的 en),`resolveRegionHint`
43
+ 答「哪个地址更可达」(cn/intl/unknown,未知绝不猜);`zh-Hant` 在前者是 zh、在后者是 intl,
44
+ 是设计不是漂移。`localeGeo` 的 tag 手术改经同一单源(行为逐条保持,GEO 段 103 checks 未动)。
45
+ - **件4 SeatCatalogEntry 对账钉(A-028.19)**:`seatContract.ts` 头注自始声称「cli 侧以
46
+ satisfies 断言钉住两形不漂」而壳全仓 grep 0 命中(声明比现实多说)。修 = cli applyOnboard.ts
47
+ 落真钉(双向 assignability + keyof 键集对轴,类型面零运行时逻辑)+ 本包 pure 门
48
+ `F4D-seat-catalog-pin` 条件腿反向钉「钉必须在场」(钉被删即红)。
49
+ - 门随批:portability index 闭包上限 128 → 132(四件纯叶逐件登记,零新外部包零 Node 内建);
50
+ typeshape unknown 出境 238 → 240(两条类型守卫 unknown 入参位,同族登记);singleton 名单
51
+ +7 常量表条目(localeGeo NEUTRAL_LOCALES 随手术迁 localeTag);INTEGRATION-CLIENTS §0a/§2/§2b
52
+ 计数与域 11/14 行同批对齐。
53
+
19
54
  ## 0.30.4
20
55
 
21
56
  - **#280 移交两件:probeCause 随卡透传 + tail meta 帧发布口(additive,零 BREAKING)**(2026-08-15;
package/README.md CHANGED
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
35
35
 
36
36
  ## Scope
37
37
 
38
- **Version:** 0.30.4
38
+ **Version:** 0.30.5
39
39
 
40
40
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
41
41
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -1,43 +1,3 @@
1
- /**
2
- * localeGeo.ts — **地域预选 v1(零网络)**:只看系统 locale 与 IANA 时区,给出一个「默认选哪个
3
- * 地址更可达」的提示。三端复用(CLI / web / 桌面)。
4
- *
5
- * ## 🔴 零耦合约束:模型 provider 目录不吃地域信号(clay 令 2026-07-31)
6
- *
7
- * 模型 provider **不分国内国外,完全用户选择,走全表** —— 模型 provider 表恒**全表呈现、顺序
8
- * 不随地域变**,选哪家 100% 是用户的事。因此本模块与模型目录之间是**设计上的零耦合**,不是巧合:
9
- * · 本模块**不导出**任何 `sortProvidersByRegion` / `filterProvidersFor(region)` 形状的函数;
10
- * · `src/model/**` **不许** import 本模块(pure 门 GEO 段有机械断言把这条钉住,不靠自觉);
11
- * · 唯一合法用途:**搜索 provider / 镜像源 / SearXNG 镜像地址**这类「取哪个地址更可达」的
12
- * **默认预选**,而且**永远只是默认值,用户可改**。
13
- *
14
- * ## 为什么 v1 零网络
15
- *
16
- * 拨号探测(测 RTT / 查出口 IP)会在**用户还没同意任何事**之前就产生一次出站请求,而它换来的
17
- * 只是一个初始默认值。locale 与时区是宿主已经知道的事实,不需要问任何人。⇒ 本模块是纯函数,
18
- * 输入由调用方注入(不读宿主的环境变量,与本包 hostEnv 纪律一致:环境是宿主资产)。
19
- * 门里那条「零环境读取」断言查的是**整份源文件**(注释也算),所以本文里刻意不出现那个成员
20
- * 表达式的字面写法 —— 判据不必先长出一个词法器,代价只是这一行说明。
21
- *
22
- * ## 三档,`unknown` 不是 `intl`
23
- *
24
- * 两个信号都没有 ⇒ `unknown`,**绝不猜 `intl`**。「不知道」和「知道它在境外」是两件事:端拿到
25
- * `unknown` 应当**不预选**(让用户自己挑),拿到 `intl` 才是预选国际地址。把不知道渲染成一个
26
- * 具体答案,就是[honest-absence-not-fabricated-zero]里那条「编造零值」的同族错误。
27
- *
28
- * ## 冲突时**时区赢**(locale 说 zh 但时区是 America/New_York)
29
- *
30
- * 理由锚在本模块的用途上 —— 判的是「**哪个地址更可达**」,那是**机器在哪张网上**决定的:
31
- * · **时区** = 系统对自己**所在位置**的记录(装机/NTP 时按位置设),与网络出口高度相关;
32
- * · **locale** = 用户偏好**哪种语言**,与机器在哪张网上无关(在纽约用中文界面的人很多)。
33
- * ⇒ 时区是位置的更强证据,冲突时它赢。两个信号一致时 `reason` 同时点名两者(证据更足)。
34
- *
35
- * ## `reason` 是「我看到了什么」,不是「你在哪」
36
- *
37
- * 端会把它渲成「已按你的系统区域预选,可随时改」。所以 reason 只陈述观测到的事实
38
- * (`system time zone Asia/Shanghai`),不做身份断言(不写 "you are in China")—— 用户看到的
39
- * 是判据本身,于是「这判据不对」是他能当场看出来并改掉的。
40
- */
41
1
  /** 三档。🔴 `unknown` ≠ `intl`:前者是没有判据,后者是有判据且指向境外。 */
42
2
  export type RegionHint = 'cn' | 'intl' | 'unknown';
43
3
  /** 全部输入由调用方注入(Node 侧传 `env.LANG`/`env.LC_ALL` 与 `Intl.DateTimeFormat().resolvedOptions().timeZone`)。 */
@@ -37,7 +37,14 @@
37
37
  * 端会把它渲成「已按你的系统区域预选,可随时改」。所以 reason 只陈述观测到的事实
38
38
  * (`system time zone Asia/Shanghai`),不做身份断言(不写 "you are in China")—— 用户看到的
39
39
  * 是判据本身,于是「这判据不对」是他能当场看出来并改掉的。
40
+ *
41
+ * ## tag 手术委托 `./localeTag.js`(F4 族D A-028.20,双出口成文)
42
+ *
43
+ * 剥 codeset/modifier、中性 locale 认定、子标签切分这台**手术**与 `env/uiLanguage.ts` 共用
44
+ * 同一份实现;但两件各答各的题(本件答「哪个地址更可达」,那件答「界面用哪种文字」),
45
+ * 输出域语义不同,刻意不收敛 —— 理由成文在 localeTag.ts 文件头。
40
46
  */
47
+ import { parseLocaleTag } from './localeTag.js';
41
48
  /**
42
49
  * 判为「内地」的 IANA 时区(含 backward 链接别名与老 `PRC` 形)。
43
50
  * 🔴 `Asia/Hong_Kong` / `Asia/Macau` / `Asia/Taipei` **不在**表内:本模块判的是「哪个镜像地址
@@ -54,10 +61,6 @@ const CN_TIME_ZONES = new Set([
54
61
  ]);
55
62
  /** IANA 时区名的形(`Area/Location`,允许多级如 `America/Argentina/Salta`)。 */
56
63
  const IANA_SHAPE = /^[a-z]+(?:\/[a-z0-9_+-]+)+$/i;
57
- /** 语言标签的形:头一段是 2–3 位字母的语言码。不合形的串(`1234` / 空串)当没信号。 */
58
- const LANGUAGE_TAG_SHAPE = /^[a-z]{2,3}(?:[-_]|$)/i;
59
- /** 显式「无 locale」的 POSIX 值 —— 它们不携带任何地域信息。 */
60
- const NEUTRAL_LOCALES = new Set(['c', 'posix']);
61
64
  /** 不含地理信息的时区(UTC 家族):有值,但不构成判据。 */
62
65
  const GEOLESS_ZONES = new Set(['utc', 'gmt', 'z', 'universal', 'zulu']);
63
66
  /** reason 里回显观测值的长度上限 —— 输入来自宿主环境变量,不该由它决定一行 UI 有多长。 */
@@ -73,11 +76,9 @@ const echo = (v) => (v.length > ECHO_MAX ? `${v.slice(0, ECHO_MAX)}…` : v);
73
76
  function localeTag(input) {
74
77
  const pick = (v) => (typeof v === 'string' ? v.trim() : '');
75
78
  const raw = pick(input.lcAll) || pick(input.lang);
76
- const rawNoCodeset = raw.split('.')[0] ?? '';
77
- const tag = (rawNoCodeset.split('@')[0] ?? '').trim();
78
- if (!tag || NEUTRAL_LOCALES.has(tag.toLowerCase()))
79
- return '';
80
- return LANGUAGE_TAG_SHAPE.test(tag) ? tag : '';
79
+ // tag 手术(剥 codeset/modifier、中性 locale、认形)单源在 localeTag.ts —— 本件只保留
80
+ // 「LC_ALL 独赢」这半句 POSIX 语义,手术本身不再自持第二份。
81
+ return parseLocaleTag(raw).tag;
81
82
  }
82
83
  /**
83
84
  * locale → 信号。`zh` 系按**地区码优先、脚本次之**读(地区码比脚本更接近地理事实:
@@ -86,7 +87,7 @@ function localeTag(input) {
86
87
  function localeSignal(tag) {
87
88
  if (!tag)
88
89
  return 'unknown';
89
- const parts = tag.toLowerCase().replace(/_/g, '-').split('-');
90
+ const parts = parseLocaleTag(tag).subtags;
90
91
  if (parts[0] !== 'zh')
91
92
  return 'intl'; // 非中文 locale = 有信号且不指向内地
92
93
  const rest = parts.slice(1);
@@ -0,0 +1,40 @@
1
+ /**
2
+ * localeTag.ts — F4 族D(A-028.20,2026-08-15):locale 标签的**纯 tag 手术**单源。
3
+ *
4
+ * ## 为什么要有这一件
5
+ *
6
+ * 「把一个 locale 标签剥成语言子标签」这台手术此前被做了三次:cli `i18n/core/uiLanguage.ts`
7
+ * (剥 codeset/modifier → 取语言子标签 → 判 UI 语言)、本包 `env/localeGeo.ts`(同一台手术 →
8
+ * 判地域预选)、web 侧第二份(答案还不一致)。手术步骤漂了(比如谁剥 `@modifier` 谁不剥),
9
+ * 同一个 `zh_CN.UTF-8@pinyin` 会在不同端得出不同判定。手术进包单源后,两个**输出域不同**的
10
+ * 判定(见下)共用这一台手术,步骤从结构上不可能再漂。
11
+ *
12
+ * ## 🔴 双出口成文:tag 手术单源 ≠ 答案单源
13
+ *
14
+ * 本件只做**手术**(剥、切、认形),不做**判定**。判定有两个,输出域**语义不同**,刻意不收敛:
15
+ * · `env/uiLanguage.ts` `pickUiLanguage` → `'en' | 'zh'`:**用户读哪种界面文字**(语言偏好域;
16
+ * 未知回落 en = 回落到可读的 fallback 目录);
17
+ * · `env/localeGeo.ts` `resolveRegionHint` → `'cn' | 'intl' | 'unknown'`:**机器在哪张网上**
18
+ * (地址可达域;未知就是 unknown,绝不猜 —— 端应当不预选)。
19
+ * 两域对同一输入给不同答案是**设计**,不是漂移:`zh-Hant` 的界面语言是中文(zh),而它的网络
20
+ * 预选是境外(intl)。谁要「收敛成一个函数」谁就把这两件事重新搅在一起。
21
+ *
22
+ * ## 手术步骤(POSIX `zh_CN.UTF-8@pinyin` → `zh_CN`)
23
+ *
24
+ * ① trim;② 剥 codeset(`.` 后);③ 剥 modifier(`@` 后)再 trim;
25
+ * ④ 中性 locale(`C`/`POSIX`)与不合语言标签形的串(头段非 2–3 位字母)⇒ **无信号**
26
+ * (`tag: ''`)——「没有 locale」和「locale 指向某语言」必须分得开;
27
+ * ⑤ 子标签统一小写、`_`→`-` 后切开(`tag` 保留原大小写 —— 端要拿它回显观测值)。
28
+ */
29
+ /** 手术产物。`tag === ''` = 无信号(空/中性/形不对),此时 language/subtags 也为空。 */
30
+ export interface ParsedLocaleTag {
31
+ /** 剥 codeset/modifier 后的标签**原样**(保留大小写,供端回显观测值);'' = 无信号。 */
32
+ tag: string;
33
+ /** 语言子标签(小写);无信号时 ''。 */
34
+ language: string;
35
+ /** 全部子标签(小写,[-_] 切);无信号时 []。 */
36
+ subtags: string[];
37
+ }
38
+ /** 纯 tag 手术(见文件头步骤表)。任何宿主形态的标签都吃:POSIX `LANG`、BCP 47、
39
+ * `navigator.language`、Electron `app.getLocale()`。 */
40
+ export declare function parseLocaleTag(raw: string | undefined): ParsedLocaleTag;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * localeTag.ts — F4 族D(A-028.20,2026-08-15):locale 标签的**纯 tag 手术**单源。
3
+ *
4
+ * ## 为什么要有这一件
5
+ *
6
+ * 「把一个 locale 标签剥成语言子标签」这台手术此前被做了三次:cli `i18n/core/uiLanguage.ts`
7
+ * (剥 codeset/modifier → 取语言子标签 → 判 UI 语言)、本包 `env/localeGeo.ts`(同一台手术 →
8
+ * 判地域预选)、web 侧第二份(答案还不一致)。手术步骤漂了(比如谁剥 `@modifier` 谁不剥),
9
+ * 同一个 `zh_CN.UTF-8@pinyin` 会在不同端得出不同判定。手术进包单源后,两个**输出域不同**的
10
+ * 判定(见下)共用这一台手术,步骤从结构上不可能再漂。
11
+ *
12
+ * ## 🔴 双出口成文:tag 手术单源 ≠ 答案单源
13
+ *
14
+ * 本件只做**手术**(剥、切、认形),不做**判定**。判定有两个,输出域**语义不同**,刻意不收敛:
15
+ * · `env/uiLanguage.ts` `pickUiLanguage` → `'en' | 'zh'`:**用户读哪种界面文字**(语言偏好域;
16
+ * 未知回落 en = 回落到可读的 fallback 目录);
17
+ * · `env/localeGeo.ts` `resolveRegionHint` → `'cn' | 'intl' | 'unknown'`:**机器在哪张网上**
18
+ * (地址可达域;未知就是 unknown,绝不猜 —— 端应当不预选)。
19
+ * 两域对同一输入给不同答案是**设计**,不是漂移:`zh-Hant` 的界面语言是中文(zh),而它的网络
20
+ * 预选是境外(intl)。谁要「收敛成一个函数」谁就把这两件事重新搅在一起。
21
+ *
22
+ * ## 手术步骤(POSIX `zh_CN.UTF-8@pinyin` → `zh_CN`)
23
+ *
24
+ * ① trim;② 剥 codeset(`.` 后);③ 剥 modifier(`@` 后)再 trim;
25
+ * ④ 中性 locale(`C`/`POSIX`)与不合语言标签形的串(头段非 2–3 位字母)⇒ **无信号**
26
+ * (`tag: ''`)——「没有 locale」和「locale 指向某语言」必须分得开;
27
+ * ⑤ 子标签统一小写、`_`→`-` 后切开(`tag` 保留原大小写 —— 端要拿它回显观测值)。
28
+ */
29
+ /** 显式「无 locale」的 POSIX 值 —— 它们不携带任何语言/地域信息。 */
30
+ const NEUTRAL_LOCALES = new Set(['c', 'posix']);
31
+ /** 语言标签的形:头一段是 2–3 位字母的语言码。不合形的串(`1234` / 空串)当没信号。 */
32
+ const LANGUAGE_TAG_SHAPE = /^[a-z]{2,3}(?:[-_]|$)/i;
33
+ /** 纯 tag 手术(见文件头步骤表)。任何宿主形态的标签都吃:POSIX `LANG`、BCP 47、
34
+ * `navigator.language`、Electron `app.getLocale()`。 */
35
+ export function parseLocaleTag(raw) {
36
+ const trimmed = typeof raw === 'string' ? raw.trim() : '';
37
+ const noCodeset = trimmed.split('.')[0] ?? '';
38
+ const tag = (noCodeset.split('@')[0] ?? '').trim();
39
+ if (!tag || NEUTRAL_LOCALES.has(tag.toLowerCase()) || !LANGUAGE_TAG_SHAPE.test(tag)) {
40
+ return { tag: '', language: '', subtags: [] };
41
+ }
42
+ const subtags = tag.toLowerCase().replace(/_/g, '-').split('-');
43
+ return { tag, language: subtags[0] ?? '', subtags };
44
+ }
@@ -0,0 +1,18 @@
1
+ /** The languages sema's own screens are available in. */
2
+ export type UiLanguage = 'en' | 'zh';
3
+ /** Menu order: the detected language is pre-focused, so this order is presentation only. */
4
+ export declare const UI_LANGUAGES: readonly UiLanguage[];
5
+ /**
6
+ * Language names in the language itself (endonyms). NEVER translated and never routed through the
7
+ * message catalog: a language picker has to be readable by someone who cannot read the current UI
8
+ * language — that is the whole point of the screen.
9
+ */
10
+ export declare const UI_LANGUAGE_ENDONYMS: Record<UiLanguage, string>;
11
+ export declare function isUiLanguage(v: unknown): v is UiLanguage;
12
+ /**
13
+ * Decide the UI language from ONE locale tag, whatever the host got it from — a POSIX `LANG`
14
+ * (`zh_CN.UTF-8`), a BCP 47 tag (`zh-Hant`), `navigator.language`, an Electron `app.getLocale()`.
15
+ * Anything whose language subtag is `zh` ⇒ 中文; everything else — including empty, `C` and
16
+ * `POSIX` — ⇒ English (which is also the fallback catalog, so "unknown" degrades to "readable").
17
+ */
18
+ export declare function pickUiLanguage(localeTag: string | undefined): UiLanguage;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * uiLanguage.ts — F4 族D(A-028.20,2026-08-15;整体自 cli `src/sema/i18n/core/uiLanguage.ts`
3
+ * 搬迁):UI 语言的**词表 + 判定**——类型、两种受支持语言、各自的 endonym、以及
4
+ * 「一个 locale 标签 → UI 语言」的纯判定。三端(CLI / web / 桌面)共用这一份。
5
+ *
6
+ * tag 手术(剥 codeset/modifier、切子标签)委托 `./localeTag.js` —— 与 `./localeGeo.js` 共用
7
+ * **同一台手术**但**各答各的题**(双出口成文,理由见 localeTag.ts 文件头:语言偏好域 ≠ 地址
8
+ * 可达域,`zh-Hant` 在前者是 zh、在后者是 intl)。
9
+ *
10
+ * 标签从哪来是宿主的事,本件不读环境:CLI 读 POSIX env(LC_ALL > LC_MESSAGES > LANG),
11
+ * 浏览器读 `navigator.language`,Electron 读 `app.getLocale()` —— 各宿主的三行 adapter 都
12
+ * 调同一个 `pickUiLanguage`。
13
+ */
14
+ import { parseLocaleTag } from './localeTag.js';
15
+ /** Menu order: the detected language is pre-focused, so this order is presentation only. */
16
+ export const UI_LANGUAGES = ['en', 'zh'];
17
+ /**
18
+ * Language names in the language itself (endonyms). NEVER translated and never routed through the
19
+ * message catalog: a language picker has to be readable by someone who cannot read the current UI
20
+ * language — that is the whole point of the screen.
21
+ */
22
+ export const UI_LANGUAGE_ENDONYMS = {
23
+ en: 'English',
24
+ zh: '中文',
25
+ };
26
+ export function isUiLanguage(v) {
27
+ return v === 'en' || v === 'zh';
28
+ }
29
+ /**
30
+ * Decide the UI language from ONE locale tag, whatever the host got it from — a POSIX `LANG`
31
+ * (`zh_CN.UTF-8`), a BCP 47 tag (`zh-Hant`), `navigator.language`, an Electron `app.getLocale()`.
32
+ * Anything whose language subtag is `zh` ⇒ 中文; everything else — including empty, `C` and
33
+ * `POSIX` — ⇒ English (which is also the fallback catalog, so "unknown" degrades to "readable").
34
+ */
35
+ export function pickUiLanguage(localeTag) {
36
+ return parseLocaleTag(localeTag).language === 'zh' ? 'zh' : 'en';
37
+ }
package/dist/index.d.ts CHANGED
@@ -242,6 +242,10 @@ export * from './agentSession/backgroundView.js';
242
242
  export * from './model/catalog.js';
243
243
  export * from './model/catalogLoader.js';
244
244
  export * from './model/providerAuth.js';
245
+ export * from './model/providerCatalog.js';
246
+ export * from './model/tierVocabulary.js';
245
247
  export * from './websearch/searchProviderPresets.js';
246
248
  export * from './env/localeGeo.js';
249
+ export * from './env/localeTag.js';
250
+ export * from './env/uiLanguage.js';
247
251
  export type { PendingCheckpoint, RuleSuggestion } from '@sema-agent/sdk';
package/dist/index.js CHANGED
@@ -416,6 +416,14 @@ export * from './model/catalogLoader.js';
416
416
  // 填别的编辑器插件的 app id = 冒用,与 §6 判 C 档出局同一条判据),因此
417
417
  // `supportsDeviceCodeAuth` 对每一家如实返回 false、UI 不显示该选项 —— 注册一家 = 加一行。
418
418
  export * from './model/providerAuth.js';
419
+ // ── F4 族D(A-028.17/.18,2026-08-15):配置/模型表单的**折表与词表**单源 ─────────────────────
420
+ // 🔴 `model/providerCatalog.ts` = 46 家表的规范折表层(表→目录行的那一次投影 + 行级文案判定)。
421
+ // 此前 cli/web 各折各的(共三份),两侧常驻全表门靠人工对齐。三条约束成文在文件头:
422
+ // 单一真源现算 / 全表不重排不过滤 / 诚实缺席(零模型家说 "model id typed in" 不编 "0 models")。
423
+ export * from './model/providerCatalog.js';
424
+ // 🔴 `model/tierVocabulary.ts` = 档位五档词表 + CC 别名 + isTier 校验 + resolveTierBinding
425
+ // fail-open 降档派生。此前跨两仓三份;settings 读写那半场仍归宿主(cli tierCore 只留存储)。
426
+ export * from './model/tierVocabulary.js';
419
427
  // ── 搜索 provider 目录 + 地域预选(2026-07-31)──────────────────────────────────────────────────
420
428
  // `websearch/searchProviderPresets` = model 目录的**同形不同表**姊妹件:数据在 json、类型与查询
421
429
  // 在 ts。它编译出来的不是模型目录而是**引擎部署 env**(`WEB_SEARCH_*`)。
@@ -430,3 +438,10 @@ export * from './websearch/searchProviderPresets.js';
430
438
  // `src/model/**` 也不许 import 它(pure 门 GEO 段有机械断言)。合法用途只有搜索 provider /
431
439
  // 镜像源地址这类「取哪个地址更可达」的**默认值**预选。
432
440
  export * from './env/localeGeo.js';
441
+ // ── F4 族D(A-028.20,2026-08-15):locale tag 手术单源 + UI 语言词表/判定 ────────────────────
442
+ // 🔴 双出口成文(localeTag.ts 文件头):tag **手术**(剥 codeset/modifier、切子标签)单源一份;
443
+ // **判定**两个且输出域语义不同,刻意不收敛 —— `pickUiLanguage` 答「界面用哪种文字」
444
+ // (en/zh,未知回落可读的 en),`resolveRegionHint` 答「哪个地址更可达」(cn/intl/unknown,
445
+ // 未知绝不猜)。`zh-Hant` 在前者是 zh、在后者是 intl —— 这是设计,不是漂移。
446
+ export * from './env/localeTag.js';
447
+ export * from './env/uiLanguage.js';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * providerCatalog.ts — F4 族D(A-028.17,2026-08-15):46 家 provider 表的**规范折表层**
3
+ * (canonical fold)。纯派生、零 IO、零宿主依赖 —— 表本体在 `providerPresets.json`,
4
+ * 本件只做「表 → 目录行」的那一次投影与行级文案判定。
5
+ *
6
+ * ## 为什么折表要进包
7
+ *
8
+ * 搬迁前这张表被折了三次:cli `src/sema/config/providerDirectory.ts`(Model Hub 目录行)、
9
+ * web `src/lib/providers/catalog.ts`(向导折表)、web `provider-catalog.ts`(降级表二次映射)。
10
+ * 三份折表各自持有「host 怎么取 / 零模型家怎么说 / 顺序动不动」这些**判定**,两侧常驻全表门
11
+ * 只能靠人工对齐 —— 判定漂了没有任何编译器会响。折表进包后:cli 换装 re-export(单一真源),
12
+ * web 侧第二份折表退役是 web 半场的事(发布帖点名)。
13
+ *
14
+ * ## 三条继承自 cli #175 的设计约束(逐字保留语义)
15
+ *
16
+ * ① **单一真源**:每个量都从 `PROVIDER_PRESETS` 现算,本文件里没有第二张表 —— 没有硬编码的
17
+ * 家名/baseUrl/模型清单。表更新一次,所有端的目录跟着变。
18
+ * ② **全表不重排**:顺序 = 表序,一行不筛、不重排、不分组(clay 令 2026-08-01:模型 provider
19
+ * 完全用户选择,走全表)。本件不导出任何排序/过滤函数。
20
+ * ③ **诚实缺席**:表里没登记模型列表的家(ollama / vllm / litellm / 聚合网关等)`modelIds`
21
+ * 就是空数组,行文案说「model id typed in」,**不写「0 models」**——「0 个模型」是编造
22
+ * 出来的量:表没登记 ≠ 这家没有模型。consoleUrl / signupUrl 缺席同样原样缺席,不回落编造。
23
+ *
24
+ * 行形是 cli DirectoryRow 与 web 折表两侧字段的**严格超集**(字段名与 cli 侧逐字一致,cli 直接
25
+ * re-export 零改动;web 侧的 label/keyEnvSuggestion/suggestedModels 对应 name/authEnv/modelIds)。
26
+ */
27
+ import { type ProviderPreset } from './providerPresets.js';
28
+ /** 目录/向导的一行(表的一次纯投影;字段全部现算,invariant:modelCount === modelIds.length)。 */
29
+ export interface ProviderCatalogRow {
30
+ /** preset 表里的 id(行 key + 回查用) */
31
+ presetId: string;
32
+ name: string;
33
+ baseUrl: string;
34
+ /** baseUrl 的 host(含端口);解析不出时给空串(诚实缺席,不猜) */
35
+ host: string;
36
+ api: ProviderPreset['api'];
37
+ /** 领 key 的环境变量**名**(绝不是值;web 侧的 keyEnvSuggestion) */
38
+ authEnv: string;
39
+ /** 表里登记了的模型 id(原序原样;空数组 = 这家的 model id 由用户手填,见约束③) */
40
+ modelIds: string[];
41
+ /** 表里**登记了**的模型条数(0 = model id 手填,不是「这家没有模型」) */
42
+ modelCount: number;
43
+ /** 聚合网关/本地引擎:模型 id 手填,ctx/maxTokens 走家族表 */
44
+ modelViaFamily: boolean;
45
+ /** 去哪拿 key 的控制台页(纯 URL);缺席 = 上游没有确证地址,不编 */
46
+ consoleUrl?: string;
47
+ /** 没账号时的注册入口;缺席 = 用 consoleUrl 那条 */
48
+ signupUrl?: string;
49
+ }
50
+ /** api 家族的行内短名(目录行/芯片行两端同一个词,不各自缩写)。 */
51
+ export declare const providerApiShortName: (api: ProviderPreset["api"]) => string;
52
+ /**
53
+ * 全表 → 目录行。**顺序 = 表序**(约束②):这里没有 filter,也没有 sort。
54
+ * `presets` 入参只给测试用(注入一张小表),产品路径恒走 `PROVIDER_PRESETS`。
55
+ */
56
+ export declare function providerCatalogRows(presets?: readonly ProviderPreset[]): ProviderCatalogRow[];
57
+ /** 目录规模。端上「家数」与目录行数同源 —— 两处不会各报一个数。 */
58
+ export declare function providerCatalogCount(): number;
59
+ /** 按 preset id 回查整条 preset(目录选中之后要拿 baseUrl / models / consoleUrl)。 */
60
+ export declare function providerPresetById(presetId: string): ProviderPreset | undefined;
61
+ /**
62
+ * 一行的 dim 说明:`host · api · 模型情况`。
63
+ * 约束③:`modelCount === 0` 说「model id typed in」,绝不写「0 models」。
64
+ */
65
+ export declare function providerCatalogRowDetail(row: Pick<ProviderCatalogRow, 'host' | 'baseUrl' | 'api' | 'modelCount' | 'modelViaFamily'>): string;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * providerCatalog.ts — F4 族D(A-028.17,2026-08-15):46 家 provider 表的**规范折表层**
3
+ * (canonical fold)。纯派生、零 IO、零宿主依赖 —— 表本体在 `providerPresets.json`,
4
+ * 本件只做「表 → 目录行」的那一次投影与行级文案判定。
5
+ *
6
+ * ## 为什么折表要进包
7
+ *
8
+ * 搬迁前这张表被折了三次:cli `src/sema/config/providerDirectory.ts`(Model Hub 目录行)、
9
+ * web `src/lib/providers/catalog.ts`(向导折表)、web `provider-catalog.ts`(降级表二次映射)。
10
+ * 三份折表各自持有「host 怎么取 / 零模型家怎么说 / 顺序动不动」这些**判定**,两侧常驻全表门
11
+ * 只能靠人工对齐 —— 判定漂了没有任何编译器会响。折表进包后:cli 换装 re-export(单一真源),
12
+ * web 侧第二份折表退役是 web 半场的事(发布帖点名)。
13
+ *
14
+ * ## 三条继承自 cli #175 的设计约束(逐字保留语义)
15
+ *
16
+ * ① **单一真源**:每个量都从 `PROVIDER_PRESETS` 现算,本文件里没有第二张表 —— 没有硬编码的
17
+ * 家名/baseUrl/模型清单。表更新一次,所有端的目录跟着变。
18
+ * ② **全表不重排**:顺序 = 表序,一行不筛、不重排、不分组(clay 令 2026-08-01:模型 provider
19
+ * 完全用户选择,走全表)。本件不导出任何排序/过滤函数。
20
+ * ③ **诚实缺席**:表里没登记模型列表的家(ollama / vllm / litellm / 聚合网关等)`modelIds`
21
+ * 就是空数组,行文案说「model id typed in」,**不写「0 models」**——「0 个模型」是编造
22
+ * 出来的量:表没登记 ≠ 这家没有模型。consoleUrl / signupUrl 缺席同样原样缺席,不回落编造。
23
+ *
24
+ * 行形是 cli DirectoryRow 与 web 折表两侧字段的**严格超集**(字段名与 cli 侧逐字一致,cli 直接
25
+ * re-export 零改动;web 侧的 label/keyEnvSuggestion/suggestedModels 对应 name/authEnv/modelIds)。
26
+ */
27
+ import { PROVIDER_PRESETS } from './providerPresets.js';
28
+ const hostOf = (u) => {
29
+ try {
30
+ return new URL(u).host;
31
+ }
32
+ catch {
33
+ return '';
34
+ }
35
+ };
36
+ /** api 家族的行内短名(目录行/芯片行两端同一个词,不各自缩写)。 */
37
+ export const providerApiShortName = (api) => api === 'anthropic-messages' ? 'anthropic' : 'openai';
38
+ /**
39
+ * 全表 → 目录行。**顺序 = 表序**(约束②):这里没有 filter,也没有 sort。
40
+ * `presets` 入参只给测试用(注入一张小表),产品路径恒走 `PROVIDER_PRESETS`。
41
+ */
42
+ export function providerCatalogRows(presets = PROVIDER_PRESETS) {
43
+ return presets.map(p => ({
44
+ presetId: p.id,
45
+ name: p.name,
46
+ baseUrl: p.baseUrl,
47
+ host: hostOf(p.baseUrl),
48
+ api: p.api,
49
+ authEnv: p.authEnv,
50
+ modelIds: p.models.map(m => m.id),
51
+ modelCount: p.models.length,
52
+ modelViaFamily: p.modelViaFamily === true,
53
+ ...(p.consoleUrl === undefined ? {} : { consoleUrl: p.consoleUrl }),
54
+ ...(p.signupUrl === undefined ? {} : { signupUrl: p.signupUrl }),
55
+ }));
56
+ }
57
+ /** 目录规模。端上「家数」与目录行数同源 —— 两处不会各报一个数。 */
58
+ export function providerCatalogCount() {
59
+ return providerCatalogRows().length;
60
+ }
61
+ /** 按 preset id 回查整条 preset(目录选中之后要拿 baseUrl / models / consoleUrl)。 */
62
+ export function providerPresetById(presetId) {
63
+ return PROVIDER_PRESETS.find(p => p.id === presetId);
64
+ }
65
+ /**
66
+ * 一行的 dim 说明:`host · api · 模型情况`。
67
+ * 约束③:`modelCount === 0` 说「model id typed in」,绝不写「0 models」。
68
+ */
69
+ export function providerCatalogRowDetail(row) {
70
+ const models = row.modelCount === 0
71
+ ? 'model id typed in'
72
+ : `${row.modelCount} model${row.modelCount === 1 ? '' : 's'}${row.modelViaFamily ? ' · or any id' : ''}`;
73
+ return [row.host || row.baseUrl, providerApiShortName(row.api), models].join(' · ');
74
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * tierVocabulary.ts — F4 族D(A-028.18,2026-08-15):档位(tier)的**词表 + 派生 + 校验**单源。
3
+ * 纯数据 + 纯函数,零 IO、零宿主依赖。
4
+ *
5
+ * ## 为什么进包
6
+ *
7
+ * 搬迁前这套词表跨两仓共三份:cli `src/sema/config/tierCore.ts`(词表 + isTier 校验 +
8
+ * resolveTierBinding 派生)、web `tiers-page.tsx`(词表)、web `chat/wording.ts`(词表 + 按
9
+ * 模型名 token 化的派生)。五档顺序/别名映射漂了没有任何编译器会响。词表进包后:cli 的
10
+ * tierCore 换装 re-export(settings 读写那半场留在壳里 —— 那是宿主存储,不是词表);web 侧
11
+ * 两份的退役是 web 半场的事(发布帖点名)。
12
+ *
13
+ * ## 与引擎侧的对齐
14
+ *
15
+ * 五档序镜像 core `DEFAULT_TIER_ORDER`、CC 别名镜像 core `CC_MODEL_TIER_ALIASES`(cli 侧
16
+ * tierStore.test.ts 有对 core 导出的运行时 drift-lock);`ultra` 预留在 max 之上(core 已排位,
17
+ * UI 今天不面)。缺省默认档 pro(clay [599])。
18
+ */
19
+ /** sema 档位(用户/模型可寻址的命名档)。`ultra` 预留(core 已排位,UI 今天不面)。 */
20
+ export type Tier = 'ultra' | 'max' | 'pro' | 'flash' | 'lite';
21
+ /** 降档链(强→弱)——镜像 core DEFAULT_TIER_ORDER;解析某档无绑定时沿链向后取第一个有绑定的档。 */
22
+ export declare const TIER_ORDER: readonly Tier[];
23
+ /** UI 呈现的四档(ultra 预留不面)。 */
24
+ export declare const SURFACED_TIERS: readonly Tier[];
25
+ /** CC 档位词 → sema 档位(内置固定映射,镜像 core CC_MODEL_TIER_ALIASES;兼容层不可配)。 */
26
+ export declare const CC_TIER_ALIASES: Readonly<Record<string, Tier>>;
27
+ /** 缺省默认档(clay [599]:默认档缺省 pro)。 */
28
+ export declare const DEFAULT_TIER: Tier;
29
+ /** 各档一句话职责(档位 Hub/onboard 行描述共用;无 roster/role 词)。 */
30
+ export declare const TIER_BLURBS: Readonly<Record<Tier, string>>;
31
+ /** 一个命名档位组的绑定形(纯类型:绑定值 = 端侧模型目录的 entry id)。 */
32
+ export type TierGroupDef = {
33
+ /** tier → 模型 entry id。未绑档沿 TIER_ORDER 向下 fail-open(见 resolveTierBinding)。 */
34
+ tiers: Partial<Record<Tier, string>>;
35
+ /** 组内默认档(缺省 DEFAULT_TIER=pro)。 */
36
+ defaultTier?: Tier;
37
+ };
38
+ export declare const isTier: (v: unknown) => v is Tier;
39
+ /**
40
+ * 组内解析某档 → 绑定 entry id(该档未绑则沿 TIER_ORDER 向下取第一个有绑定的档;全空
41
+ * undefined)。fail-open 沿链向下,镜像 core expandTiers 语义。
42
+ */
43
+ export declare function resolveTierBinding(def: TierGroupDef, tier: Tier): string | undefined;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * tierVocabulary.ts — F4 族D(A-028.18,2026-08-15):档位(tier)的**词表 + 派生 + 校验**单源。
3
+ * 纯数据 + 纯函数,零 IO、零宿主依赖。
4
+ *
5
+ * ## 为什么进包
6
+ *
7
+ * 搬迁前这套词表跨两仓共三份:cli `src/sema/config/tierCore.ts`(词表 + isTier 校验 +
8
+ * resolveTierBinding 派生)、web `tiers-page.tsx`(词表)、web `chat/wording.ts`(词表 + 按
9
+ * 模型名 token 化的派生)。五档顺序/别名映射漂了没有任何编译器会响。词表进包后:cli 的
10
+ * tierCore 换装 re-export(settings 读写那半场留在壳里 —— 那是宿主存储,不是词表);web 侧
11
+ * 两份的退役是 web 半场的事(发布帖点名)。
12
+ *
13
+ * ## 与引擎侧的对齐
14
+ *
15
+ * 五档序镜像 core `DEFAULT_TIER_ORDER`、CC 别名镜像 core `CC_MODEL_TIER_ALIASES`(cli 侧
16
+ * tierStore.test.ts 有对 core 导出的运行时 drift-lock);`ultra` 预留在 max 之上(core 已排位,
17
+ * UI 今天不面)。缺省默认档 pro(clay [599])。
18
+ */
19
+ /** 降档链(强→弱)——镜像 core DEFAULT_TIER_ORDER;解析某档无绑定时沿链向后取第一个有绑定的档。 */
20
+ export const TIER_ORDER = ['ultra', 'max', 'pro', 'flash', 'lite'];
21
+ /** UI 呈现的四档(ultra 预留不面)。 */
22
+ export const SURFACED_TIERS = ['max', 'pro', 'flash', 'lite'];
23
+ /** CC 档位词 → sema 档位(内置固定映射,镜像 core CC_MODEL_TIER_ALIASES;兼容层不可配)。 */
24
+ export const CC_TIER_ALIASES = {
25
+ fable: 'max',
26
+ mythos: 'max',
27
+ opus: 'pro',
28
+ sonnet: 'flash',
29
+ haiku: 'lite',
30
+ };
31
+ /** 缺省默认档(clay [599]:默认档缺省 pro)。 */
32
+ export const DEFAULT_TIER = 'pro';
33
+ /** 各档一句话职责(档位 Hub/onboard 行描述共用;无 roster/role 词)。 */
34
+ export const TIER_BLURBS = {
35
+ ultra: 'reserved above max — binds when a stronger model lands',
36
+ max: 'hardest problems — deep reasoning, big refactors',
37
+ pro: 'everyday driving — the default tier',
38
+ flash: 'fast + cheap — titles, recaps, volume work',
39
+ lite: 'cheapest — bulk classification, simple lookups',
40
+ };
41
+ export const isTier = (v) => typeof v === 'string' && TIER_ORDER.includes(v);
42
+ /**
43
+ * 组内解析某档 → 绑定 entry id(该档未绑则沿 TIER_ORDER 向下取第一个有绑定的档;全空
44
+ * undefined)。fail-open 沿链向下,镜像 core expandTiers 语义。
45
+ */
46
+ export function resolveTierBinding(def, tier) {
47
+ const direct = def.tiers[tier];
48
+ if (direct)
49
+ return direct;
50
+ const at = TIER_ORDER.indexOf(tier);
51
+ if (at === -1)
52
+ return undefined;
53
+ for (let i = at + 1; i < TIER_ORDER.length; i++) {
54
+ const next = def.tiers[TIER_ORDER[i]];
55
+ if (next)
56
+ return next;
57
+ }
58
+ return undefined;
59
+ }
@@ -23,7 +23,7 @@
23
23
  | peer:wire 契约 | `@sema-agent/sdk` **>=6.17.2**(value-level,非 type-only) | `package.json` `peerDependencies` |
24
24
  | peer:会话词汇表 | `@sema-agent/agent-types` **>=0.2.0**(type-only,零运行时) | 同上 |
25
25
  | runtime dep | `diff` ^9.0.0(**唯一**一条;portability 门按**等值**钉死) | `package.json` `dependencies` |
26
- | 公开导出面 | **707** 个运行期符号(+ 33 个测试钩) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
26
+ | 公开导出面 | **724** 个运行期符号(+ 33 个测试钩) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
27
27
  | 常驻门 | 以 `scripts/gates-manifest.json` 的 `suites` 长度为准(**本档不抄这个数**) | `scripts/gates-manifest.json`;`npm test` 的名单等值门与它逐名对账 |
28
28
  | 沿革档 | 0.29.0 起建 `CHANGELOG.md`;更早批次记账在 `src/index.ts` 文件头 + `docs/REFACTOR-LEDGER.md` | — |
29
29
 
@@ -99,7 +99,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
99
99
 
100
100
  ## §2 公共导出面地图(按域)
101
101
 
102
- > 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**707** 项)。
102
+ > 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**724** 项)。
103
103
  > 本节**不逐名抄**,只给「域 → 承重导出 → 用途 → 实现锚」。承重导出 = 一个端为了让这个域干活
104
104
  > **必须**直接调到的那几个符号;其余是它们的类型、变体与辅助位。
105
105
  > 单一入口:`import { … } from '@sema-agent/client-core'`(`exports` 只有 `.` 一个;
@@ -109,7 +109,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
109
109
 
110
110
  `public-export-baseline.json` 由 **`dist/index.js` 的运行期导出**生成(生成口径自述见
111
111
  `scripts/run-client-core-typeshape-test.mjs`,双向精确集合门在 `scripts/run-public-surface-test.mjs`)。
112
- 实测:707 项 **100% 是运行期导出,零 type-only**。
112
+ 实测:724 项 **100% 是运行期导出,零 type-only**。
113
113
 
114
114
  **推论(端必须知道)**:
115
115
  - barrel 导出的**类型**面比 707 大得多,且**不被这道门看守** —— `AdapterContext` / `SeamEvent` /
@@ -118,12 +118,12 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
118
118
  端依赖这些类型是合法的,但**不要**拿基线 diff 当"类型面没变"的证据。
119
119
  - `src/agentSession/contract.ts` 对基线贡献 **0** 项(纯类型模块,`export *` 在 dist 里是空转发)。
120
120
 
121
- 707 项的内部构成(帮助端估读表大小):**210** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
121
+ 724 项的内部构成(帮助端估读表大小):**217** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
122
122
  (矩阵、键集、env 名、锚串)而非可调用物;**4** 项是 PascalCase 运行期值
123
123
  (`ControlRouter` / `ControlSafetyError` / `HitlBridge` / `HitlSafetyError`);
124
124
  **39** 项是 `*For(sessionKey, …)` 的 per-session 变体(§6;其中 `engineNamespaceKeyFor` 是命名巧合 —— 参数是 baseUrl 不是 sessionKey,见域 14)。
125
125
 
126
- ### 2b. 域图(16 域,逐域计数之和 = 707)
126
+ ### 2b. 域图(16 域,逐域计数之和 = 724)
127
127
 
128
128
  | # | 域 | 名数 | 承重导出 | 用途 | 实现锚 |
129
129
  |---|---|---|---|---|---|
@@ -137,10 +137,10 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
137
137
  | 8 | **工具结果卡** | 25 | `structuredToToolUseResult` · `readAsyncLaunchedAgentReceipt` · `wireOutputToBody` · `parseModelFacingBash` · `getPatchFromContents` · `toolEndResultToUserFrame` · `flattenToolOutput` | 铸端要渲的 `tool_result` 卡体,含客户端 diff hunk(唯一 runtime dep 的用处) | `src/toolResult.ts`、`src/printToolResultFrame.ts`、`src/diff/patch.ts` |
138
138
  | 9 | **能力/旋钮 wire 门族** | 86 | `projectAgentsForWire` / `awaitTaskAgentsWire` · `hooksForWire` · `mcpConfigsToSpecs` · `ultracodeForRequest` · `resolveWebSearch` + `buildWebSearchEnv` · `attachmentsForRequest` · `projectEffectiveBody` | 逐功能的「引擎有没有这个能力 + 这个字段怎么上 wire」投影器,由 `buildTaskRequest` 组装。🔴 `hooksForWire()` 是 **fail-closed**:无 `SettingsPort` ⇒ 返回 `undefined`(信任门,不是取值) | 17 个 `src/*WireCaps.ts` + `src/scratchpadWireCaps.ts`、`src/hooksWireCaps.ts`、`src/goalStopHook.ts`、`src/websearch/searchProviderPresets.ts` |
139
139
  | 10 | **headless / 部署旋钮 wire** | 61 | `parseSandboxArgv` / `sandboxRequestFields` · `parseLimitsArgv` / `limitsForPrint` · `resolveHeadlessFinalVerify` · `resolveHeadlessPermissionMode` · `resolveHeadlessInteractiveTools` · `armDetachCancel` + `detachCancelArm` + `isDetachArmed` · `withHeadlessR1Reconnect` | `-p`/headless 车道的 env+argv 旋钮。🔴 `detachWire` 是**拆**的补偿:判定与 cancel-arm 台账在库,信号路径的裸 fetch 留宿主(`detachCancelArm()` 是取件口) | `src/sandboxWire.ts`、`scenarioWire.ts`、`finalVerifyWire.ts`、`limitsWire.ts`、`interactiveToolsWire.ts`、`headlessPermissionModeWire.ts`、`headlessReconnectWire.ts`、`detachWire.ts` |
140
- | 11 | **模型目录与预算** | 52 | `resolveModelCatalog` · `loadCatalogWithSources` · `PROVIDER_PRESETS` / `MODEL_FAMILIES` · `defaultMaxTokensFor` · `getLiveModelCatalog` / `setLiveModelCatalogRefresher` · `providerAuthMethods` / `beginDeviceCodeAuth` | 三层 provider 目录解析(线上 URL → 包内预设 → 用户覆盖)+ per-model `maxTokens` 封顶。线上腿需注入 `CatalogFetchJson`,缺席 ⇒ 整条不启用(`online.reason='no-fetch-port'`);缓存落盘经 `CatalogCachePort` | `src/model/{catalog,catalogLoader,providerAuth,providerPresets}.ts`、`src/liveModelCatalog.ts`、`src/modelBudgetRule.ts`、`src/sessionModelLatch.ts`、`src/effortWire.ts` |
140
+ | 11 | **模型目录与预算** | 64 | `resolveModelCatalog` · `loadCatalogWithSources` · `PROVIDER_PRESETS` / `MODEL_FAMILIES` · `defaultMaxTokensFor` · `getLiveModelCatalog` / `setLiveModelCatalogRefresher` · `providerAuthMethods` / `beginDeviceCodeAuth` · `providerCatalogRows` / `providerCatalogRowDetail` / `providerPresetById`(#244 F4 族D A-028.17:46 家表的规范折表层 —— 全表不重排、诚实缺席「model id typed in」,cli 目录/web 向导同一份折表)· `TIER_ORDER` / `CC_TIER_ALIASES` / `isTier` / `resolveTierBinding`(A-028.18:档位词表+校验+fail-open 降档派生单源;settings 存储归宿主) | 三层 provider 目录解析(线上 URL → 包内预设 → 用户覆盖)+ per-model `maxTokens` 封顶。线上腿需注入 `CatalogFetchJson`,缺席 ⇒ 整条不启用(`online.reason='no-fetch-port'`);缓存落盘经 `CatalogCachePort` | `src/model/{catalog,catalogLoader,providerAuth,providerPresets,providerCatalog,tierVocabulary}.ts`、`src/liveModelCatalog.ts`、`src/modelBudgetRule.ts`、`src/sessionModelLatch.ts`、`src/effortWire.ts` |
141
141
  | 12 | **workflow 与后台工作视图** | 15 | `projectWorkflowRun` · `createLiveWorkflowSource` · `createBackgroundView` · `projectBackgroundView` · `recordWorkflowAgentTaskId` · `agentDisplayStatus` | 活过一个 turn 的长任务读面:workflow run + 跨 session 后台任务归一表(`assistant.tasks` 与 fleet SSE **两源独立降级**) | `src/workflow.ts`、`src/workflowClient.ts`、`src/workflowMonitor.ts`、`src/agentSession/backgroundView.ts`(+ 纯类型 `src/agentSession/contract.ts`) |
142
142
  | 13 | **座位 IPC 契约** | 33 | `LOCAL_SESSIONS_SPEC` · `SEAT_METHOD_NAMES` · `SEAT_EVENT_TYPES` · `isLocalSessionEvent` · `isToolPermissionRequest` · `toolPermissionRequestId` · `SEAT_VALIDATOR_KEY_COVERAGE` | desktop↔web 座位 IPC 契约的**单一真源**(此前两边各一份、名字零重合 ⇒ 编译器永远不会告诉你它们漂了)。🔴 加 verb 忘了加 `LOCAL_SESSIONS_SPEC` **不报错**:preload 不注册 channel、渲染端读到 `undefined` | `src/seatContract.ts`(**零 import**,纯类型 + 常量 + 纯谓词) |
143
- | 14 | **宿主端口与会话槽** | 21 | `installHost` · `installHostFor` · `hostPortMisses(For)` · `DEFAULT_SESSION_KEY` · `hostEnv` · `unrefTimer` · `engineNamespaceKeyFor` / `mergeSessionMapRecord` / `mergeEngineEntry`(A-028.12:会话 id 映射单一键形 + merge 判定;存储经 `SessionMapStorePort` 归端 —— cli 文件锁/原子写,web localStorage)| 进程/端级装配层(settings/fs/queue/timers/session/log/probe),与 per-turn 的 `AdapterContext` **分层**。头注的判定规则:**这个能力每 turn 都会变吗?** 会 ⇒ `ctx`;不会 ⇒ `installHost` | `src/host.ts`、`src/hostEnv.ts`、`src/sessionSlot.ts`、`src/unrefTimer.ts`、`src/env/localeGeo.ts`、`src/sessionMap.ts` |
143
+ | 14 | **宿主端口与会话槽** | 26 | `installHost` · `installHostFor` · `hostPortMisses(For)` · `DEFAULT_SESSION_KEY` · `hostEnv` · `unrefTimer` · `parseLocaleTag` / `pickUiLanguage`(#244 F4 族D A-028.20:locale tag 手术单源 + UI 语言判定;与 `resolveRegionHint` 双出口成文 —— 语言偏好域 en/zh ≠ 地址可达域 cn/intl/unknown,`zh-Hant` 前者 zh 后者 intl 是设计)· `engineNamespaceKeyFor` / `mergeSessionMapRecord` / `mergeEngineEntry`(A-028.12:会话 id 映射单一键形 + merge 判定;存储经 `SessionMapStorePort` 归端 —— cli 文件锁/原子写,web localStorage)| 进程/端级装配层(settings/fs/queue/timers/session/log/probe),与 per-turn 的 `AdapterContext` **分层**。头注的判定规则:**这个能力每 turn 都会变吗?** 会 ⇒ `ctx`;不会 ⇒ `installHost` | `src/host.ts`、`src/hostEnv.ts`、`src/sessionSlot.ts`、`src/unrefTimer.ts`、`src/env/{localeGeo,localeTag,uiLanguage}.ts`、`src/sessionMap.ts` |
144
144
  | 15 | **控制面与传输** | 68 | `ControlRouter`(+ `ControlSafetyError`)· `makeEngineWireClient` + `resolveWireAuth` · `installEngineWireTarget` · `diagnoseSseIdleTear` / `isSseIdleError` · `attemptActiveRunSelfHeal` + `activeRunBusySignal` + `activeRunSelfHealRow` / `activeRunBusyHeadlessRow` · `kickEngineCapsProbe` / `engineCapTrue` · `mapBrainStatusToRetry` · `waitForClaimRelease` + `CLAIM_RELEASED_STATES` / `CLAIM_HELD_STATES` · `atMostOnceFailureClass` / `readSteerDelivery` · `INTERACTIVE_WAY_OUT`(默认出路串单源)· `normalizeWirePrincipal`(A-028.10:principal 在场性 trim 原语 —— 全空白=缺席不发头,engineWireTarget 两臂/makeEngineWireClient/壳 livePrincipal 同尺)· `classifyTurnWireError` / `isWireTransportError` / `isPreStreamDrainingReject` / `isResumeAtRejection` / `drainingRetryDelayMs` / `scenarioDenyFromError` + `WIRE_NETWORK_ERROR_PATTERN`(A-028.11/.13:turn 错误分型判定半场,人话文案与渲染归端) | 上行通道的**监管**半场(submit / steer / kill / 队列命令定序)+ 传输构造、caps 探测、SSE 断流分诊、**409 active-run 自愈** | `src/controlRouter.ts`、`steering.ts`、`sseIdleTriage.ts`、`retryStatus.ts`、`diagnostics.ts`、`engineWireSdk.ts`、`engineWireTarget.ts`、`src/principalWire.ts`、`src/wireErrorTriage.ts`、`engineSessionParam.ts`、`engineCapsCache.ts`、`liveInitToolFace.ts`、`adapter/activeRunSelfHeal.ts` |
145
145
  | 16 | **引擎词汇表与包自检** | 39 | `CONFIG_REFUSAL_CODES` / `isConfigRefusalCode` · `STOP_CONFLICT_CODES` · `isInterruptedToolEndCode` · `isRewindFamilyCode` · `CLIENT_VERBS` · `compensationSplitViolations` | 三端分臂共用的**去字面化** `errorCode` 词表(病根正是三端各抄一份字面);编译期 verb 闭合门;搬迁补偿登记表 | `src/engineErrorCodes.ts`(32 项;A-028.11/.13 补 `DRAINING_ERROR_CODE`/`SCENARIO_NOT_ALLOWED_ERROR_CODE`/`RESUME_AT_ERROR_CODE_PREFIX`)、`src/classifierVerdictWire.ts`、`src/compensations.ts`、`src/clientSlice.ts` |
146
146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.30.4",
3
+ "version": "0.30.5",
4
4
  "description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Blackboard [1832] design axioms; [1651]/[1652]/[1653] signed seam design. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
5
5
  "license": "MIT",
6
6
  "type": "module",