@epoch-agent/protocol 0.1.0 → 0.2.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.
- package/README.md +37 -34
- package/dist/index.d.ts +2768 -106
- package/dist/index.js +483 -101
- package/package.json +4 -2
package/dist/index.d.ts
CHANGED
|
@@ -114,12 +114,18 @@ interface PlanProposal {
|
|
|
114
114
|
/**
|
|
115
115
|
* 计划审批独有的三个出口。第四个出口是 `deny`(拒绝),与工具审批**共用** ——
|
|
116
116
|
* 「不批」这件事在两种审批里语义相同,没必要有两个名字。
|
|
117
|
+
*
|
|
118
|
+
* ⚠️ **这里只有码,没有文案。** 四个出口的显示文字在 catalog 的 `approval.*` 下
|
|
119
|
+
* (`plan_execute` / `plan_readonly` / `plan_revise` / `deny`),cli / tui / web
|
|
120
|
+
* 三个宿主共用那四条。以前这个文件里还有一个 `PLAN_OUTCOME_LABELS` 常量装中文,
|
|
121
|
+
* 2026-08-22(RECORD-40-i18n §34.2)删了:protocol 是零依赖层,够不着 `t()`,所以任何
|
|
122
|
+
* 装在这里的文案都只能是**单语言**的 —— web 当时被迫在自己的 catalog 段下抄了
|
|
123
|
+
* 第二份,再用一条用例锁住两边逐字相等。判据同 `Diagnostic.module`:**协议层出码,
|
|
124
|
+
* 文案在 catalog**。
|
|
117
125
|
*/
|
|
118
126
|
declare const PLAN_OUTCOMES: readonly ["plan-execute", "plan-readonly", "plan-revise"];
|
|
119
127
|
type PlanApprovalOutcome = (typeof PLAN_OUTCOMES)[number];
|
|
120
128
|
declare function isPlanOutcome(value: string): value is PlanApprovalOutcome;
|
|
121
|
-
/** 计划审批四个出口各自的一行说明。宿主直接拿去当选项文案,别各写一份 */
|
|
122
|
-
declare const PLAN_OUTCOME_LABELS: Record<PlanApprovalOutcome | 'deny', string>;
|
|
123
129
|
|
|
124
130
|
/**
|
|
125
131
|
* 权限契约。
|
|
@@ -356,6 +362,23 @@ interface ToolContext {
|
|
|
356
362
|
homeDir?: string;
|
|
357
363
|
/** 当前权限级别 */
|
|
358
364
|
permissionLevel: string;
|
|
365
|
+
/**
|
|
366
|
+
* `terminal` 家族的 OS 沙箱开关(`EpochConfig.sandbox.terminal`,方案 46 §11.3
|
|
367
|
+
* 第一条)。**不给 = 开着**,也就是没有这个字段之前的行为,逐字节相同。
|
|
368
|
+
*
|
|
369
|
+
* 唯一的消费者是 `plugin-terminal` 的 `sandboxPolicyFor()`;`code_exec` 那条路
|
|
370
|
+
* 走另一个接缝(`isolate()`),**不读这一格**。为什么开关在这里而不是让插件
|
|
371
|
+
* 自己去读配置:插件按分层只许依赖 protocol + infra,够不着 core 的 loader ——
|
|
372
|
+
* 而这一格是引擎侧唯一同时握着「用户的配置」和「这一次工具调用」的地方。
|
|
373
|
+
*
|
|
374
|
+
* ⚠️ **判它必须显式跟 `false` 比**(`ctx.sandboxTerminal !== false`)。
|
|
375
|
+
* 写成裸真值判断的话,任何一个没填这一格的宿主(嵌入宿主自己拼 `ToolContext`
|
|
376
|
+
* 是支持的用法)都会**静默失去 OS 强制的写入边界**,而工具输出那一行沙箱说明
|
|
377
|
+
* 会一致地跟着说「关」—— 于是看起来像是用户自己关的。缺省朝「开」是刻意的:
|
|
378
|
+
* 漏填这一格的表现是**开关失灵**(配了 `false` 却照旧有沙箱),
|
|
379
|
+
* 而那种失败用户报得出来。
|
|
380
|
+
*/
|
|
381
|
+
sandboxTerminal?: boolean;
|
|
359
382
|
/** 取消信号(AbortController.signal) */
|
|
360
383
|
signal?: {
|
|
361
384
|
readonly aborted: boolean;
|
|
@@ -373,6 +396,50 @@ interface ToolContext {
|
|
|
373
396
|
* 调用它是**同步且廉价**的(往缓冲区里塞一段字符串),可以在 data 事件里直接调。
|
|
374
397
|
*/
|
|
375
398
|
onOutput?: (chunk: string) => void;
|
|
399
|
+
/**
|
|
400
|
+
* 从一个工具里再调一次工具(方案 51「不是绕过审批的后门」)。**这是 `run_code` 的子调用通道。**
|
|
401
|
+
*
|
|
402
|
+
* ## 它不是后门,是同一条管线的入口
|
|
403
|
+
*
|
|
404
|
+
* 引擎侧把它实现成**递归调用 `ToolExecutor.execute()` 自己**,所以一次子调用
|
|
405
|
+
* 逐字走完和模型直接调那个工具一模一样的五步:危险命令表 → 权限规则
|
|
406
|
+
* (deny > ask > allow)→ 审批 → 沙箱 → 不可信内容包裹。
|
|
407
|
+
*
|
|
408
|
+
* 方案 51「不是绕过审批的后门」 那句话是硬要求:「`run_code` 的工具绑定必须走 `ToolExecutor`,
|
|
409
|
+
* 不许直连 `tool.execute()`」。**在别处包一层门面绕过它,就是把审批关掉。**
|
|
410
|
+
*
|
|
411
|
+
* ## 引擎侧还叠了两道,判据都不在这个字段上
|
|
412
|
+
*
|
|
413
|
+
* 1. **只放行只读工具**(方案 51 PR-1 的范围,判据在 core 的
|
|
414
|
+
* `code-mode/readonly.ts`)—— 写类工具要等 PR-3 的审批取舍先拍板;
|
|
415
|
+
* 2. **只有第一层拿得到它。** 一次子调用自己的 `ToolContext` 里**这一格是缺席
|
|
416
|
+
* 的**,于是「工具 A 调 B、B 再调 C」这条路在第二跳就断了。少了它,
|
|
417
|
+
* 一个工具就能靠自己调自己把线程转到底。
|
|
418
|
+
*
|
|
419
|
+
* ## 不给 = 这条通道没接上,`run_code` 报错
|
|
420
|
+
*
|
|
421
|
+
* 缺省朝「不可用」是刻意的:嵌入宿主自己拼 `ToolContext` 是支持的用法,
|
|
422
|
+
* 而漏填这一格的表现是**工具明说自己不可用**,不是一条静默绕过管线的路。
|
|
423
|
+
* 方向和 `sandboxTerminal` 那一格相反,因为两者「漏填」的危险面相反 ——
|
|
424
|
+
* 那一格漏了会少一层边界,这一格漏了只是少一个能力。
|
|
425
|
+
*/
|
|
426
|
+
callTool?: SubToolCall;
|
|
427
|
+
}
|
|
428
|
+
/** {@link ToolContext.callTool} 的形状 */
|
|
429
|
+
interface SubToolCall {
|
|
430
|
+
(name: string, args: Record<string, unknown>): Promise<SubToolCallResult>;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* 一次子调用的结果。
|
|
434
|
+
*
|
|
435
|
+
* **只有两格,刻意的**:`output` 是**过完整条链之后**的那份文本(含不可信包裹和
|
|
436
|
+
* 截断),也就是模型直接调那个工具时会看到的**同一份**。交原始 `ToolResult`
|
|
437
|
+
* 的话,`run_code` 的程序会看到一份比模型更「干净」的输出 —— 那等于给它开了一条
|
|
438
|
+
* 跳过输出治理的路,而那些治理(截断、包裹)本来就是安全和成本的一部分。
|
|
439
|
+
*/
|
|
440
|
+
interface SubToolCallResult {
|
|
441
|
+
success: boolean;
|
|
442
|
+
output: string;
|
|
376
443
|
}
|
|
377
444
|
/** 工具执行结果 */
|
|
378
445
|
interface ToolResult {
|
|
@@ -463,10 +530,25 @@ interface ToolProvider {
|
|
|
463
530
|
/**
|
|
464
531
|
* 国际化契约(方案 40)。
|
|
465
532
|
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
533
|
+
* ## 这里有什么、没有什么(边界 2026-08-22 挪过一次)
|
|
534
|
+
*
|
|
535
|
+
* | 在这儿 | 不在这儿 |
|
|
536
|
+
* | ----------------------------------- | ------------------------------------------------- |
|
|
537
|
+
* | **值域**:{@link Lang} / {@link LANGS} / {@link DEFAULT_LANG} | 加载:读盘、找 `locales/` 目录、缓存 |
|
|
538
|
+
* | **纯查表**:{@link makeTranslate} / {@link flattenCatalog} / {@link placeholdersOf} | 进程级状态:`setLang()` / `currentLang()` / `resolveLang()` |
|
|
539
|
+
*
|
|
540
|
+
* 右边那一列住在 [infra/src/i18n.ts](../../infra/src/i18n.ts)。
|
|
541
|
+
* 分界是**「有没有 I/O 和状态」**:纯函数放这儿,读盘和进程语言放 infra。
|
|
542
|
+
*
|
|
543
|
+
* > ⚠️ 这段原来写的是「这里**只有值域**,没有实现」。2026-08-22(方案 40 PR-2)
|
|
544
|
+
* > 把纯查表提上来了,理由是**第三个消费方**:tui 只许依赖 protocol + view,
|
|
545
|
+
* > 而 web 已经为此在 `web/src/i18n/catalog.ts` 抄了一份 `t()`(那个文件头自己
|
|
546
|
+
* > 写着「两份 t() 会走散」)。tui 再抄一份就是三份。提上来之后 web 和 tui
|
|
547
|
+
* > 兼一份实现,infra 那份留着(它是零内部依赖包,import 不了这里 ——
|
|
548
|
+
* > 判据写在 {@link makeTranslate} 上)。
|
|
549
|
+
*
|
|
550
|
+
* 分开的理由和 `SHELL_KINDS` 一样:tui / web / cli 都要能写出 `Lang` 这个类型,
|
|
551
|
+
* 而它们里面有两个(tui、web)连 infra 都不许依赖。
|
|
470
552
|
*
|
|
471
553
|
* ## ⚠️ 值域是封闭的:`zh` 和 `en`,没有第三个
|
|
472
554
|
*
|
|
@@ -541,6 +623,71 @@ declare function isLang(value: string): value is Lang;
|
|
|
541
623
|
* (浏览器的系统偏好,不是用户在设置页里选的那一档)。逐条判据在方案 58 §1.1。
|
|
542
624
|
*/
|
|
543
625
|
declare const WIRE_LANG_PARAM = "lang";
|
|
626
|
+
/**
|
|
627
|
+
* 一条文案的占位符取值。
|
|
628
|
+
*
|
|
629
|
+
* 数字也收:`{count}` 那种地方调用方手动 `String()` 一遍纯属噪音,
|
|
630
|
+
* 而 `t()` 里本来就要 `String()`(判据在 {@link makeTranslate})。
|
|
631
|
+
*/
|
|
632
|
+
type TranslateVars = Record<string, string | number>;
|
|
633
|
+
/** 取一条文案的签名。tui 的 `t()` 和 web 的 `useT()` 拿到的都是它 */
|
|
634
|
+
type Translate = (key: string, vars?: TranslateVars) => string;
|
|
635
|
+
/** 一份拍平的 catalog:点分 key → 文案 */
|
|
636
|
+
type FlatCatalog = Record<string, string>;
|
|
637
|
+
/** 语言 → 拍平的 catalog。宿主注入 / 构建期注入的那份就是这个形状 */
|
|
638
|
+
type Catalogs = Partial<Record<Lang, FlatCatalog>>;
|
|
639
|
+
/**
|
|
640
|
+
* 一句文案里用到的占位符(去重,按出现顺序)。
|
|
641
|
+
*
|
|
642
|
+
* 导出它是因为**判据必须只有一份**:查表按这个正则替换,一致性用例按同一个正则
|
|
643
|
+
* 比对 zh / en 两边。抄成两份的话,「翻译时漏了 `{path}`」这种错误就会从
|
|
644
|
+
* 「用例红」退化成「运行时少一句关键信息,而且不报错」。
|
|
645
|
+
*/
|
|
646
|
+
declare function placeholdersOf(text: string): string[];
|
|
647
|
+
/**
|
|
648
|
+
* 把嵌套的 catalog(yaml 解出来的那棵树)拍成点分 key。
|
|
649
|
+
*
|
|
650
|
+
* 非字符串的叶子(数字、布尔、数组)**直接丢掉**而不是 `String()` 一下:
|
|
651
|
+
* catalog 里出现它们一定是写错了,静默转成 `[object Object]` 只会让错误晚几个
|
|
652
|
+
* 小时才被发现。丢掉之后根那条一致性用例会把它报成「zh 有 en 没有」。
|
|
653
|
+
*
|
|
654
|
+
* 住在 protocol 是因为**读盘的人和不读盘的人都要它**:web 的 vite 插件在 node 上
|
|
655
|
+
* 读 yaml、tui 的用例夹具也读 yaml,而这两个包都不许 import infra。
|
|
656
|
+
*/
|
|
657
|
+
declare function flattenCatalog(value: unknown, prefix?: string, out?: FlatCatalog): FlatCatalog;
|
|
658
|
+
/**
|
|
659
|
+
* 绑一组 catalog,得到查表函数 —— **三层回落,永不抛**。
|
|
660
|
+
*
|
|
661
|
+
* ```
|
|
662
|
+
* 目标语言的 key → DEFAULT_LANG 的 key → 返回 key 路径本身
|
|
663
|
+
* ```
|
|
664
|
+
*
|
|
665
|
+
* 第三层是关键:**catalog 写坏了不能让界面白屏 / 终端空行**。最坏情况用户看到
|
|
666
|
+
* `tui.commands.help_title` 这种字符串 —— 难看,但能跑。
|
|
667
|
+
*
|
|
668
|
+
* `vars` 里没给的占位符**原样留着**(`{path}` 照样显示):换成空串会让一句话
|
|
669
|
+
* 看起来完好无损、实际丢了关键信息,那正是最难查的形态。
|
|
670
|
+
*
|
|
671
|
+
* ## 为什么是工厂而不是直接读一个模块级常量
|
|
672
|
+
*
|
|
673
|
+
* 为了**回落链能被单独测**:「en 缺一条 → 回落中文」这种场景拿真 catalog 造不出来
|
|
674
|
+
* —— 根 `__tests__/i18n-catalog.test.ts` 恰好钉着两边 key 集合完全相同。
|
|
675
|
+
* infra 那边用 `EPOCH_LOCALES_DIR` 指一个临时目录达到同样效果,
|
|
676
|
+
* 而浏览器和 tui 里没有那个东西,所以换成注入。
|
|
677
|
+
*
|
|
678
|
+
* ## ⚠️ infra 里还有一份**同语义**的实现,那不是漏改
|
|
679
|
+
*
|
|
680
|
+
* `infra/src/i18n.ts` 的 `t()` 走的是同一条回落链,但它**不能**调这里 ——
|
|
681
|
+
* infra 是零内部依赖包(`check-layers.mjs` 的 `ALLOWED` 里它的内部依赖是空的,
|
|
682
|
+
* 只挂着三个没有源码的 vendor-ripgrep)。这跟 {@link LANGS} 在两边各写一份是
|
|
683
|
+
* 同一个形状、同一个办法:**抄一份 + 一条钉住「两边行为逐项相等」的用例**
|
|
684
|
+
* (根 `__tests__/i18n-catalog.test.ts`)。
|
|
685
|
+
* 反过来把 infra 那份删掉、让它 import protocol,等于把 protocol ← infra 这条
|
|
686
|
+
* 依赖方向反过来 —— 那是 `check:layers` 当场打红的事。
|
|
687
|
+
*/
|
|
688
|
+
declare function makeTranslate(catalogs: Catalogs): (lang: Lang, key: string, vars?: TranslateVars) => string;
|
|
689
|
+
/** 绑定语言,得到一个 {@link Translate}。tui 和 web 的 React 那半边都用它 */
|
|
690
|
+
declare function translatorFor(catalogs: Catalogs, lang: Lang): Translate;
|
|
544
691
|
|
|
545
692
|
/**
|
|
546
693
|
* 启动诊断的契约(方案 17)。
|
|
@@ -604,6 +751,21 @@ interface Diagnostic {
|
|
|
604
751
|
*
|
|
605
752
|
* 同一个模块的多条诊断共用一个 module(比如 Trust 可能既有 OK 也有一条说明)。
|
|
606
753
|
* 按 server / 插件分格的用 `MCP <name>` 这种带实例名的形式。
|
|
754
|
+
*
|
|
755
|
+
* ⚠️ **一律英文 PascalCase**(2026-08-23 定死,RECORD-40-i18n §32.2)。它是标识符不是文案,
|
|
756
|
+
* 所以**永远不进 catalog、永远不走 `t()`** —— 翻它等于把宿主的分支和去重键换掉。
|
|
757
|
+
* 而「不翻」还剩两种写法,这一条要的是后者:
|
|
758
|
+
*
|
|
759
|
+
* - ❌ 中文标识(`Agent 角色` / `额外目录`):不翻,于是英文界面上 `epoch doctor`
|
|
760
|
+
* 的第一列是「一堆英文夹几个中文」,而那一列正是宿主和用户都拿来对齐的东西;
|
|
761
|
+
* - ✅ 英文标识(`AgentRole` / `ExtraDirs`):两种界面语言下第一列都一样。
|
|
762
|
+
*
|
|
763
|
+
* 单复数跟着这一行说的东西走(`Plugin` 是一个子系统、`ExtraDirs` 是用户给的一批
|
|
764
|
+
* 目录),**不算契约的一部分** —— 契约只是「这个字符串不许变」。
|
|
765
|
+
*
|
|
766
|
+
* 门禁是 i18n 那个棘轮(`scripts/i18n-scan.mjs`):module 是字面量,新写一个中文的
|
|
767
|
+
* 当场把基线顶上去。⚠️ 它数的是中文,所以**只挡得住中文标识**,挡不住把一个已有的
|
|
768
|
+
* 英文标识改个字面 —— 后者只有引用它的用例会红。
|
|
607
769
|
*/
|
|
608
770
|
module: string;
|
|
609
771
|
status: DiagnosticStatus;
|
|
@@ -784,6 +946,80 @@ interface AgentRole {
|
|
|
784
946
|
* 不含 `terminal` 也不含 `code_exec`,那时它才是硬边界。
|
|
785
947
|
*/
|
|
786
948
|
tools?: string[];
|
|
949
|
+
/**
|
|
950
|
+
* 技能索引白名单(2026-08-26)。**缺省语义同 `tools`**:不给 = 全量索引,
|
|
951
|
+
* 给 `[]` = 技能段整个缺席,给了就是交集 —— 它是同一份 frontmatter 里
|
|
952
|
+
* 挨着 `tools` 的两行,给邻居两套缺省是纯陷阱。
|
|
953
|
+
*
|
|
954
|
+
* ## ⚠️ 它收窄的是**索引**,不是技能本身 —— 这不是漏接,是判断
|
|
955
|
+
*
|
|
956
|
+
* 读正文那个工具**不受这一格约束**:父 agent 说「读一下 deploy 技能」时,
|
|
957
|
+
* 被收窄过的子 agent 照样读得到。两条判据:
|
|
958
|
+
*
|
|
959
|
+
* 1. **它当不了边界,想当也当不了。** 技能是盘上的 md 文件,任何拿得到
|
|
960
|
+
* `file_read` 的角色都能直接读 `~/.epoch/skills/` 下任何一条 `SKILL.md` ——
|
|
961
|
+
* 而两个内置只读角色的 `READ_ONLY_TOOLS` 里**都有 `file_read`**。
|
|
962
|
+
* 这比上面 `tools` 那句「对含 `terminal` 的角色不构成安全边界」更彻底:
|
|
963
|
+
* 那边是「某些角色能绕」,这边是**每个**角色都能绕。所以它的定位只能是
|
|
964
|
+
* **上下文预算**(同 `skills.maxIndexed` 封顶那一档),不是权限轴;
|
|
965
|
+
* 2. 顺着这个定位,读正文保持开着是对的:收窄从不推翻一句明确的指令。
|
|
966
|
+
*
|
|
967
|
+
* ## ⚠️ 📮 2026-08-27:**「技术上它也够不着」那句话是假的,删了。**
|
|
968
|
+
*
|
|
969
|
+
* 原话是:「`ToolContext` 上没有角色,而那个工具是装配期注册一次的进程级实例。
|
|
970
|
+
* 要它按角色分叉得先往 `ToolContext` 上加一格,那是另一次判断。」于是那笔账
|
|
971
|
+
* (`RECORD-skill-index-narrowing` §四第 4 条)被写成「要修得先答『角色进不进
|
|
972
|
+
* `ToolContext`』」。**核下来它够得着,而且很便宜**:
|
|
973
|
+
*
|
|
974
|
+
* - `AgentConfig.role` 是一个 `() => {name, prompt?, scope?}` 的**现读口**,
|
|
975
|
+
* `AgentLoop` 每一轮都在调它(`run-start` 那枚徽标);
|
|
976
|
+
* - `ToolExecutor` 是那个 loop **自己 `new` 的**(`loop.ts`),deps 里加一个
|
|
977
|
+
* 现读口、再填进 `ToolContext` —— 加上契约上那一格,**四处各一行**。
|
|
978
|
+
*
|
|
979
|
+
* 这不是估的:2026-08-27 反向验证时**真接了一遍**(`ToolContext` 一格 +
|
|
980
|
+
* `ToolExecutor` 的 deps 和填充各一行 + loop 那一行),四处,然后让
|
|
981
|
+
* `skill_view` 按它分叉 —— 下面那条门禁当场红。接完就还原了。
|
|
982
|
+
*
|
|
983
|
+
* 那句话在的时候,上面判据 2(「读正文保持开着是对的」)读起来像是在给一个
|
|
984
|
+
* 做不到的事找台阶。**它不是台阶,它是结论**:这不是「做不到」,是**不做**。
|
|
985
|
+
*
|
|
986
|
+
* ## 那个问题的答案:**角色不进 `ToolContext`**,三条判据
|
|
987
|
+
*
|
|
988
|
+
* 1. **它会成为「按角色收窄」的第二处实现,而第一处已经在了。** 今天角色收窄
|
|
989
|
+
* 只有一个机制:`roleScopedProvider` —— 一层 `ToolProvider` 上的过滤器,
|
|
990
|
+
* 管的是**哪些工具存在**。把角色挂上 `ToolContext` 是另开一条轴:**每个工具
|
|
991
|
+
* 自己决定拿角色干什么**。两条轴并存就能不一致,而不一致从外面看不出来
|
|
992
|
+
* (同一个角色下,读 `ctx.role` 的工具和不读的工具行为不同)。
|
|
993
|
+
* `agent-role/scope.ts` 文件头写着那套收窄之所以住在 core 而不是装配层,
|
|
994
|
+
* 是因为「装配层每写一遍就多一处可能写错的地方」—— 发给每个工具是同一件事
|
|
995
|
+
* 的放大版:不是装配层那几处,是**每一个工具作者**;
|
|
996
|
+
* 2. **今天没有一个够格的消费者,而唯一的候选已经被判成不该分叉**(就是上面
|
|
997
|
+
* 判据 2 本身)。为一个我们决定不写的消费者加一格字段,它唯一的作用是
|
|
998
|
+
* 引诱下一个人;
|
|
999
|
+
* 3. **`ToolContext` 上今天没有一样东西是「策略」。** `sessionId` / `workDir` /
|
|
1000
|
+
* `homeDir` / `permissionLevel` / `signal` 全是**环境事实** —— 任何工具都可能
|
|
1001
|
+
* 正当地需要,而且没有一个是别的层已经做完的判断。角色不是事实,是一个
|
|
1002
|
+
* **已经在别处执行过的策略**(工具表早按它裁过了)。再发一份给工具,
|
|
1003
|
+
* 等于把一个已经落地的决定重新变成一个开放问题。
|
|
1004
|
+
* ⚠️ 唯一的近似反例是 `ToolContext.sandboxTerminal`,而它自己的 JSDoc 就在
|
|
1005
|
+
* 解释为什么例外:插件按分层够不着 core 的 loader,**引擎侧只有那一处同时
|
|
1006
|
+
* 握着用户的配置和这一次调用**。角色没有那个理由 —— 需要它的那一层
|
|
1007
|
+
* (装配层)本来就握着它。
|
|
1008
|
+
*
|
|
1009
|
+
* 这个决定由 `core/__tests__/skill-in-prompt.test.ts` 里那条钉着:一个被收窄过
|
|
1010
|
+
* 的角色**照样 `skill_view` 得到白名单外那条技能的正文**。
|
|
1011
|
+
*
|
|
1012
|
+
* ## 顺着这个答案,`available()` 那件事**不是劈叉**,定性改了
|
|
1013
|
+
*
|
|
1014
|
+
* 模型把名字打错时,读正文那个工具会把全量技能名(≤20 个)吐给一个被收窄过的
|
|
1015
|
+
* 子 agent。2026-08-26 把它记成「劈叉」;顺着上面的答案它**不是** ——
|
|
1016
|
+
* `skill_view` 本来就对全量开放,报出全量的名字和那个政策**是一致的**。
|
|
1017
|
+
*
|
|
1018
|
+
* 它是一笔**预算**上的小漏(打错名字那一次多花几十个 token),不是**边界**上的
|
|
1019
|
+
* 漏。仍然不修,但理由换了:不是「有界所以先欠着」,是「和已定的政策一致,
|
|
1020
|
+
* 只是不省钱」。
|
|
1021
|
+
*/
|
|
1022
|
+
skills?: string[];
|
|
787
1023
|
/** 轮次预算。不给则沿用 `max(4, floor(父 maxTurns / 2))` */
|
|
788
1024
|
maxTurns?: number;
|
|
789
1025
|
source: AgentRoleSource;
|
|
@@ -817,7 +1053,7 @@ declare function isValidAgentRoleName(name: string): boolean;
|
|
|
817
1053
|
* > 逐段的核对结果就贴在每一段的开头,读的时候连着读。
|
|
818
1054
|
*
|
|
819
1055
|
* **接完之后这一档仍然不加**,判据在方案 57 §2.1:身份行 `session` 那句是
|
|
820
|
-
*
|
|
1056
|
+
* 「你是一个本地智能体,**这一次**以「x」的身份工作」——「这一次」在逐条
|
|
821
1057
|
* 语境下正好是对的,**逐条和会话级在模型读到的字节上没有区别**。加一档要么两句
|
|
822
1058
|
* 一模一样(那就是一个没有消费方的枚举值,同 {@link AgentRole} 上那条判据),
|
|
823
1059
|
* 要么另编一句措辞而没有任何理由。
|
|
@@ -893,9 +1129,29 @@ declare function isValidAgentRoleName(name: string): boolean;
|
|
|
893
1129
|
* > 把身份那一半留在外面**换不回缓存**,只换来一个作用域劈叉。
|
|
894
1130
|
* > 上限判据没变:chip 粘住不清空,连发五条不换专家就只作废一次。
|
|
895
1131
|
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
1132
|
+
* > 📮 **2026-08-26 再订正一次:上面那条订正的前提没了,结论要分两半看。**
|
|
1133
|
+
* > `## 可用工具` 那一段删掉了(判据在 `core/agent/message-utils.ts` 的
|
|
1134
|
+
* > `SystemPromptSegments` 上:它和发给 provider 的 tools schema 数组完全重复)。
|
|
1135
|
+
* > 于是:
|
|
1136
|
+
* >
|
|
1137
|
+
* > - **「工具那一半无论如何都要动 system prompt」不再成立。** 今天工具表只通过
|
|
1138
|
+
* > `PromptBuilder.projectBlock()` 里那四个策略块影响 prompt
|
|
1139
|
+
* > (`isOpenWorldTool` / `enter_plan_mode` / `lsp_diagnostics` /
|
|
1140
|
+
* > `ask_user_question`)。一条只收窄了别的工具的 `allowed-tools` 斜杠命令
|
|
1141
|
+
* > **现在真的一个字节都不动 system prompt** —— 那笔「大家早就在付」的账,
|
|
1142
|
+
* > 多数情况下已经不用付了。
|
|
1143
|
+
* > - **「进 system prompt」那个结论不变。** 它靠的是身份那一半本身:身份要真的
|
|
1144
|
+
* > 换,就必须动 prompt。变的只是它不能再借工具那一半的车 —— 这笔缓存失效
|
|
1145
|
+
* > 现在是逐条专家自己买单,而上限判据(chip 粘住不清空)还是那一条。
|
|
1146
|
+
*
|
|
1147
|
+
* 身份行是 `systemPromptSegments()` 的 `identity` 段,也就是整段 prompt cache 的
|
|
1148
|
+
* 前缀。逐条换专家 ⇒ **每换一次作废整段缓存**。而那个函数的文件头逐字写着它存在
|
|
1149
|
+
* 的目的是「整轮对话中只构建一次,保证字节级不变」。
|
|
1150
|
+
*
|
|
1151
|
+
* > ⚠️ 2026-08-21 订正一处:这里原来写的是「`head` 的**第一行**」,而当时
|
|
1152
|
+
* > `PromptBuilder.buildSystem()` 把项目块拼在 `head` **前面** —— 也就是身份行
|
|
1153
|
+
* > 那时并不是整段的前缀,「作废整段缓存」这句话反而是**偏保守**的(真正在前面的
|
|
1154
|
+
* > 项目块不受影响)。那一轮把身份行拆成独立一段并提到最前,于是这句话从此为真。
|
|
899
1155
|
*
|
|
900
1156
|
* 这条代价本仓库已经**拒过一次**:`core/agent/loop.ts` 里那条后台任务通知,宁可作为
|
|
901
1157
|
* 末尾的一条 `user` 消息挂进去,也不并进 system prompt,逐字理由是「每轮往里塞一句
|
|
@@ -979,6 +1235,18 @@ interface ProviderInfo {
|
|
|
979
1235
|
/** 是否在交互式 provider 选择列表里出现(bedrock/azure 需要额外凭据,走配置文件) */
|
|
980
1236
|
interactive: boolean;
|
|
981
1237
|
}
|
|
1238
|
+
/**
|
|
1239
|
+
* ⚠️ **`label` 只放品牌名,一个注解都不挂。**
|
|
1240
|
+
*
|
|
1241
|
+
* `ollama` 和 `openai-compatible` 这两条原来是 `'Ollama (本地)'` /
|
|
1242
|
+
* `'OpenAI Compatible (通用适配)'` —— 括号里那半句是**注解,该翻**,而 protocol 是
|
|
1243
|
+
* 零依赖层、够不着 `t()`,挂在这里就只能是单语言的:一个把界面切成 English 的人,
|
|
1244
|
+
* 在模型菜单上会看到「Ollama (本地)」。2026-08-22(RECORD-40-i18n §34.3)把注解挪到了
|
|
1245
|
+
* runtime 的 `providerLabel()`,那两条完整显示名在 catalog 的 `provider.label_*` 下。
|
|
1246
|
+
*
|
|
1247
|
+
* 判据同这个文件删掉 `PLAN_OUTCOME_LABELS` 那一次:**协议层出码和品牌名,文案在
|
|
1248
|
+
* catalog**。品牌名不进 catalog 是因为它本来就不翻 —— 十三家里另外十一家一个字都不用动。
|
|
1249
|
+
*/
|
|
982
1250
|
declare const PROVIDER_INFOS: readonly ProviderInfo[];
|
|
983
1251
|
declare const PROVIDER_TYPES: readonly ProviderType[];
|
|
984
1252
|
declare function isProviderType(value: string): value is ProviderType;
|
|
@@ -1068,6 +1336,104 @@ interface EpochConfig {
|
|
|
1068
1336
|
/** 关掉就是最初的行为:任何目录都直接加载项目指令 / hook / policy */
|
|
1069
1337
|
enabled?: boolean;
|
|
1070
1338
|
};
|
|
1339
|
+
/**
|
|
1340
|
+
* `terminal` 家族的 OS 沙箱开关(方案 46 §11.3 第一条,2026-08-21 落地)。
|
|
1341
|
+
* **缺省 = 开着**,整段不配和显式写 `terminal: true` 行为逐字节相同。
|
|
1342
|
+
*
|
|
1343
|
+
* 它管的是 infra 那个 `confine()` 接缝 —— 也就是 `terminal` 的三条路径
|
|
1344
|
+
* (管道 / 后台 / PTY)和 `shell_*` 那一族。**`code_exec` 不受它影响**:
|
|
1345
|
+
* 那条路走的是另一个接缝(`isolate()`),边界也不一样(写入限于沙箱临时目录、
|
|
1346
|
+
* 默认断网、读取白名单),它自己那半从第一天起就没有开关。
|
|
1347
|
+
*
|
|
1348
|
+
* ## 为什么需要一个能关掉的开关
|
|
1349
|
+
*
|
|
1350
|
+
* 2026-08-16 起**每一条终端命令**都套上了 OS 强制的写入边界,而
|
|
1351
|
+
* `workspace-write` 那张可写目录表(工作区 + 临时目录 + 一张工具链缓存表,
|
|
1352
|
+
* 逐条列在 infra 的 `TOOLCHAIN_CACHE_DIRS` 上)**是猜出来的**。真撞上一条
|
|
1353
|
+
* 表里没有的缓存目录时,用户看到的是**一条本来能跑的命令突然被挡** ——
|
|
1354
|
+
* 那时他需要的是一个能立刻关掉的开关,而不是等我们发一版把目录补进表里。
|
|
1355
|
+
*
|
|
1356
|
+
* ## ⚠️ `bypass` 档不是它的替代品
|
|
1357
|
+
*
|
|
1358
|
+
* `bypass` 映射到 `danger-full-access`,沙箱确实整个让开 —— 但它**同时把审批
|
|
1359
|
+
* 也全放开了**,而那两件事本来正交(方案 46 §2.3「我们不做二维」)。
|
|
1360
|
+
* 一个只想解开写入边界的人被迫连审批一起关掉,那不是应急路径,是一次降级。
|
|
1361
|
+
* 这条判据不止写在这儿:`sandboxPolicyFor()` 关掉这个开关时**`mode` 照旧按
|
|
1362
|
+
* 权限档位推**(`default` 仍然是 `workspace-write`,不会被改写成
|
|
1363
|
+
* `danger-full-access`),所以两个轴始终分得开,`plugin-terminal` 那边有一条
|
|
1364
|
+
* 用例钉着这一点。
|
|
1365
|
+
*
|
|
1366
|
+
* ## ⚠️ 只有开 / 关,不许长出第二个维度
|
|
1367
|
+
*
|
|
1368
|
+
* 不加 per-command、不加 per-directory、不加第二张模式表 —— 方案 46 §2.3
|
|
1369
|
+
* 已经拍过「保持一维」,代价当时就说明了(想「自动批准但绝对不许写文件」的人
|
|
1370
|
+
* 今天只能用 `plan` 档)。真要做二维,路径是命名预设 + 派生 `custom`,
|
|
1371
|
+
* 那是另一次立项,不是往这一段上加键。
|
|
1372
|
+
*
|
|
1373
|
+
* ## 判它必须显式跟 `false` 比
|
|
1374
|
+
*
|
|
1375
|
+
* ```ts
|
|
1376
|
+
* config.sandbox?.terminal !== false; // ✅ 没配 = 沙箱在
|
|
1377
|
+
* config.sandbox?.terminal; // ❌ undefined 是 falsy = 没配就裸跑
|
|
1378
|
+
* ```
|
|
1379
|
+
*
|
|
1380
|
+
* 第二种写法的后果是**静默关掉一层 OS 强制边界**,而屏幕上什么都不会变
|
|
1381
|
+
* (工具输出那行沙箱说明也会跟着说「关」,于是看起来还挺一致)。
|
|
1382
|
+
* 这条线上每一跳的缺省都是「开」,所以漏填一跳的表现是**开关失灵**
|
|
1383
|
+
* (配了 `false` 却照旧有沙箱),不是沙箱静默消失 —— 方向是刻意选的。
|
|
1384
|
+
*
|
|
1385
|
+
* ## 它只在用户级那一层(②)
|
|
1386
|
+
*
|
|
1387
|
+
* 不进 `<项目根>/.epoch/settings.json`(那份只认 `permissions` / `model` /
|
|
1388
|
+
* `maxTurns`)。判据比 `artifacts` 那条更硬:一份 clone 下来、提交进仓库的
|
|
1389
|
+
* 文件如果关得掉 OS 强制边界,那就是一条能靠 PR 送进来的提权路径 ——
|
|
1390
|
+
* 方向和 [SETTINGS.md](../../../docs/SETTINGS.md) 第三节那句「可以替你收紧、
|
|
1391
|
+
* 不能替你放开」是同一条。
|
|
1392
|
+
*/
|
|
1393
|
+
sandbox?: {
|
|
1394
|
+
/** 缺省 = `true`(沙箱照常包)。见上面那段「判它必须显式跟 `false` 比」 */
|
|
1395
|
+
terminal?: boolean;
|
|
1396
|
+
};
|
|
1397
|
+
/**
|
|
1398
|
+
* 按需发现工具(2026-08-26)。
|
|
1399
|
+
*
|
|
1400
|
+
* 判据全文在 `core/src/config/schema.ts` 的 `ToolsSchema` 上,这里只留结论:
|
|
1401
|
+
* **默认关**,因为打开是一次**静默的能力缩减**(模型的初始工具列表里少了一批,
|
|
1402
|
+
* 而屏幕上没有任何东西说明发生了什么)。值不值得开由 `/context` 的工具那一行
|
|
1403
|
+
* 告诉你 —— 实测每个 MCP 工具边际 ≈ 200 tokens。
|
|
1404
|
+
*/
|
|
1405
|
+
tools?: {
|
|
1406
|
+
/** MCP 工具改成 `Deferred` 曝光:不进初始工具列表,模型用 `tool_search` 按需发现 */
|
|
1407
|
+
deferMcp?: boolean;
|
|
1408
|
+
/**
|
|
1409
|
+
* code mode(方案 51)。**缺省 = `'tools'`**,也就是今天的行为,一个字不变。
|
|
1410
|
+
*
|
|
1411
|
+
* 判据全文在 `core/src/config/schema.ts` 的 `ToolsSchema.mode` 上,
|
|
1412
|
+
* 这里只留结论:**默认关**,因为打开会把审批的粒度从「一次工具调用」放大到
|
|
1413
|
+
* 「一个程序」,而那是一次用户必须知情的安全模型改动。
|
|
1414
|
+
*
|
|
1415
|
+
* `'code'`(2026-08-27,PR-2)比 `'both'` 多做一件事:**程序真能调的那些
|
|
1416
|
+
* 工具从模型的工具表里切走**,改以一段生成的 SDK 声明进 system prompt。
|
|
1417
|
+
* 判据全文在 `core/src/code-mode/exposure.ts` 上,这里只留那一句:
|
|
1418
|
+
* **一个工具从工具表里消失,当且仅当程序真的能替模型调它** —— 于是这一档
|
|
1419
|
+
* 不会让任何工具变得没有路可走。
|
|
1420
|
+
*/
|
|
1421
|
+
mode?: 'tools' | 'both' | 'code';
|
|
1422
|
+
};
|
|
1423
|
+
/**
|
|
1424
|
+
* 技能索引的封顶(2026-08-26)。
|
|
1425
|
+
*
|
|
1426
|
+
* 判据全文在 `core/src/config/schema.ts` 的 `SkillsSchema` 上,这里只留结论:
|
|
1427
|
+
* **默认不封**,而且比 `tools.deferMcp` 更该守这条 —— 被 defer 的工具模型自己
|
|
1428
|
+
* `tool_search` 找得回来,被索引截掉的技能模型**永远不知道它存在**,全仓没有
|
|
1429
|
+
* 第二条发现路径。封顶是「丢哪些、要不要告诉人」两半,这里只管条数这一半,
|
|
1430
|
+
* 另一半(丢的次序、索引末尾那句提示、`/context` 上的 hint)是 `SkillSystem`
|
|
1431
|
+
* 的事。
|
|
1432
|
+
*/
|
|
1433
|
+
skills?: {
|
|
1434
|
+
/** 索引里最多列几条。缺省 = 不封(全部进索引,改造前的行为) */
|
|
1435
|
+
maxIndexed?: number;
|
|
1436
|
+
};
|
|
1071
1437
|
/** 成本与预算闸门。全部缺省 = 不限制 */
|
|
1072
1438
|
budget?: {
|
|
1073
1439
|
/** 单次会话的美元上限,超了发 finish: 'budget-exceeded' */
|
|
@@ -1257,6 +1623,32 @@ interface EpochConfig {
|
|
|
1257
1623
|
* 因为侧栏那一行还要装搜索框。
|
|
1258
1624
|
*/
|
|
1259
1625
|
brand?: string;
|
|
1626
|
+
/**
|
|
1627
|
+
* `~/.epoch/artifacts/` 的保留策略(方案 47 PR-4)。**缺省 = 内置默认**
|
|
1628
|
+
* (7 天 / 512MB)—— 整段不配和显式写这两个默认值,行为逐字节相同。
|
|
1629
|
+
*
|
|
1630
|
+
* 清理本来就一直在跑(core 的 `pruneArtifacts`,每进程一次、清在第一次真要
|
|
1631
|
+
* 落盘那一刻),这一段加的是**旋钮不是开关** —— 同 `compression` 那一类,
|
|
1632
|
+
* 不同于 `search` 那种「不配就压根没有这个能力」的段。
|
|
1633
|
+
*
|
|
1634
|
+
* ## 为什么只有 artifact 这一半可配
|
|
1635
|
+
*
|
|
1636
|
+
* `~/.epoch/` 上有两条容量上限:检查点 200MB、artifact 512MB(两个数字现在
|
|
1637
|
+
* 写在 core 的 `disk-ledger.ts` 同一张表里,不再互不知道)。只有这一条开了
|
|
1638
|
+
* 配置项,判据是**谁会真的想改它**:artifact 装的是命令输出全文,一个天天跑
|
|
1639
|
+
* 大输出的用户几天就能把它撑到上限;而检查点那边先被「每会话 20 条」那道闸
|
|
1640
|
+
* 兜住,200MB 至今没人撞到过。**没有消费方的配置项就是死键**,所以那一半
|
|
1641
|
+
* 留在构造参数上(`CheckpointStoreOptions`),等真有人撞到再提上来。
|
|
1642
|
+
*/
|
|
1643
|
+
artifacts?: {
|
|
1644
|
+
/** 超过这个天数的**会话目录**整个删掉,缺省 7。当前会话目录永远不删 */
|
|
1645
|
+
maxAgeDays?: number;
|
|
1646
|
+
/**
|
|
1647
|
+
* `artifacts/` 根目录的总容量上限(字节),缺省 512MB。超了从最旧的会话
|
|
1648
|
+
* 目录开始删 —— **不逐文件删**,半个会话的截图比没有更难理解。
|
|
1649
|
+
*/
|
|
1650
|
+
maxTotalBytes?: number;
|
|
1651
|
+
};
|
|
1260
1652
|
}
|
|
1261
1653
|
/** 支持的搜索后端。`searxng` 是**自托管无 key**那一档,内网 / 隐私敏感场景的答案 */
|
|
1262
1654
|
declare const SEARCH_PROVIDER_TYPES: readonly ["tavily", "brave", "searxng"];
|
|
@@ -1564,10 +1956,11 @@ declare const COMMAND_NAME_PATTERN: RegExp;
|
|
|
1564
1956
|
declare function isValidCommandName(name: string): boolean;
|
|
1565
1957
|
|
|
1566
1958
|
/**
|
|
1567
|
-
* 键位契约(方案 31 PR-1)—— 「这个按键是哪个动作」的**值域**。
|
|
1959
|
+
* 键位契约(方案 31 PR-1 / PR-2)—— 「这个按键是哪个动作」的**值域**。
|
|
1568
1960
|
*
|
|
1569
|
-
* 这里只有三样跨包共用的东西:动作的全集({@link KEY_ACTIONS}
|
|
1570
|
-
*
|
|
1961
|
+
* 这里只有三样跨包共用的东西:动作的全集({@link KEY_ACTIONS})、一条绑定的形状
|
|
1962
|
+
* ({@link KeyChord} 与 {@link KeySequence})、以及把 `"ctrl+l"` / `"escape escape"`
|
|
1963
|
+
* 这种写法和它互转的纯函数。
|
|
1571
1964
|
* 默认表住在 [tui/src/keybindings/defaults.ts](../../tui/src/keybindings/defaults.ts),
|
|
1572
1965
|
* 读盘 / 校验 / 冲突检测住在
|
|
1573
1966
|
* [core/src/config/keybindings.ts](../../core/src/config/keybindings.ts)。
|
|
@@ -1596,8 +1989,21 @@ declare function isValidCommandName(name: string): boolean;
|
|
|
1596
1989
|
* (见 `resolver.ts` 的索引构建)。今天默认表里一个冲突都没有,这只是兜底。
|
|
1597
1990
|
*
|
|
1598
1991
|
* PR-1 只收**今天已经硬编码在 app.tsx / input-box.tsx 里的那些键**,
|
|
1599
|
-
* 外加一个 `rewind`(见 defaults.ts)。
|
|
1600
|
-
*
|
|
1992
|
+
* 外加一个 `rewind`(见 defaults.ts)。
|
|
1993
|
+
*
|
|
1994
|
+
* ⚠️ **PR-3 一个 vim 动作都没加**,而 PR-1 这里原先写的是「`vim-normal` /
|
|
1995
|
+
* `vim-insert` 等归 PR-3」。落地时那个设计走不通,两条理由:
|
|
1996
|
+
*
|
|
1997
|
+
* 1. **进 NORMAL 那一下不该是一个新动作。** 它就是 `interrupt`(Esc)——
|
|
1998
|
+
* 在 INSERT 里回 NORMAL、在 NORMAL 里照旧中断本轮(§4.3 #19)。真给它开一个
|
|
1999
|
+
* `vim-normal` 并默认绑 Esc,冲突检测会**当场拒掉**它:`interrupt` 是 `global`、
|
|
2000
|
+
* 它是 `input`,而这两个上下文是同时活着的({@link SIMULTANEOUS_CONTEXTS})。
|
|
2001
|
+
* 2. **NORMAL 里那几十个键压根不走这张表。** `d` / `w` / `x` / `.` 是一台状态机
|
|
2002
|
+
* 的输入(`tui/src/vim/machine.ts`),不是二十几个各自可绑的动作 ——
|
|
2003
|
+
* 把它们登记进来等于让用户能把 `dw` 拆散。
|
|
2004
|
+
*
|
|
2005
|
+
* PR-2 一个动作都没加:序列(`escape escape`)改的是**一条绑定长什么样**,
|
|
2006
|
+
* 不是有哪些动作。`rewind` 早在 PR-1 就登记了,只是那时没有键。
|
|
1601
2007
|
*/
|
|
1602
2008
|
declare const KEY_ACTIONS: readonly ["interrupt", "rewind", "clear-screen", "open-artifact", "transcript-search", "exit", "exit-if-empty", "submit", "newline", "history-prev", "history-next", "cursor-left", "cursor-right", "line-start", "line-end", "kill-word", "kill-line-start", "kill-line-end", "delete-char-left", "delete-char-right", "paste-image", "complete", "complete-prev", "complete-next"];
|
|
1603
2009
|
/** 一个动作 */
|
|
@@ -1656,12 +2062,49 @@ interface KeyChord {
|
|
|
1656
2062
|
shift?: boolean;
|
|
1657
2063
|
}
|
|
1658
2064
|
/**
|
|
1659
|
-
*
|
|
2065
|
+
* 一条绑定 —— **按顺序按下的一个或多个 chord**(方案 31 PR-2 §4.2)。
|
|
2066
|
+
*
|
|
2067
|
+
* 长度 1 是绝大多数(`ctrl+l`),长度 ≥ 2 是序列(`escape escape`)。
|
|
2068
|
+
*
|
|
2069
|
+
* ## 为什么是「非空元组」而不是 `KeyChord[]`
|
|
2070
|
+
*
|
|
2071
|
+
* 空序列没有意义,而它能从两处混进来:用户写了 `[""]`,或者代码里 `.slice()` 出
|
|
2072
|
+
* 一段。类型上堵住之后,`seq[0]` 在 `noUncheckedIndexedAccess` 下也不用再兜一次
|
|
2073
|
+
* undefined —— 那种兜底代码里必然有一支永远走不到,也就永远测不到。
|
|
2074
|
+
*
|
|
2075
|
+
* ## 为什么不是「单键表 + 序列表」两张
|
|
2076
|
+
*
|
|
2077
|
+
* 想过。两张表就要在**每一处**构造点同时填对两张(默认表、用户合并、冲突检测),
|
|
2078
|
+
* 而它们迟早会漂 —— 同 `RESERVED_COMMAND_NAMES` 那条判据(一份真源,别养双向
|
|
2079
|
+
* 用例去锁两份相等)。代价是 {@link resolveAction} 那侧要显式跳过长度 ≥ 2 的,
|
|
2080
|
+
* 那一句写在它的索引构建里。
|
|
2081
|
+
*/
|
|
2082
|
+
type KeySequence = readonly [KeyChord, ...KeyChord[]];
|
|
2083
|
+
/**
|
|
2084
|
+
* 一张生效的绑定表:动作 → 若干条绑定(一个动作可以绑多个键 / 多条序列)。
|
|
1660
2085
|
*
|
|
1661
2086
|
* **交给 tui 的一定是已经校验过的**:动作名合法、chord 解析过、保留键剔掉了、
|
|
1662
2087
|
* 冲突消解过。tui 这一侧不做校验,也就没有「一半有效的表」这种状态。
|
|
1663
2088
|
*/
|
|
1664
|
-
type KeybindingTable = Readonly<Record<KeyAction, readonly
|
|
2089
|
+
type KeybindingTable = Readonly<Record<KeyAction, readonly KeySequence[]>>;
|
|
2090
|
+
/**
|
|
2091
|
+
* 序列**目前只在这些上下文里生效**(方案 31 PR-2 起,PR-3 补上 `input`)。
|
|
2092
|
+
*
|
|
2093
|
+
* 不是设计上的限制,是**落地范围**:序列的状态机是一台一台接上去的 ——
|
|
2094
|
+
* PR-2 接了 `app.tsx`(`global`),PR-3 接了 `input-box.tsx`(`input`)。
|
|
2095
|
+
* `dialog` 还没接:补全面板开着的那几个键(↑↓ / Tab / Enter)今天走的是单键那一层
|
|
2096
|
+
* (`input-box.tsx` 里那次 `actionOf(input, key, 'dialog')`),而面板开着的时候
|
|
2097
|
+
* 用户正在选东西,把它做成「按一下等半秒看有没有第二下」的形状收益是负的。
|
|
2098
|
+
*
|
|
2099
|
+
* ⚠️ **两个上下文各是一台状态机,不是一台服务两边。** 一次按键会被两个 `useInput`
|
|
2100
|
+
* 各收一次(见 {@link SIMULTANEOUS_CONTEXTS}),共用一台等于把窗口推进两步 ——
|
|
2101
|
+
* 判据全文在 `tui/src/keybindings/use-key-sequences.ts` 的文件头。
|
|
2102
|
+
*
|
|
2103
|
+
* 加载期据此**拒绝**绑在还没接的那些动作上的序列并出一条诊断,而不是收下来让它不
|
|
2104
|
+
* 生效:「配了但没反应」是最难自查的一类问题,而这一条自查起来尤其难 ——
|
|
2105
|
+
* 同一份文件里 `rewind` 那条序列明明是好的。
|
|
2106
|
+
*/
|
|
2107
|
+
declare const SEQUENCE_CONTEXTS: readonly KeyContext[];
|
|
1665
2108
|
/**
|
|
1666
2109
|
* 保留键:**不许被重绑**。试图绑上去的那一条拒绝加载 + 一条 warn 诊断
|
|
1667
2110
|
* (不是整份配置作废 —— 那是「文件坏了」,另一回事)。
|
|
@@ -1687,13 +2130,96 @@ declare function chordId(chord: KeyChord): string;
|
|
|
1687
2130
|
* 不认识的修饰键、空的键名、多于一个 code point 的键名一律判不合法 ——
|
|
1688
2131
|
* 静默忽略半个 chord 会让「配了没生效」变成一个没人查得出的问题。
|
|
1689
2132
|
*
|
|
1690
|
-
* ⚠️ **序列(`"escape escape"
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
2133
|
+
* ⚠️ **序列(`"escape escape"`)不在这里**,在 {@link parseSequence} —— 它按空格
|
|
2134
|
+
* 切开再逐段调本函数。所以这里遇到空白仍然直接判不合法,而不是取第一段:
|
|
2135
|
+
* 取第一段的话,一份带序列的配置会静默降级成单键,用户以为序列生效了。
|
|
1693
2136
|
*/
|
|
1694
2137
|
declare function parseChord(text: string): KeyChord | undefined;
|
|
2138
|
+
/**
|
|
2139
|
+
* 序列的规范字符串:`"escape escape"`。**比较和展示都走它**,同 {@link chordId}。
|
|
2140
|
+
*
|
|
2141
|
+
* 分隔符是一个空格,而 {@link parseSequence} 收任意空白 —— 用户写两个空格、写
|
|
2142
|
+
* tab,归一之后仍然是同一条绑定。
|
|
2143
|
+
*/
|
|
2144
|
+
declare function sequenceId(sequence: KeySequence): string;
|
|
2145
|
+
/**
|
|
2146
|
+
* `"escape escape"` → 序列;`"ctrl+l"` → 长度 1 的序列。认不出来返回 undefined。
|
|
2147
|
+
*
|
|
2148
|
+
* **整条要么全对要么全废**:`"escape ctrl+shit+l"` 判不合法,而不是留下前半段。
|
|
2149
|
+
* 半条序列在运行期的表现是「按了第一下有反应、第二下没有」,而用户会以为是
|
|
2150
|
+
* 时间窗口的问题,跑去调那个 500ms。
|
|
2151
|
+
*/
|
|
2152
|
+
declare function parseSequence(text: string): KeySequence | undefined;
|
|
1695
2153
|
/** 是不是一个我们认识的动作名。用户输入(配置文件的键名)收窄用 */
|
|
1696
2154
|
declare function isKeyAction(value: string): value is KeyAction;
|
|
2155
|
+
/**
|
|
2156
|
+
* 一条绑定是哪一层来的。
|
|
2157
|
+
*
|
|
2158
|
+
* 这是 PR-2 真正的工作量所在:PR-1 的 `loadKeybindings` 只回「最后生效的表」+
|
|
2159
|
+
* 诊断,**表里看不出哪条是用户改的** —— 而用户查键位问题时的第一个问题永远是
|
|
2160
|
+
* 「这条是谁定的、我该去改哪个文件」(同 `/permissions` 那个 `layer`)。
|
|
2161
|
+
*/
|
|
2162
|
+
type KeybindingSource = 'default' | 'user';
|
|
2163
|
+
/** 一条生效的绑定 + 它的来源 */
|
|
2164
|
+
interface BoundKey {
|
|
2165
|
+
keys: KeySequence;
|
|
2166
|
+
source: KeybindingSource;
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* 一条**没**生效的绑定,以及为什么。
|
|
2170
|
+
*
|
|
2171
|
+
* `keys` 是**用户写的原文**而不是 {@link sequenceId}:`invalid` 那一档压根解析不
|
|
2172
|
+
* 出来,而用户要拿这一串回去在文件里找到它。
|
|
2173
|
+
*/
|
|
2174
|
+
interface RejectedKey {
|
|
2175
|
+
/** 动作名。`unknown-action` 那一档它就是那个拼错的名字,所以类型是 string */
|
|
2176
|
+
action: string;
|
|
2177
|
+
/** 用户写的那一串,逐字 */
|
|
2178
|
+
keys: string;
|
|
2179
|
+
reason: RejectReason;
|
|
2180
|
+
/** `conflict` 那一档:这个键被谁占住了 */
|
|
2181
|
+
winner?: KeyAction;
|
|
2182
|
+
}
|
|
2183
|
+
/**
|
|
2184
|
+
* 一条绑定被丢掉的原因。五档各对应一条加载期判定:
|
|
2185
|
+
*
|
|
2186
|
+
* | 档 | 判据 |
|
|
2187
|
+
* | --------------- | ---------------------------------------------------------- |
|
|
2188
|
+
* | `reserved` | 撞上 {@link RESERVED_CHORDS}(逃生通道不许被锁死) |
|
|
2189
|
+
* | `conflict` | 同时活着的上下文里这个键已经有主人了 |
|
|
2190
|
+
* | `invalid` | {@link parseSequence} 认不出来 |
|
|
2191
|
+
* | `unknown-action`| 动作名不在 {@link KEY_ACTIONS} 里 |
|
|
2192
|
+
* | `unsupported` | 序列绑在了输入框动作上,见 {@link SEQUENCE_CONTEXTS} |
|
|
2193
|
+
*/
|
|
2194
|
+
type RejectReason = 'reserved' | 'conflict' | 'invalid' | 'unknown-action' | 'unsupported';
|
|
2195
|
+
/**
|
|
2196
|
+
* 一次加载的完整账本 —— `/keybindings` 印的就是它。
|
|
2197
|
+
*
|
|
2198
|
+
* 和 {@link KeybindingTable} 的分工:表是**给键盘用的**(每次按键查一次,所以它
|
|
2199
|
+
* 只有「哪个键 → 哪个动作」),账本是**给人看的**(多了来源和被丢掉的那些)。
|
|
2200
|
+
* 表由 {@link tableOf} 从账本投影出来,**不是两处各构造一遍** —— 后者迟早会出现
|
|
2201
|
+
* 「/keybindings 说 ctrl+k 是清屏,按下去却不清」。
|
|
2202
|
+
*/
|
|
2203
|
+
interface KeybindingReport {
|
|
2204
|
+
/** 用户那份配置文件的路径。「我该去改哪个文件」的答案 */
|
|
2205
|
+
path: string;
|
|
2206
|
+
/** 那个文件在不在。不在是**正常情况**(绝大多数人不改键) */
|
|
2207
|
+
present: boolean;
|
|
2208
|
+
bindings: Readonly<Record<KeyAction, readonly BoundKey[]>>;
|
|
2209
|
+
rejected: readonly RejectedKey[];
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* 逐动作造一张全量表。**「每个动作都有一格」这件事由这个函数保证**,调用方不用再
|
|
2213
|
+
* 各自 `Object.fromEntries(...) as` 一次。
|
|
2214
|
+
*
|
|
2215
|
+
* 里面那次断言是 `Partial<Record<…>>` → `Record<…>`,也就是「循环跑完之后每一格都
|
|
2216
|
+
* 填上了」—— 这一句在这里成立(循环由 {@link KEY_ACTIONS} 驱动),而它是全仓唯一
|
|
2217
|
+
* 需要成立的一处。`fromEntries` 那种写法给出的是索引签名,断成 `Record` 会被
|
|
2218
|
+
* TS 判成两个类型压根不重叠,只能再套一层 `as unknown`,而那就真的什么都不检查了。
|
|
2219
|
+
*/
|
|
2220
|
+
declare function perAction<T>(make: (action: KeyAction) => T): Readonly<Record<KeyAction, T>>;
|
|
2221
|
+
/** 账本 → 键盘要查的那张表。投影,不是第二份真源 */
|
|
2222
|
+
declare function tableOf(bindings: KeybindingReport['bindings']): KeybindingTable;
|
|
1697
2223
|
|
|
1698
2224
|
/**
|
|
1699
2225
|
* 可观测性契约(方案 14)。
|
|
@@ -2201,11 +2727,61 @@ type AgentEvent =
|
|
|
2201
2727
|
model?: string;
|
|
2202
2728
|
breach?: BudgetBreach;
|
|
2203
2729
|
}
|
|
2730
|
+
/**
|
|
2731
|
+
* 引擎有一句话要对用户说,而**它不是回复正文**(2026-08-26)。
|
|
2732
|
+
*
|
|
2733
|
+
* ## 发它的是谁,以及为什么非要一条新帧
|
|
2734
|
+
*
|
|
2735
|
+
* 今天唯一的发射方是 core 的 `AgentLoop`:技能学习器在上一轮收尾之后
|
|
2736
|
+
* fire-and-forget 学到了一条技能,而那件事没有任何出口(判据全文在
|
|
2737
|
+
* `agent/loop.ts` 的 `maybeLearn()`)。三条现成的路各自被什么挡掉:
|
|
2738
|
+
*
|
|
2739
|
+
* 1. **`text-delta`** —— 它会被折进 assistant 那一段正文,于是这句话进历史、
|
|
2740
|
+
* 进落盘、下一轮再进模型。一句「你学会了 X」被当成模型自己说过的话,
|
|
2741
|
+
* 等于往上下文里塞一条它没说过的发言;
|
|
2742
|
+
* 2. **往 system prompt 里塞** —— 那是 prompt cache 的前缀,每轮加一句会让整段
|
|
2743
|
+
* 对话的缓存逐轮失效。同一条判据 `loop.ts` 里为后台任务通知讲过一遍;
|
|
2744
|
+
* 3. **一条 `[系统]` 进历史**(`systemNote()` + `recordNote()`,样板是
|
|
2745
|
+
* `runtime` 的 `markRoleHandoff()`)—— 它是真出口,但那句话**只在刷新 /
|
|
2746
|
+
* 回放时看得见**,而且今天只有 web 认那个前缀,TUI 会把 `[系统] ` 原样露出来。
|
|
2747
|
+
*
|
|
2748
|
+
* ## 为什么是「码 + 已经翻好的话」两个字段
|
|
2749
|
+
*
|
|
2750
|
+
* `text` 由**发射方**(core,它有 `t()`)翻好:`@epoch-agent/view` 只依赖
|
|
2751
|
+
* protocol、够不着 catalog,在那儿兜底就只能是中文 —— 同 `error` 那条空 message
|
|
2752
|
+
* 的兜底为什么落在两个宿主身上,是同一条判据。
|
|
2753
|
+
*
|
|
2754
|
+
* `code` 是给**要分支的宿主**的:判「这是哪一类通知」不许去 `includes()` 那句
|
|
2755
|
+
* 随时会改的话(RECORD-40-i18n §32.1 那条 `reason === '已取消'` 的教训)。
|
|
2756
|
+
*
|
|
2757
|
+
* ⚠️ **要拿技能名去做别的事**(跳到能力页、打开那份 SKILL.md),就再加一个
|
|
2758
|
+
* 字段,别去正则 `text` —— 那句话是会随文案改的。
|
|
2759
|
+
*/
|
|
2760
|
+
| {
|
|
2761
|
+
type: 'notice';
|
|
2762
|
+
code: NoticeCode;
|
|
2763
|
+
text: string;
|
|
2764
|
+
}
|
|
2204
2765
|
/** 运行中出错(随后会有一个 finish: 'error') */
|
|
2205
2766
|
| {
|
|
2206
2767
|
type: 'error';
|
|
2207
2768
|
message: string;
|
|
2208
2769
|
};
|
|
2770
|
+
/**
|
|
2771
|
+
* 一条 `notice` 是哪一类。
|
|
2772
|
+
*
|
|
2773
|
+
* 今天只有一种,但写成联合是为了让**将来加第二种时消费方的 switch 会红** ——
|
|
2774
|
+
* 判据同 wire.ts 的 `WireResetReason`。
|
|
2775
|
+
*/
|
|
2776
|
+
type NoticeCode =
|
|
2777
|
+
/**
|
|
2778
|
+
* 技能学习器在上一轮收尾之后学到了一条技能。
|
|
2779
|
+
*
|
|
2780
|
+
* ⚠️ 这一帧发在**下一轮的头上**,不是学成的那一刻 —— 学成时那条流早关了
|
|
2781
|
+
* (`maybeLearn()` 紧接着就是 `yield finish('stop'); return;`)。发在这里还有
|
|
2782
|
+
* 第二层意思:那一轮正是这条技能**真的进了 system prompt** 的那一轮。
|
|
2783
|
+
*/
|
|
2784
|
+
'skill-learned';
|
|
2209
2785
|
type AgentEventType = AgentEvent['type'];
|
|
2210
2786
|
/** 取出某个具体事件的类型,例如 AgentEventOf<'tool-call'> */
|
|
2211
2787
|
type AgentEventOf<T extends AgentEventType> = Extract<AgentEvent, {
|
|
@@ -2529,11 +3105,23 @@ interface ScheduleDefinition {
|
|
|
2529
3105
|
*/
|
|
2530
3106
|
workDir: string;
|
|
2531
3107
|
model?: string;
|
|
2532
|
-
/**
|
|
3108
|
+
/**
|
|
3109
|
+
* 「召唤专家」。**今天永远不带 —— 但已经不是被别人挡着了。**
|
|
3110
|
+
*
|
|
3111
|
+
* 立案时写的是「等方案 42 PR-1 把顶层会话的角色接线」,而那一笔
|
|
3112
|
+
* **2026-08-15 就结清了**(`SessionFactory.create({ role })` +
|
|
3113
|
+
* `WireCreateSessionRequest.role`,见 [agent-role.ts](./agent-role.ts) 文件头)。
|
|
3114
|
+
* 挡着这一格的只剩「没人做」:`add` / 表单 / 端点三条写入路都不写它。
|
|
3115
|
+
* ⚠️ **别把「被别人挡着」和「没人做」读成一件事** —— 逐条状态在
|
|
3116
|
+
* [验收记录 9.2](../../../docs/verify/VERIFY_RECORD-45-automation.md)。
|
|
3117
|
+
*/
|
|
2533
3118
|
role?: string;
|
|
2534
|
-
/**
|
|
3119
|
+
/** 同 {@link role}:方案 44 的清单契约早就到齐了,缺的是写入点。今天永远不带 */
|
|
2535
3120
|
skills?: readonly string[];
|
|
2536
|
-
/**
|
|
3121
|
+
/**
|
|
3122
|
+
* 同 {@link role},缺的是写入点。
|
|
3123
|
+
* 连接器的免确认另有一条现成的路:`allowRules` 里的 `mcp__<server>__*`。
|
|
3124
|
+
*/
|
|
2537
3125
|
mcpServers?: readonly string[];
|
|
2538
3126
|
/**
|
|
2539
3127
|
* 权限档 —— **和对话里那枚胶囊同一套取值、同一套词**。
|
|
@@ -2632,6 +3220,213 @@ interface ScheduleRunnerSpec {
|
|
|
2632
3220
|
label?: string;
|
|
2633
3221
|
}
|
|
2634
3222
|
|
|
3223
|
+
/**
|
|
3224
|
+
* 内置模板六张([方案 45](../../../docs/verify/VERIFY_RECORD-45-automation.md) §7.3)。
|
|
3225
|
+
*
|
|
3226
|
+
* 判据是「**这件事必须每天/每周做一次,而且做完是一份能读的东西**」——
|
|
3227
|
+
* 所以 WorkBuddy 那 12 张消费级的(儿童睡前故事 / 父母联系提醒 / 萌宠壁纸)
|
|
3228
|
+
* 一张都没抄,换成开发者场景。
|
|
3229
|
+
*
|
|
3230
|
+
* ## ⚠️ 三条硬约束都摊在类型上,不靠注释
|
|
3231
|
+
*
|
|
3232
|
+
* | §7.3 那句话 | 落在类型的哪一格 |
|
|
3233
|
+
* | ------------------------------- | -------------------------------------------------------------------- |
|
|
3234
|
+
* | 模板**不联网** | 这是一份 `const`,没有取数入口 —— 在线模板市场 42 已定不做 |
|
|
3235
|
+
* | 一律不预设「跳过全部检查」 | {@link ScheduleTemplate.permission} 的取值里**没有** `bypass` |
|
|
3236
|
+
* | 填进表单后**必须让用户过一遍** | 整份形状里**没有** `maxBudgetUsd`,而它是必填 —— 表单因此保存不了 |
|
|
3237
|
+
*
|
|
3238
|
+
* 最后一条是三条里最要紧的,而它是**结构性**的而不是靠一句提示:预算必填、
|
|
3239
|
+
* 没有默认值(§3.5),于是一张模板卡填进表单之后那个保存按钮**天然是灰的**,
|
|
3240
|
+
* 用户不看完那一屏就出不去。写成「模板里预填 0.50 + 一句『请检查』」的话,
|
|
3241
|
+
* 「必须过一遍」就退化成一句没人读的话。
|
|
3242
|
+
*
|
|
3243
|
+
* ## 为什么住在 protocol,而不是「core 一份 + 这里抄一份」
|
|
3244
|
+
*
|
|
3245
|
+
* 任务书给的两条路是「从 server 端点下发」或者「抄一份到 protocol + 一条逐项相等
|
|
3246
|
+
* 的用例」(照 `WireScheduleIssueCode` 的先例)。**两条都不用,因为这一份和那个
|
|
3247
|
+
* 先例不是同一种东西**:
|
|
3248
|
+
*
|
|
3249
|
+
* - `WireScheduleIssueCode` 的真源在 core**必须**在 core —— 那张码表是
|
|
3250
|
+
* `validateSchedule()` 的出口集合,它和判定逻辑长在一起,搬不动;
|
|
3251
|
+
* - 而这一份是**纯数据、零判定**:六个触发器、六档权限、六份最小清单。
|
|
3252
|
+
* 它谁都不依赖。而 [AGENTS.md](../../../AGENTS.md) 的分层表里 protocol 那一行
|
|
3253
|
+
* 写着「跨包共享的东西**只能**放这里」。
|
|
3254
|
+
*
|
|
3255
|
+
* 放这儿的直接收益是**不存在第二份**:`packages/web` 够得着它(只依赖
|
|
3256
|
+
* protocol + view)、cli 够得着、server 够得着、假服务端也够得着。抄一份到
|
|
3257
|
+
* protocol 再钉一条「逐项相等」的用例,是在为一个可以不存在的分叉修围栏;
|
|
3258
|
+
* 走一条 server 端点则要为一份**从不改变的常量**加一条 HTTP 往返和一个失败态
|
|
3259
|
+
* (「模板拉不下来」——而它明明就在包里)。
|
|
3260
|
+
*
|
|
3261
|
+
* ## ⚠️ 这里一个中文字都没有,措辞在 web 那份 catalog 里
|
|
3262
|
+
*
|
|
3263
|
+
* 名字 / 说明 / **提示词**三样都是文案:提示词要跟着用户的界面语言走
|
|
3264
|
+
* (一个英文界面的用户不该收到一份中文报告),而 protocol 里没有 `t()`、
|
|
3265
|
+
* 也不该有。所以这一份只给 `id`,措辞表在
|
|
3266
|
+
* `web/src/automation/labels.ts` 的 `TEMPLATE_TEXT` 上 —— 那张表
|
|
3267
|
+
* `satisfies Record<ScheduleTemplateId, …>`,**加一张模板而不给它文案会当场编译不过**。
|
|
3268
|
+
*/
|
|
3269
|
+
|
|
3270
|
+
/**
|
|
3271
|
+
* 六张模板的 id。
|
|
3272
|
+
*
|
|
3273
|
+
* 它同时是**文案表的键**(web 那侧 `satisfies Record<ScheduleTemplateId, …>`),
|
|
3274
|
+
* 所以这个联合就是「有几张模板」的唯一真源 —— 加一张要改的地方由编译器点出来。
|
|
3275
|
+
*/
|
|
3276
|
+
type ScheduleTemplateId = 'repo-sweep' | 'weekly-digest' | 'dep-checkup' | 'stale-docs' | 'test-flake' | 'todo-digest';
|
|
3277
|
+
/**
|
|
3278
|
+
* 一张模板 = 一条任务除了「名字 / 提示词 / 预算」之外的那些格子。
|
|
3279
|
+
*
|
|
3280
|
+
* 三样刻意缺席,各有各的理由:
|
|
3281
|
+
*
|
|
3282
|
+
* - `name` / `prompt` —— 是**文案**,见文件头最后一节;
|
|
3283
|
+
* - `maxBudgetUsd` —— 必填且没有默认值(§3.5),见文件头那张表第三行;
|
|
3284
|
+
* - `workDir` —— 模板不知道用户的仓库在哪儿。留空的语义是
|
|
3285
|
+
* `~/.epoch/automation/<id>/`(`ScheduleDefinition.workDir` 上的判据),
|
|
3286
|
+
* 而**清单里那条 `file_write(./reports/**)` 是相对的**,所以它跟着工作区走 ——
|
|
3287
|
+
* 不管用户填不填,报告都落在那条任务自己的目录下面。
|
|
3288
|
+
*/
|
|
3289
|
+
interface ScheduleTemplate {
|
|
3290
|
+
id: ScheduleTemplateId;
|
|
3291
|
+
trigger: ScheduleTrigger;
|
|
3292
|
+
/**
|
|
3293
|
+
* 权限档。**取值里没有 `bypass`**(§7.3:模板一律不预设「跳过全部检查」),
|
|
3294
|
+
* 也没有 `auto`(界面压根不暴露它,§3.2)。
|
|
3295
|
+
*
|
|
3296
|
+
* 两档各有各的用法,而分界线是 by-level 那张表:
|
|
3297
|
+
*
|
|
3298
|
+
* - `plan`(只读)—— 「读一遍然后写一份报告」的那几张。它天然零授权面,
|
|
3299
|
+
* 清单里只要一条 `file_write`(那条走**第 2 层**规则匹配,所以在只读档下
|
|
3300
|
+
* 照样放行 —— §3.2 那句「只读 + `file_write(<任务目录>/**)`」说的就是它);
|
|
3301
|
+
* - `default`(按需确认)—— 要跑命令的那几张。**除了 `bypass`,写命令在每一档
|
|
3302
|
+
* 下都要确认**,所以它们只能靠清单逐条放行,而这正是清单存在的理由。
|
|
3303
|
+
*/
|
|
3304
|
+
permission: 'plan' | 'default' | 'acceptEdits';
|
|
3305
|
+
/** 整个工具免确认(粗)。多数模板不用它 —— 逐条列出来的清单才审得动 */
|
|
3306
|
+
allowTools?: readonly string[];
|
|
3307
|
+
allowOperations?: readonly OperationType[];
|
|
3308
|
+
/** 规则(细)。⚠️ **路径一律写成相对的**,理由见 {@link ScheduleTemplate} */
|
|
3309
|
+
allowRules?: readonly string[];
|
|
3310
|
+
/**
|
|
3311
|
+
* 墙钟超时(毫秒)。不给就用引擎缺省的 15 分钟。
|
|
3312
|
+
*
|
|
3313
|
+
* 只有「测试 flake 复盘」给了 —— 它真的要跑一遍测试,而缺省那把尺子会在
|
|
3314
|
+
* 半路把它砍掉。**这一格存在的意义就是让那一张不至于生下来就超时**。
|
|
3315
|
+
*/
|
|
3316
|
+
timeoutMs?: number;
|
|
3317
|
+
}
|
|
3318
|
+
/**
|
|
3319
|
+
* 六张。顺序就是界面上卡片的顺序:**两张每天的排在前面** ——
|
|
3320
|
+
* 「每天一次」比「每周一次」更容易让人第一次就试出效果,而这一屏的空状态
|
|
3321
|
+
* 要答的问题是「这东西能替我做什么」。
|
|
3322
|
+
*/
|
|
3323
|
+
declare const SCHEDULE_TEMPLATES: readonly ScheduleTemplate[];
|
|
3324
|
+
|
|
3325
|
+
/**
|
|
3326
|
+
* 「还欠着的那几张欠条」——
|
|
3327
|
+
* [方案 45](../../../docs/verify/VERIFY_RECORD-45-automation.md) §八 ① / ③ 的素材。
|
|
3328
|
+
*
|
|
3329
|
+
* 三个界面读的是同一个数:dock 上那一条「自动化待确认」、侧栏「自动化」入口上
|
|
3330
|
+
* 那个点、TUI 启动时那一行。**它们必须是同一个数** —— 三处各数一遍的下场是
|
|
3331
|
+
* 侧栏有点、点进去 dock 上什么都没有。
|
|
3332
|
+
*
|
|
3333
|
+
* ## 一、为什么这份投影住在 protocol
|
|
3334
|
+
*
|
|
3335
|
+
* 它是**纯集合运算**:手上有哪几条任务、每条最近跑过什么、清单里已经有哪几条规则。
|
|
3336
|
+
* 没有日期算术、不读盘、不查权限,一个字的 core 知识都不需要。
|
|
3337
|
+
* 而它的消费者有四个:`server`(那条端点)、`cli`(`--fix` 和 TUI 那行开机提示)、
|
|
3338
|
+
* `packages/web/dev` 那台假服务端、以及将来任何一个嵌入宿主。
|
|
3339
|
+
* [AGENTS.md](../../../AGENTS.md) 分层表里 protocol 那一行写着
|
|
3340
|
+
* 「跨包共享的东西**只能**放这里」,这就是那一行说的情况。
|
|
3341
|
+
*
|
|
3342
|
+
* ⚠️ **这一份落地之前,那个集合运算在仓库里有三份**(`server` 的 `fixSchedule`、
|
|
3343
|
+
* `cli` 的 `applyFix`、假服务端的 `fakeScheduleFix`),逐字一样的六行。
|
|
3344
|
+
* 三份都是「往授权清单里加规则」这条路上的判定 —— 也就是**提权路径上的重复实现**。
|
|
3345
|
+
* 收成一份是这个文件存在的第二个理由,比第一个理由更硬。
|
|
3346
|
+
*
|
|
3347
|
+
* ## 二、「还欠着」= 算得出规则、而且规则还不在清单里
|
|
3348
|
+
*
|
|
3349
|
+
* 两道筛,**缺一道这个数就会自己卡住**:
|
|
3350
|
+
*
|
|
3351
|
+
* 1. **`suggestedRule` 必须有值。** 算不出精确规则的那几条(目标为空的操作)
|
|
3352
|
+
* 进不了这份清单 —— dock 上唯一的动作是「加进授权清单」,而对它们那一下
|
|
3353
|
+
* 什么都加不了。一条点了没用的通知比没有通知坏(设计稿决定 20 ①),
|
|
3354
|
+
* 而且它**永远清不掉**:没有任何动作能让它消失。
|
|
3355
|
+
* ⚠️ 它们并没有被藏起来:「运行记录」那一屏上那一轮照旧列着它们、
|
|
3356
|
+
* 照旧写着「这一条反推不出精确规则」。这里筛掉的是**通知**,不是事实。
|
|
3357
|
+
* 2. **规则还不在 `allowRules` 里。** 用户按过那一下之后规则就进清单了,
|
|
3358
|
+
* 于是这个数**自己减一**。这条让整件事不需要「读标记」那套东西 ——
|
|
3359
|
+
* 没有迁移、没有「哪个界面算看过」这个产品判断、也没有一个只能靠
|
|
3360
|
+
* 「我看过了」来清的角标。
|
|
3361
|
+
*
|
|
3362
|
+
* ## ⚠️ 三、这个数**不含**「上一次没跑成」的那几条,而那是一条判断不是遗漏
|
|
3363
|
+
*
|
|
3364
|
+
* `failed` / `timeout` / `budget` 三档同样是「你该知道的事」,但它们**没有下一步
|
|
3365
|
+
* 动作**:屏幕上给不出一个按钮,用户只能去看运行记录。把它们算进来的话,这个点
|
|
3366
|
+
* 会一直亮着,直到那条任务下一次跑成 —— 而在那之前它已经不再指向任何可做的事,
|
|
3367
|
+
* 也就退化成了一个「有东西发生过」的常亮灯。
|
|
3368
|
+
*
|
|
3369
|
+
* 三档失败在「运行记录」那一屏上有自己的三态色(✅ ⚠️ ❌),一条都没藏。
|
|
3370
|
+
* 📮 真要给它们一个计数,前置是一份**读标记**(`schedule` 那个 namespace 加一次
|
|
3371
|
+
* 迁移 + 一条写端点 + 「web 打开那个 tab 算不算看过、TUI 启动算不算」这个产品
|
|
3372
|
+
* 判断)。那是独立的一次改动,不该顺手塞进这一轮。
|
|
3373
|
+
*/
|
|
3374
|
+
|
|
3375
|
+
/**
|
|
3376
|
+
* 一条**算得出规则**的欠条。
|
|
3377
|
+
*
|
|
3378
|
+
* `suggestedRule` 从可选收成必填 —— 这不是换个写法:它让「dock 上那个按钮
|
|
3379
|
+
* 一定加得进东西」变成一件类型上成立的事,而不是一句要人记住的注释。
|
|
3380
|
+
*/
|
|
3381
|
+
type FixableApproval = SchedulePendingApproval & {
|
|
3382
|
+
suggestedRule: string;
|
|
3383
|
+
};
|
|
3384
|
+
/**
|
|
3385
|
+
* dock / 侧栏 / TUI 三处共用的一条。
|
|
3386
|
+
*
|
|
3387
|
+
* 带 `scheduleName` 而不是只带 id:这条通知的措辞必须**有宾语**
|
|
3388
|
+
* (「《每日仓库巡检》差一条授权」,不是「有 1 条待确认」)——
|
|
3389
|
+
* 判据和跨会话待办条那一条同源(`web/src/dock/crossbar.tsx` 文件头
|
|
3390
|
+
* 「措辞必须带宾语」那一节):一个不带宾语的计数器逼用户挨个点开去看。
|
|
3391
|
+
*/
|
|
3392
|
+
interface SchedulePendingItem {
|
|
3393
|
+
scheduleId: string;
|
|
3394
|
+
scheduleName: string;
|
|
3395
|
+
/** 欠条挂在哪一次运行上。`POST /api/schedules/:id/fix` 收的就是它 */
|
|
3396
|
+
runId: string;
|
|
3397
|
+
/** 那一次运行的开始时刻(epoch 毫秒)。界面上那句「已欠了多久」 */
|
|
3398
|
+
at: number;
|
|
3399
|
+
/** 还没进清单的那几条,**至少一条**(空的那几轮压根不进这份清单) */
|
|
3400
|
+
approvals: readonly FixableApproval[];
|
|
3401
|
+
}
|
|
3402
|
+
/**
|
|
3403
|
+
* 这一轮的欠条里,有哪几条规则还不在清单里。**去重,顺序照欠条的顺序。**
|
|
3404
|
+
*
|
|
3405
|
+
* 三个调用点:这个文件自己(算那个计数)、`POST /api/schedules/:id/fix`
|
|
3406
|
+
* (真往清单里写的那一下)、`epoch schedule run --fix`。
|
|
3407
|
+
* ⚠️ **三处必须是同一个函数**:它决定的是「往授权清单里加哪几条规则」,
|
|
3408
|
+
* 而三份实现走散的方向可能是提权(一边加了另一边没加)。
|
|
3409
|
+
*/
|
|
3410
|
+
declare function unfixedRules(allowRules: readonly string[], approvals: readonly SchedulePendingApproval[]): string[];
|
|
3411
|
+
/**
|
|
3412
|
+
* 手上那几条任务 + 最近那些运行 → 该通知的清单。
|
|
3413
|
+
*
|
|
3414
|
+
* ## ⚠️ 同一条任务只报**最近**那一次,不是每一次
|
|
3415
|
+
*
|
|
3416
|
+
* 一条「每天 9 点」的任务每天记同一条欠条,一周就是七条一模一样的通知。
|
|
3417
|
+
* 所以按任务收拢、取最近那一次。这不会漏掉东西:修最近那一次加进去的规则
|
|
3418
|
+
* 通常把老的那几次也一起覆盖了;万一老的那次欠的是**另一条**规则,
|
|
3419
|
+
* 它在下一次取数时自然浮上来成为「最近的那条未解决的」。
|
|
3420
|
+
*
|
|
3421
|
+
* ⚠️ 判的是「最近的**还欠着的**那一次」,不是「最近的那一次」:一条任务昨天
|
|
3422
|
+
* 卡了、今天跑成了,昨天那条欠条**仍然欠着**(清单里那条规则还是没加),
|
|
3423
|
+
* 下一次到点还会再卡一遍。拿「最近一次的状态」当判据会把它静默吞掉。
|
|
3424
|
+
*
|
|
3425
|
+
* @param defs 全部任务(`ScheduleControl.list()`)
|
|
3426
|
+
* @param runs 最近那些运行,**最新的在前**(`ScheduleControl.recentRuns()` 的顺序)
|
|
3427
|
+
*/
|
|
3428
|
+
declare function collectPendingApprovals(defs: readonly ScheduleDefinition[], runs: readonly ScheduleRun[]): SchedulePendingItem[];
|
|
3429
|
+
|
|
2635
3430
|
/**
|
|
2636
3431
|
* 工作区信任契约。
|
|
2637
3432
|
*
|
|
@@ -2795,45 +3590,246 @@ interface EpochFilePart {
|
|
|
2795
3590
|
/** 文件原始字节数,用于展示 */
|
|
2796
3591
|
bytes?: number;
|
|
2797
3592
|
}
|
|
2798
|
-
type EpochContentPart = EpochTextPart | EpochImagePart | EpochFilePart;
|
|
2799
|
-
/** 用户消息内容 —— 纯文本或多模态部件数组 */
|
|
2800
|
-
type EpochUserContent = string | EpochContentPart[];
|
|
2801
|
-
/** 附件在纯文本投影里长什么样。**不吐文件内容** —— 内容走 `parts` 那一路 */
|
|
2802
|
-
declare function filePartSummary(part: EpochFilePart): string;
|
|
2803
3593
|
/**
|
|
2804
|
-
*
|
|
2805
|
-
* 文件同样只折成一行占位,**不吐内容**。
|
|
3594
|
+
* 引用的会话为什么只有标题没有内容(方案 53 §4.2)。
|
|
2806
3595
|
*
|
|
2807
|
-
*
|
|
2808
|
-
*
|
|
3596
|
+
* 前五个是**稳定的错误码**,给宿主路由用(headless / 嵌入宿主要能按码分流,
|
|
3597
|
+
* TUI 和 Web 要各自渲染)。清单本体是 {@link SESSION_REFERENCE_ERROR_CODES},
|
|
3598
|
+
* 那才是同构位置的唯一真源。
|
|
2809
3599
|
*/
|
|
2810
|
-
|
|
2811
|
-
|
|
3600
|
+
type SessionOmitReason =
|
|
3601
|
+
/** `@:xxx` 匹配不到**唯一**的一段会话(零个,或好几个) */
|
|
3602
|
+
'invalid-reference'
|
|
3603
|
+
/** 引用的是当前这段会话自己 */
|
|
3604
|
+
| 'self-reference'
|
|
3605
|
+
/** 超过每条消息能引用的会话个数 */
|
|
3606
|
+
| 'too-many'
|
|
3607
|
+
/** 会话可读性判定不放行(方案 48 的那一处判定,见 core/session/authorization.ts) */
|
|
3608
|
+
| 'not-authorized'
|
|
3609
|
+
/** 这一条消息的附件总量已经用完 */
|
|
3610
|
+
| 'budget-exceeded'
|
|
2812
3611
|
/**
|
|
2813
|
-
*
|
|
3612
|
+
* 会话是 resume 出来的,引用的内容**不重读**。
|
|
2814
3613
|
*
|
|
2815
|
-
*
|
|
2816
|
-
*
|
|
3614
|
+
* 判据逐字同 {@link FileOmitReason} 的 `not-restored`:内容当初是过了可读性
|
|
3615
|
+
* 判定才附上的,而那次判定发生在另一个上下文里。恢复时无声地再读一遍等于让
|
|
3616
|
+
* 一次旧的授权在新的上下文里继续生效。模型仍然看得见「用户引用过这段会话」,
|
|
3617
|
+
* 要内容就自己调 `session_search`(方案 48 那条路,有工具审批)。
|
|
2817
3618
|
*
|
|
2818
|
-
*
|
|
2819
|
-
* 会让 loop / codec / 压缩器每处都得判一次类型。改成加一个 `parts` 旁路:
|
|
2820
|
-
* `content` 仍然是权威的**文本投影**(压缩器、审计、持久化都只看它),
|
|
2821
|
-
* `parts` 只在 sdk-adapter 那一处被读走。两者的关系由 `contentToText` 保证。
|
|
3619
|
+
* **它不是错误码**,所以不在 {@link SESSION_REFERENCE_ERROR_CODES} 里。
|
|
2822
3620
|
*/
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
3621
|
+
| 'not-restored';
|
|
3622
|
+
/**
|
|
3623
|
+
* 五个稳定的错误码(方案 53 §4.2)—— **同构位置的唯一真源**。
|
|
3624
|
+
*
|
|
3625
|
+
* 为什么要稳定的码而不是一句话:headless / 嵌入宿主要能路由这些失败
|
|
3626
|
+
* (`docs/HEADLESS.md` 的退出码那套已经是这个思路),而且 TUI 和 Web 要各自渲染。
|
|
3627
|
+
*
|
|
3628
|
+
* `not-restored` 刻意不在里面 —— 它是「会话恢复了」这个事实,不是一次失败。
|
|
3629
|
+
*/
|
|
3630
|
+
declare const SESSION_REFERENCE_ERROR_CODES: readonly SessionOmitReason[];
|
|
3631
|
+
/**
|
|
3632
|
+
* 引用另一段会话的部件 —— 用户用 `@:xxx` 提到的那段会话(方案 53)。
|
|
3633
|
+
*
|
|
3634
|
+
* ## 塞进来的是那段会话的「当前面」,不是原始全量(§二)
|
|
3635
|
+
*
|
|
3636
|
+
* 一个跑了两小时、压缩过三次的会话,`messages` 表里有 800 条,但模型当时能看到
|
|
3637
|
+
* 的只有 60 条(其余 `active = 0`)。这里带的是**那 60 条**。三条理由:800 条
|
|
3638
|
+
* 任何预算都塞不下;那 60 条就是这段会话的结论(压缩器已经把前面浓缩进去了);
|
|
3639
|
+
* 原始全量里有已经被推翻的内容(「先试了 A 不行改用 B」,压缩之后留下的是 B)。
|
|
3640
|
+
*
|
|
3641
|
+
* ## 它是**快照**,不是活引用
|
|
3642
|
+
*
|
|
3643
|
+
* 引用是发消息那一刻的快照,之后源会话被归档、删除、继续聊都不影响这一条。
|
|
3644
|
+
* **别去做「引用失效」的检测** —— 那等于让一条已经发出去的消息随时间变形。
|
|
3645
|
+
*
|
|
3646
|
+
* ## `text` 缺席是常态
|
|
3647
|
+
*
|
|
3648
|
+
* 匹配不到、引用自己、超上限、判定不放行时**只留 `ref` 和原因**。这时候模型
|
|
3649
|
+
* 看到的是「用户引用了某段会话,但内容没给你,原因是 X」—— 要内容它可以自己调
|
|
3650
|
+
* `session_search`(方案 48)。这保证了 `@:` 永远不会成为一条比 `session_search`
|
|
3651
|
+
* **更宽松**的读取通道,和 `@文件` 对 `file_read` 那条判据逐字对应。
|
|
3652
|
+
*/
|
|
3653
|
+
interface EpochSessionPart {
|
|
3654
|
+
type: 'session';
|
|
3655
|
+
/** 用户敲的那个引用串(`@:` 后面那一截),原样保留用于显示 */
|
|
3656
|
+
ref: string;
|
|
3657
|
+
/** 匹配到的会话 id。**没匹配上时缺席** */
|
|
3658
|
+
sessionId?: string;
|
|
3659
|
+
/** 那段会话的标题(空标题时是 sessionId,见方案 53 §5.3) */
|
|
3660
|
+
title?: string;
|
|
3661
|
+
/** 那段会话的工作目录,用于让人 / 模型看出「这是哪个项目的会话」 */
|
|
3662
|
+
cwd?: string;
|
|
3663
|
+
/** 当前面的正文。**缺席时看 `omitted`** */
|
|
3664
|
+
text?: string;
|
|
3665
|
+
/** 没给内容的原因;给了 `text` 时不出现 */
|
|
3666
|
+
omitted?: SessionOmitReason;
|
|
3667
|
+
/** 内容被截断到多少字节。给了就是截断过 —— 截断**永远不静默** */
|
|
3668
|
+
truncatedTo?: number;
|
|
3669
|
+
/** 当前面有多少条消息(`active = 1` 的那些) */
|
|
3670
|
+
messageCount?: number;
|
|
3671
|
+
/** 那段会话一共多少条(含被压掉的)。两个数不等就说明它压缩过 */
|
|
3672
|
+
totalMessages?: number;
|
|
3673
|
+
}
|
|
3674
|
+
type EpochContentPart = EpochTextPart | EpochImagePart | EpochFilePart | EpochSessionPart;
|
|
3675
|
+
/** 用户消息内容 —— 纯文本或多模态部件数组 */
|
|
3676
|
+
type EpochUserContent = string | EpochContentPart[];
|
|
3677
|
+
/**
|
|
3678
|
+
* 附件在**给模型的**纯文本投影里长什么样。**不吐文件内容** —— 内容走 `parts` 那一路。
|
|
3679
|
+
*
|
|
3680
|
+
* 输出**逐字冻住**:门禁在 `packages/protocol/__tests__/message-projection.test.ts`,
|
|
3681
|
+
* 它把五种 `omitted` 原因加截断、不截断两支全钉死了。改一个字那份会当场红 ——
|
|
3682
|
+
* 那不是「用例过时了」,是你正在改模型的输入契约(判据见 {@link OMIT_LABEL})。
|
|
3683
|
+
*/
|
|
3684
|
+
declare function filePartSummary(part: EpochFilePart): string;
|
|
3685
|
+
/**
|
|
3686
|
+
* 引用的会话在纯文本投影里长什么样。**不吐那段会话的正文** —— 同 `filePartSummary`。
|
|
3687
|
+
*
|
|
3688
|
+
* ## 这一行刻意是英文 + 码,不是中文散文
|
|
3689
|
+
*
|
|
3690
|
+
* 它的读者有三个,**没有一个是界面**:模型(经 sdk-adapter 进上下文)、
|
|
3691
|
+
* `messages.content` 那一列(存盘 / 压缩 / 审计 / FTS)、以及宿主的日志。
|
|
3692
|
+
* 界面要展示这一部件时读的是结构化字段(`omitted` / `title` / `messageCount`),
|
|
3693
|
+
* 自己组措辞 —— TUI 那一侧的中文在宿主用 `t()` 拼,见 locales 的 `mention.*`。
|
|
3694
|
+
*
|
|
3695
|
+
* > ⚠️ 「没有一个是界面」这句话 2026-08-24 之前**有个洞**:`contentToText()`
|
|
3696
|
+
* > 会把这一行塞进 tui / web 的气泡里,于是它事实上也是界面的。洞补在
|
|
3697
|
+
* > §34.4 —— 界面改走 `contentToDisplayText()`,那一份把这一行换成
|
|
3698
|
+
* > catalog 的 `message_part.session*`。所以上面那句话现在是真的了。
|
|
3699
|
+
*
|
|
3700
|
+
* 所以这里直接把**那五个稳定错误码**原样写进去(§4.2):宿主能 grep,模型能读,
|
|
3701
|
+
* 而且它不必跟着界面语言变 —— 一条落库的记录跟着 `EPOCH_LANG` 变语言,
|
|
3702
|
+
* 等于把同一段会话的历史劈成两半(`goal/wording.ts` 那条豁免记的是同一个坑)。
|
|
3703
|
+
*
|
|
3704
|
+
* 显示名用标题而不是 sessionId:后者是一串随机字符,对人和模型都没有信息量。
|
|
3705
|
+
* 标题为空的会话在解析那一侧已经用 sessionId 兜过底了(方案 53 §5.3)。
|
|
3706
|
+
*/
|
|
3707
|
+
declare function sessionPartSummary(part: EpochSessionPart): string;
|
|
3708
|
+
/**
|
|
3709
|
+
* 取部件数组的纯文本投影:图片折成一句占位,绝不吐 base64;
|
|
3710
|
+
* 文件和引用的会话同样只折成一行占位,**不吐内容**。
|
|
3711
|
+
*
|
|
3712
|
+
* 投影是**存盘、压缩、审计**看的那一份。把几十 KB 的文件内容塞进来,
|
|
3713
|
+
* `sessions.db` 会跟着一起胖,而回放时那份内容本来就能从 `parts` 里拿。
|
|
3714
|
+
*
|
|
3715
|
+
* ## ⚠️ 这一份**只给模型和落库**,界面不许用它(RECORD-40-i18n §34.4)
|
|
3716
|
+
*
|
|
3717
|
+
* 改造前它一个人喂三类消费者,于是把界面切成 English 的人会在**自己那条消息的
|
|
3718
|
+
* 气泡里**看到 `[图片 image/png]` / `[文件 …:二进制文件,未附内容]`。
|
|
3719
|
+
* 这笔债在 §34.4 记了、§34.4 还掉,处置是**分家**:
|
|
3720
|
+
*
|
|
3721
|
+
* | 谁要 | 用哪一份 | 为什么 |
|
|
3722
|
+
* | ---- | -------- | ------ |
|
|
3723
|
+
* | 模型(`sdk-adapter.ts` / `agent/loop.ts` / `runtime/agent-session.ts`) | **这一个** | 模型输入契约 + eval 指纹,跟着 locale 变就漂(§33.2) |
|
|
3724
|
+
* | `messages.content` 那一列(存盘 / 压缩 / 审计 / FTS) | **这一个** | 一条落库的记录跟着 `EPOCH_LANGUAGE` 变语言,等于把同一段会话的历史劈成两半 |
|
|
3725
|
+
* | 界面(tui 的历史项、web 的气泡) | `contentToDisplayText(content, t)` | 那一份走 catalog 的 `message_part.*`,住在两个宿主里 |
|
|
3726
|
+
*
|
|
3727
|
+
* 界面那一份**落在宿主层**而不是这儿,判据同 §34.6 给 `view` 那 1 处的做法
|
|
3728
|
+
* (兜底文案挪给渲染端):这个包的依赖白名单是空的,`t()` 只能由有它的那一层调。
|
|
3729
|
+
*/
|
|
3730
|
+
declare function contentToText(content: EpochUserContent): string;
|
|
3731
|
+
/**
|
|
3732
|
+
* 「上一轮实际动过手」在**给模型的**历史里长什么样(2026-08-28)。
|
|
3733
|
+
*
|
|
3734
|
+
* ## 它修的是什么
|
|
3735
|
+
*
|
|
3736
|
+
* 喂模型的那份历史只有 user / assistant 两种角色的**文本**
|
|
3737
|
+
* (`runtime/src/agent-session.ts` 的累积、`session-store.ts` 的 `loadHistory()`),
|
|
3738
|
+
* 工具调用和结果一条都不在里面 —— 这是刻意的(孤立的 tool 消息会被 provider 拒掉,
|
|
3739
|
+
* 全量 transcript 是另一个体量的事)。代价此前没人算:一段五条消息的会话,
|
|
3740
|
+
* 模型看到的是**五条又长又具体的指令,配四句十来个字的「办完了」**。
|
|
3741
|
+
* 早期那几条指令的字数远大于「它已经做完了」的证据量,于是模型把它们当成还在
|
|
3742
|
+
* 生效的待办,回头去做 —— 而用户刚发的那一条被摊薄。实测在
|
|
3743
|
+
* `packages/runtime/__tests__/context-recency.test.ts`。
|
|
3744
|
+
*
|
|
3745
|
+
* 这一行就是补那份证据:不复原过程,只留一句「这一轮真的调了这些工具」。
|
|
3746
|
+
*
|
|
3747
|
+
* ## 判据用**结果**而不是调用
|
|
3748
|
+
*
|
|
3749
|
+
* `EpochToolCall` 上有工具名,但一次跑到一半被中止的轮次里那些调用**从没执行过**
|
|
3750
|
+
* (判据逐字同 `runtime/src/session-file-changes.ts` 的边界 1)。而一条
|
|
3751
|
+
* `EpochToolResult` 存在就意味着它跑完了 —— 拿结果当输入,「执行过没有」这件事
|
|
3752
|
+
* 不需要另外判一次。
|
|
3753
|
+
*
|
|
3754
|
+
* ⚠️ **它分不出成功和失败**:落盘的结果上没有 `success`(同上那份的边界 2)。
|
|
3755
|
+
* 所以措辞是「调用过」而不是「做成了」—— 一次被权限拒掉的 `file_write`
|
|
3756
|
+
* 在这里照样留一笔,而那句话仍然是真的。
|
|
3757
|
+
*
|
|
3758
|
+
* ## 为什么住在 protocol、且是冻住的中文
|
|
3759
|
+
*
|
|
3760
|
+
* 它的读者是**模型**,和 {@link contentToText} / {@link filePartSummary} 同一类,
|
|
3761
|
+
* 所以走同一条规矩:住在这个零依赖的包里、逐字冻住、**不跟着界面语言变**
|
|
3762
|
+
* (门禁在 `packages/protocol/__tests__/message-projection.test.ts`)。一条进了
|
|
3763
|
+
* 上下文的历史跟着 `EPOCH_LANGUAGE` 变语言,等于把同一段会话劈成两半。
|
|
3764
|
+
*
|
|
3765
|
+
* 只有工具名,不带参数 / 路径:带路径要 `affectedPaths()` + 工作目录,而那两样
|
|
3766
|
+
* 住在 core 和装配层,拿不到的地方(`loadHistory()`)就会退化成另一种措辞 ——
|
|
3767
|
+
* 两条路产出的历史一旦不一样,症状就是「刷新之后模型看到的和刚才不是同一段」。
|
|
3768
|
+
*
|
|
3769
|
+
* ⚠️ **调用方要的多半是 {@link assistantTurnText},不是这一个** —— 那一份连
|
|
3770
|
+
* 「正文和这一行之间怎么接」一起定了,而那正是两条路会走散的地方。
|
|
3771
|
+
*
|
|
3772
|
+
* @param results 这**一轮**(不是一步)执行过的全部工具结果,按发生序
|
|
3773
|
+
* @returns 空数组时是空串 —— 这一轮没动手,那就没有这句话可说
|
|
3774
|
+
*/
|
|
3775
|
+
declare function turnDigest(results: readonly Pick<EpochToolResult, 'toolName'>[]): string;
|
|
3776
|
+
/**
|
|
3777
|
+
* 一轮的 assistant 消息在**给模型的历史**里长什么样:正文 + {@link turnDigest}。
|
|
3778
|
+
*
|
|
3779
|
+
* ## 为什么这条胶水也得住在这儿
|
|
3780
|
+
*
|
|
3781
|
+
* 因为它有**两个调用方,走两条路,产出必须逐字节相同**:
|
|
3782
|
+
*
|
|
3783
|
+
* - 直播:`runtime/src/agent-session.ts` 用 `TurnStepCollector` 收的那一轮;
|
|
3784
|
+
* - 恢复:`runtime/src/session-store.ts` 的 `loadHistory()` 从 DB 行折回来的那一轮。
|
|
3785
|
+
*
|
|
3786
|
+
* 两边各写一次 `text + '\n\n' + digest` 的下场是:某天一边多一个换行,
|
|
3787
|
+
* 而症状是**刷新页面之后模型看到的历史和刷新前不是同一段** —— `reloadHistory()`
|
|
3788
|
+
* 的 JSDoc 里那句「它们一旦分叉,症状就是屏幕上和模型看到的对不上」骂的正是这个。
|
|
3789
|
+
* 门禁是 `packages/runtime/__tests__/context-recency.test.ts` 里那条逐字节比对。
|
|
3790
|
+
*
|
|
3791
|
+
* 正文为空、只有 digest 时**不留前导空行**:那一轮模型只调了工具没说话
|
|
3792
|
+
* (`loop.ts` 允许),这时这条消息的全部内容就是那一行。
|
|
3793
|
+
*
|
|
3794
|
+
* @returns 空串 = 这一轮既没正文也没动手,调用方**别往历史里塞这一条**
|
|
3795
|
+
*/
|
|
3796
|
+
declare function assistantTurnText(text: string, results: readonly Pick<EpochToolResult, 'toolName'>[]): string;
|
|
3797
|
+
/**
|
|
3798
|
+
* 部件类型的穷尽性守卫 —— **这是「加了一种部件却漏了一处分支」的那道会红的门禁**。
|
|
3799
|
+
*
|
|
3800
|
+
* 消费 `EpochContentPart` 的地方有五处(这里、`sdk-adapter` 的 `toSdkPart`、
|
|
3801
|
+
* `session/payload` 的 `persistParts` / `restoreParts`、以及展示层),改造前它们
|
|
3802
|
+
* 全都写成「if text / if file / 剩下的当图片」。那个形状下加一种部件**不会有
|
|
3803
|
+
* 任何东西变红**:新部件会被静默当成图片,而症状是模型收到一个 `image:` 里
|
|
3804
|
+
* 塞着会话正文的请求,provider 报 400。
|
|
3805
|
+
*/
|
|
3806
|
+
declare function assertNeverPart(part: never): never;
|
|
3807
|
+
type EpochMessageRole = 'system' | 'user' | 'assistant' | 'tool';
|
|
3808
|
+
/**
|
|
3809
|
+
* 一条会话消息。
|
|
3810
|
+
*
|
|
3811
|
+
* `content` 永远是字符串(tool 消息为空串,内容在 `toolResults` 里),
|
|
3812
|
+
* 这样「取这条消息的文本」永远不需要判类型。
|
|
3813
|
+
*
|
|
3814
|
+
* 多模态**不是**把 `content` 放宽成联合类型 —— 那正是本文件开头骂过的病,
|
|
3815
|
+
* 会让 loop / codec / 压缩器每处都得判一次类型。改成加一个 `parts` 旁路:
|
|
3816
|
+
* `content` 仍然是权威的**文本投影**(压缩器、审计、持久化都只看它),
|
|
3817
|
+
* `parts` 只在 sdk-adapter 那一处被读走。两者的关系由 `contentToText` 保证。
|
|
3818
|
+
*/
|
|
3819
|
+
interface EpochMessage {
|
|
3820
|
+
role: EpochMessageRole;
|
|
3821
|
+
content: string;
|
|
3822
|
+
/**
|
|
3823
|
+
* 多模态部件。给了就以它为准喂模型,`content` 退化成它的文本投影。
|
|
3824
|
+
* **不落 SQLite** —— 图片走 artifact 目录,DB 里只留 content 的占位文本。
|
|
3825
|
+
*/
|
|
3826
|
+
parts?: EpochContentPart[];
|
|
3827
|
+
/** assistant 要求的工具调用;与 content 并存(模型可以边说话边调工具) */
|
|
3828
|
+
toolCalls?: EpochToolCall[];
|
|
3829
|
+
/** role 为 'tool' 时携带的执行结果 */
|
|
3830
|
+
toolResults?: EpochToolResult[];
|
|
3831
|
+
/**
|
|
3832
|
+
* 这一步模型的思考原文(推理模型的 `reasoning-delta` 累加)。2026-08-18。
|
|
2837
3833
|
*
|
|
2838
3834
|
* **只给人看,不回灌模型**:`sdk-adapter.ts` 一个字都不读它。加它是因为
|
|
2839
3835
|
* 刷新页面之前屏幕上有「深度思考」那一段,刷新之后整段消失 —— 而那不是
|
|
@@ -2852,23 +3848,262 @@ interface EpochMessage {
|
|
|
2852
3848
|
}
|
|
2853
3849
|
|
|
2854
3850
|
/**
|
|
2855
|
-
*
|
|
3851
|
+
* `@` 提及的抽取规则、附件的体量上限、以及会话引用的装配
|
|
3852
|
+
* (方案 25 §2.2、方案 53 §1.2 / §二 / §4.1)。
|
|
2856
3853
|
*
|
|
2857
3854
|
* ## 为什么这条规则住在 protocol
|
|
2858
3855
|
*
|
|
2859
3856
|
* 它有**两个**消费方,而且两边必须逐字一致:
|
|
2860
3857
|
*
|
|
2861
|
-
* - TUI(`app.tsx`)—— 提交时要知道这条消息提到了哪几个文件
|
|
2862
|
-
* - 引擎(`core/context/mentions.ts`)——
|
|
3858
|
+
* - TUI(`app.tsx`)—— 提交时要知道这条消息提到了哪几个文件 / 哪几段会话
|
|
3859
|
+
* - 引擎(`core/context/mentions.ts`)—— 拿着这些提及去读内容
|
|
2863
3860
|
*
|
|
2864
3861
|
* 两边各写一份的后果不是崩溃,是**静默错位**:面板让你选了一个文件、
|
|
2865
3862
|
* 抽取那侧却不认它,于是内容没附上,而界面上看不出任何异常。
|
|
2866
3863
|
* tui 不许 import core,所以这条规则只能落在两边都够得着的地方。
|
|
2867
3864
|
*
|
|
3865
|
+
* ## 下半截(体量上限 + `attachSessionSurface`)为什么也在这儿(方案 53 PR-4)
|
|
3866
|
+
*
|
|
3867
|
+
* 同一条判据,只是消费方换了一对:会话引用的装配从 2026-08-21 起有**两条路**——
|
|
3868
|
+
*
|
|
3869
|
+
* | 路 | 谁在解析 | `t()` 从哪来 |
|
|
3870
|
+
* | --------------- | ----------------------------------- | --------------------- |
|
|
3871
|
+
* | TUI / CLI | `core/src/context/mentions.ts` | `infra` |
|
|
3872
|
+
* | Web | `server/src/mentions.ts`(PR-4) | `runtime` 转出来的那份 |
|
|
3873
|
+
*
|
|
3874
|
+
* 而 **`@epoch-agent/server` 不许 import core**(`check-layers.mjs` 的 ALLOWED 表),
|
|
3875
|
+
* 于是「50KB 怎么截、截了之后 `EpochSessionPart` 长什么样」这件事只有两个落点:
|
|
3876
|
+
* 在服务端抄第二遍,或者搬到两边都够得着的这里。抄第二遍的形态很具体 ——
|
|
3877
|
+
* TUI 上一段会话附了 48 条、web 上同一段附了 60 条,两边都不报错,
|
|
3878
|
+
* 而「模型到底看见了什么」从此有两个答案。
|
|
3879
|
+
*
|
|
3880
|
+
* ⚠️ **那五个错误码(`SESSION_REFERENCE_ERROR_CODES`)住在 `message.ts` 已经是同一条
|
|
3881
|
+
* 判据的先例**;这一份搬的是产出那个码的算术。
|
|
3882
|
+
*
|
|
3883
|
+
* ⚠️ **上限那张表整张搬,不搬一半。** 文件那三个数(单文件 / 每条几个 / 总量)和会话
|
|
3884
|
+
* 那两个是**四道关卡的第四道**同一张表,而总量那一个本来就由两者共享 ——
|
|
3885
|
+
* 拆成「文件的在 core、会话的在 protocol」的下场是两半各自漂移,
|
|
3886
|
+
* 而它们相加才是「这一条消息能有多大」。
|
|
3887
|
+
*
|
|
3888
|
+
* ⚠️ **这个文件里不许出现 `Buffer`。** protocol 会被 `@epoch-agent/web` 打进浏览器包,
|
|
3889
|
+
* 而 `Buffer` 在浏览器里不存在。字节口径一律走 `TextEncoder` / `TextDecoder`
|
|
3890
|
+
* (两边都有,且和 `Buffer.byteLength(s, 'utf8')` 同值)。
|
|
3891
|
+
*
|
|
2868
3892
|
* 补全面板的**触发**条件(`tui/completion/files.ts` 的 `mentionAtCursor`)
|
|
2869
3893
|
* 也必须和这里对齐 —— 那一侧是「光标处」的判定,逻辑不同但边界相同。
|
|
3894
|
+
*
|
|
3895
|
+
* ## 两种提及,一个前缀字符分开(方案 53 §1.2)
|
|
3896
|
+
*
|
|
3897
|
+
* ```
|
|
3898
|
+
* @src/a.ts → 文件(方案 25)
|
|
3899
|
+
* @:ses_abc… → 会话(方案 53)
|
|
3900
|
+
* ```
|
|
3901
|
+
*
|
|
3902
|
+
* 用 `@:` 而不是让解析器去猜「这个字符串是路径还是会话」。判据是**猜会错**:
|
|
3903
|
+
* 一个叫 `refactor` 的会话和一个叫 `refactor` 的目录长得一样,而猜错的后果是
|
|
3904
|
+
* 「我明明要引用那个会话,它给我塞了一个目录」。
|
|
3905
|
+
*
|
|
3906
|
+
* ⚠️ **两种提及走的是同一条正则、同一套边界**(见下面那三条规则)。会话那一种
|
|
3907
|
+
* 只是在拿到 `@` 后面那一截之后多剥一个 `:` —— 分成两条正则的话,
|
|
3908
|
+
* 「`@` 前面必须是行首或空白」这条约束就有了两个实现,而它是挡住邮箱
|
|
3909
|
+
* `a@b.com` 的唯一一道。
|
|
3910
|
+
*/
|
|
3911
|
+
|
|
3912
|
+
/**
|
|
3913
|
+
* 会话提及的前缀。**只有这一个字符**:`@:` 之后整截都是会话的匹配串
|
|
3914
|
+
* (补全面板插进来的是 sessionId,手打时是标题 / cwd 的子串)。
|
|
3915
|
+
*/
|
|
3916
|
+
declare const SESSION_MENTION_PREFIX = ":";
|
|
3917
|
+
/** 一条消息里提到的东西,按种类分开 */
|
|
3918
|
+
interface MentionSet {
|
|
3919
|
+
/** `@路径` 提到的文件,去重保序 */
|
|
3920
|
+
files: string[];
|
|
3921
|
+
/** `@:xxx` 提到的会话匹配串(**不含** `:` 前缀),去重保序 */
|
|
3922
|
+
sessions: string[];
|
|
3923
|
+
}
|
|
3924
|
+
/**
|
|
3925
|
+
* 从一行输入里抽出所有 `@` 提及。
|
|
3926
|
+
*
|
|
3927
|
+
* 三条规则(两种提及共用):
|
|
3928
|
+
*
|
|
3929
|
+
* 1. **`@` 前面必须是行首或空白**。否则邮箱 `a@b.com` 会被当成提及了一个叫
|
|
3930
|
+
* `b.com` 的文件 —— 用户不会理解为什么自己的邮箱变成了附件
|
|
3931
|
+
* 2. **中文标点直接不算路径字符**,英文标点只在结尾剥掉。这两条不对称是刻意的:
|
|
3932
|
+
* 中文里「看一下 @src/a.ts,然后…」逗号后面**不留空格**,靠「剥结尾」救不了
|
|
3933
|
+
* (整串 `src/a.ts,然后…` 中间没有空白);而英文的 `.` `-` `_` 在文件名里
|
|
3934
|
+
* 极常见,不能整个排除,只能剥结尾
|
|
3935
|
+
* 3. **去重保序**,两种提及各自去重。同一个文件在一句话里提两次不该附两遍
|
|
3936
|
+
*
|
|
3937
|
+
* 反斜杠一律换成 `/`:Windows 上用户可能敲 `@src\a.ts`,而候选清单、
|
|
3938
|
+
* 匹配、以及最终的部件 `path` 全都用 `/`。
|
|
3939
|
+
*
|
|
3940
|
+
* ⚠️ **光秃秃一个 `@:` 什么都不产出**(既不是会话也不是文件)。改造前它会变成
|
|
3941
|
+
* 一个叫 `:` 的文件提及,然后附上一句「`@:` 不在工作区里」—— 而用户那一刻
|
|
3942
|
+
* 其实是刚敲下前缀、面板还没选。这一条有用例守着。
|
|
3943
|
+
*/
|
|
3944
|
+
declare function extractAllMentions(text: string): MentionSet;
|
|
3945
|
+
/**
|
|
3946
|
+
* 只要文件那一种。
|
|
3947
|
+
*
|
|
3948
|
+
* 留着这个名字是因为它有一堆调用点,而**它的语义没变**:`@:xxx` 从来就不是
|
|
3949
|
+
* 一个文件提及(改造前它会变成一个叫 `:xxx` 的路径,那是个 bug 不是特性)。
|
|
2870
3950
|
*/
|
|
2871
3951
|
declare function extractMentions(text: string): string[];
|
|
3952
|
+
/** 只要会话那一种(方案 53) */
|
|
3953
|
+
declare function extractSessionMentions(text: string): string[];
|
|
3954
|
+
/**
|
|
3955
|
+
* 这个路径**能不能被 `@` 表达出来**(方案 63:web 那一半的 `@文件`)。
|
|
3956
|
+
*
|
|
3957
|
+
* ## 为什么是一个「跑一遍真规则」的函数,而不是一张字符表
|
|
3958
|
+
*
|
|
3959
|
+
* 补全面板列出来的每一条,用户选中之后会被填进输入框,然后由
|
|
3960
|
+
* {@link extractAllMentions} 再读一遍。这两侧不一致的后果**在界面上完全看不出来**:
|
|
3961
|
+
* 面板让你选了 `src/my file.ts`,抽取那侧只认到 `src/my`,于是模型收到的是
|
|
3962
|
+
* 「用户提到了一个叫 `src/my` 的文件,而它不存在」——
|
|
3963
|
+
* 而屏幕上那一行字看起来一切正常。
|
|
3964
|
+
*
|
|
3965
|
+
* 所以这个判定**不许自己写一遍字符类**(`/[\s,。;]/` 那种)。上面那条
|
|
3966
|
+
* `MENTION_RE` 加上剥尾巴、剥反斜杠、`:` 分流一共四条规则,抄成一张表的那一刻
|
|
3967
|
+
* 它就有了两个实现,而它们会在下一次改规则时分叉 —— 而分叉的形态正是上一段
|
|
3968
|
+
* 那种「看不出来的错位」。这里的做法是**把候选真的过一遍抽取器,看它回来还是不是
|
|
3969
|
+
* 原样**,于是「边界逐字相同」不是一句叮嘱,是一个恒等式。
|
|
3970
|
+
*
|
|
3971
|
+
* 今天被它挡掉的四类(都不是假想的):
|
|
3972
|
+
*
|
|
3973
|
+
* | 路径长什么样 | 为什么 `@` 表达不出来 |
|
|
3974
|
+
* | ------------------- | ------------------------------------------ |
|
|
3975
|
+
* | `src/my file.ts` | 空白就是提及的右边界 |
|
|
3976
|
+
* | `src/a,b.ts` | 中文标点直接不算路径字符 |
|
|
3977
|
+
* | `notes/(1).md` | 结尾的 `)` 会被当标点剥掉 |
|
|
3978
|
+
* | `:weird` | 开头那个 `:` 会把它分流成一个**会话**提及 |
|
|
3979
|
+
*
|
|
3980
|
+
* ⚠️ **挡掉不等于可以不说。** 候选清单少一条而屏幕上没有任何交代,就是这个仓库
|
|
3981
|
+
* 反复在骂的那种沉默截断 —— 所以数出来那个数要一路递到面板上
|
|
3982
|
+
* (`WireFileCandidatesResponse.unmentionable`)。
|
|
3983
|
+
*/
|
|
3984
|
+
declare function isMentionableFilePath(path: string): boolean;
|
|
3985
|
+
/** 单个文件最多附多少字节 */
|
|
3986
|
+
declare const MAX_ATTACH_BYTES: number;
|
|
3987
|
+
/** 一条消息最多附几个文件 */
|
|
3988
|
+
declare const MAX_ATTACH_FILES = 20;
|
|
3989
|
+
/**
|
|
3990
|
+
* 一条消息附件总量上限。**文件和会话共享这一个数**(方案 53 §4.1)——
|
|
3991
|
+
* 两者进的是同一条消息,各自记一本账等于这条消息实际能有 400KB。
|
|
3992
|
+
*/
|
|
3993
|
+
declare const MAX_ATTACH_TOTAL_BYTES: number;
|
|
3994
|
+
/**
|
|
3995
|
+
* 单段会话的当前面最多附多少字节(方案 53 §4.1)。
|
|
3996
|
+
*
|
|
3997
|
+
* 比单文件的 100KB 紧一半:一个会话的当前面比一个文件更容易失控 ——
|
|
3998
|
+
* 文件是人写的,当前面是几十轮对话加工具输出攒出来的。
|
|
3999
|
+
*
|
|
4000
|
+
* ## 2026-08-21 拿真库量过一次(方案 53 §九「单个 50 KB 是否合适」)
|
|
4001
|
+
*
|
|
4002
|
+
* 一个 9 段会话的真实 `sessions.db`:**3 段超过 50KB**(57.4 / 69.8 / 71.9 KB),
|
|
4003
|
+
* 而它们的当前面里 **72% ~ 85% 是工具输出** —— 上面那句「更容易失控」是量出来的,
|
|
4004
|
+
* 不是猜的。三段各自被截成 49.9 / 49.2 / 49.7 KB,分别丢掉 6 / 17 / 24 条。
|
|
4005
|
+
*
|
|
4006
|
+
* **数字不改**,两条判据:一是总量共享 200KB,放到 100KB 之后两段引用就能吃掉
|
|
4007
|
+
* 这条消息的全部预算(而 `@文件` 排在会话前面吃预算,见 `resolveMentions`);
|
|
4008
|
+
* 二是丢掉的那一段是**中间**,两头(压缩摘要 + 结论)都留着。
|
|
4009
|
+
* 真正的结论是**截断是常态而不是边界情况**,所以那句提示语一个都不能省。
|
|
4010
|
+
*/
|
|
4011
|
+
declare const MAX_SESSION_ATTACH_BYTES: number;
|
|
4012
|
+
/**
|
|
4013
|
+
* 一条消息最多引用几段会话。
|
|
4014
|
+
*
|
|
4015
|
+
* **2 而不是 20**(文件那个数):引用三段会话说明该用检索而不是引用 ——
|
|
4016
|
+
* 那时候用户其实不知道自己要哪一段,而 `session_search` 才是干这个的。
|
|
4017
|
+
*/
|
|
4018
|
+
declare const MAX_SESSION_REFS = 2;
|
|
4019
|
+
/**
|
|
4020
|
+
* 当前面里的一条消息。
|
|
4021
|
+
*
|
|
4022
|
+
* **是结构镜像,不是把 core 的类型导出来**:真身是
|
|
4023
|
+
* `core/src/session/reference.ts` 的 `SurfaceMessage`,而它结构上满足这一份
|
|
4024
|
+
* (多出来的字段无所谓)。protocol 不许依赖 core,而这条判据在
|
|
4025
|
+
* [wire-goal.ts](./wire-goal.js) 的文件头已经写过一遍。
|
|
4026
|
+
*/
|
|
4027
|
+
interface SessionSurfaceMessage {
|
|
4028
|
+
role: string;
|
|
4029
|
+
content: string;
|
|
4030
|
+
/** 工具消息才有 */
|
|
4031
|
+
toolName?: string | undefined;
|
|
4032
|
+
}
|
|
4033
|
+
/** 一段会话的当前面。core 的 `SessionSurface` 结构上满足它 */
|
|
4034
|
+
interface SessionSurfaceView {
|
|
4035
|
+
sessionId: string;
|
|
4036
|
+
/** 空标题在读的那一侧已经兜过 sessionId 了(方案 53 §5.3) */
|
|
4037
|
+
title: string;
|
|
4038
|
+
cwd?: string | undefined;
|
|
4039
|
+
/** `active = 1` 的那些,时间正序 */
|
|
4040
|
+
messages: readonly SessionSurfaceMessage[];
|
|
4041
|
+
/** 那段会话一共多少条(含被压掉的) */
|
|
4042
|
+
totalMessages: number;
|
|
4043
|
+
}
|
|
4044
|
+
/**
|
|
4045
|
+
* 这一段引用最后落成了哪一档。
|
|
4046
|
+
*
|
|
4047
|
+
* 四档**穷尽**,而且各配一句不同的话(措辞归调用方 —— TUI 走 `locales` 的
|
|
4048
|
+
* `mention.*`,web 走它自己那份 catalog):
|
|
4049
|
+
*
|
|
4050
|
+
* | 档 | 部件上 | 该说的话 |
|
|
4051
|
+
* | ------------------ | ------------------------------- | ------------------------------ |
|
|
4052
|
+
* | `full` | `text` 全在 | 不必说话,这是正常路径 |
|
|
4053
|
+
* | `truncated` | `text` + `truncatedTo` | 附了几条 / 一共几条 / 上限多少 |
|
|
4054
|
+
* | `empty` | `text` 是空串 | 那段会话还没有留下消息 |
|
|
4055
|
+
* | `budget-exceeded` | 没有 `text`,`omitted` 是那个码 | 这条消息的附件总量用满了 |
|
|
4056
|
+
*/
|
|
4057
|
+
type SessionAttachKind = 'full' | 'truncated' | 'empty' | 'budget-exceeded';
|
|
4058
|
+
/** {@link attachSessionSurface} 的结果 */
|
|
4059
|
+
interface SessionAttachResult {
|
|
4060
|
+
part: EpochSessionPart;
|
|
4061
|
+
kind: SessionAttachKind;
|
|
4062
|
+
/** 真的附上了几条 */
|
|
4063
|
+
kept: number;
|
|
4064
|
+
/** 当前面一共几条 */
|
|
4065
|
+
surfaced: number;
|
|
4066
|
+
/** 这一段实际能用的字节预算;`budget-exceeded` 那一档是 0 */
|
|
4067
|
+
budget: number;
|
|
4068
|
+
/** 正文的字节数。**调用方拿它记这条消息的总账**,没附内容时 0 */
|
|
4069
|
+
bytes: number;
|
|
4070
|
+
}
|
|
4071
|
+
/**
|
|
4072
|
+
* 一段当前面 + 已经用掉的预算 → 一个 `EpochSessionPart`。
|
|
4073
|
+
*
|
|
4074
|
+
* **可读性判定不在这儿**(`SessionReferences.resolve` 已经判过了,那一处和方案 48
|
|
4075
|
+
* 共用)。这个函数只做算术:够不够预算、要不要截、截了之后那几个字段怎么填。
|
|
4076
|
+
*
|
|
4077
|
+
* ⚠️ **`usedBytes` 是这条消息已经用掉的字节**(文件先吃、会话后到,见
|
|
4078
|
+
* `core/src/context/mentions.ts` 的 `resolveMentions`)。传 0 就是「这条消息上
|
|
4079
|
+
* 只有会话引用」—— web 那条路今天恰好是这一档(浏览器上还没有 `@文件`)。
|
|
4080
|
+
*/
|
|
4081
|
+
declare function attachSessionSurface(ref: string, surface: SessionSurfaceView, usedBytes: number): SessionAttachResult;
|
|
4082
|
+
/**
|
|
4083
|
+
* utf8 字节数 —— 预算的单位。
|
|
4084
|
+
*
|
|
4085
|
+
* ⚠️ **`TextEncoder` 而不是 `Buffer.byteLength`**,理由见文件头最后那条 ⚠️:
|
|
4086
|
+
* 这个文件会被打进浏览器包。两者对良构字符串同值。
|
|
4087
|
+
*/
|
|
4088
|
+
declare function utf8ByteLength(text: string): number;
|
|
4089
|
+
/**
|
|
4090
|
+
* 当前面 → 一段文本,超预算时**从中间丢**(方案 53 §4.1「截断并告知,不是拒绝」)。
|
|
4091
|
+
*
|
|
4092
|
+
* ## 为什么丢中间,而不是像文件那样切尾巴
|
|
4093
|
+
*
|
|
4094
|
+
* 当前面的两头恰好是最值钱的两段:**头部**多半是压缩摘要(压缩器把前面几百条
|
|
4095
|
+
* 浓缩成的那一段),**尾部**是这段会话的结论。切尾巴等于把结论丢掉,
|
|
4096
|
+
* 只留头等于把结论丢掉的另一种写法。
|
|
4097
|
+
*
|
|
4098
|
+
* ## 为什么按**消息边界**丢,而不是按字节切
|
|
4099
|
+
*
|
|
4100
|
+
* 按字节切会在一条消息中间断开,模型读到半句话 + 一个替换字符。文件那一路只能
|
|
4101
|
+
* 按字节切(它没有「条」这个结构),这一路有,就该用上。
|
|
4102
|
+
*/
|
|
4103
|
+
declare function packSessionSurface(messages: readonly SessionSurfaceMessage[], budget: number): {
|
|
4104
|
+
text: string;
|
|
4105
|
+
dropped: number;
|
|
4106
|
+
};
|
|
2872
4107
|
|
|
2873
4108
|
/**
|
|
2874
4109
|
* 「这一条 `user` 消息其实是系统在说话」—— 那个前缀的**唯一真源**
|
|
@@ -3791,24 +5026,128 @@ declare const HEADLESS_INPUT_EVENT_TYPES: ReadonlySet<string>;
|
|
|
3791
5026
|
*/
|
|
3792
5027
|
type HeadlessInputEventType = HeadlessInputEvent['type'];
|
|
3793
5028
|
|
|
5029
|
+
/**
|
|
5030
|
+
* 上下文预算的构成(方案 25 §2.5 的 `/context`)。
|
|
5031
|
+
*
|
|
5032
|
+
* ## 为什么值得一个契约
|
|
5033
|
+
*
|
|
5034
|
+
* 我们有压缩器,会在接近上限时自动压。但用户看不见**预算怎么花的** ——
|
|
5035
|
+
* 压缩发生时他只知道「压了」,不知道「为什么这么快就压了」。
|
|
5036
|
+
* 而答案常常很具体:一个 MCP server 的工具定义吃掉了四分之一窗口。
|
|
5037
|
+
*
|
|
5038
|
+
* 形状住在 protocol 是因为 TUI 和 web 都要画它,而两边都不许 import core。
|
|
5039
|
+
*/
|
|
5040
|
+
/** 预算里的一段 */
|
|
5041
|
+
interface ContextSegment {
|
|
5042
|
+
/** 展示名,例如「工具定义」 */
|
|
5043
|
+
label: string;
|
|
5044
|
+
/** 估算的 token 数 */
|
|
5045
|
+
tokens: number;
|
|
5046
|
+
/** 一句补充说明,例如「14 个内置 + 6 个 MCP」。没有就不显示 */
|
|
5047
|
+
detail?: string;
|
|
5048
|
+
}
|
|
5049
|
+
/**
|
|
5050
|
+
* 一次上下文预算快照。
|
|
5051
|
+
*
|
|
5052
|
+
* ⚠️ **不变式:`segments` 之和 === `used`;`limit > 0` 时还有 `used + remaining === limit`。**
|
|
5053
|
+
* 这不是「顺便成立」的性质,是这个结构存在的意义 —— 一个分段账目如果对不上,
|
|
5054
|
+
* 它给用户的每一个结论都是错的。生产它的那一侧必须**由 segments 推出 used**,
|
|
5055
|
+
* 而不是两边各算一遍。有用例钉着(`context-breakdown.test.ts`)。
|
|
5056
|
+
*
|
|
5057
|
+
* 第二条为什么要挂 `limit > 0`:查不到模型元数据时 `limit` 和 `remaining` **都是 0**,
|
|
5058
|
+
* 而 `used` 照样是真数 —— 此时等式必然不成立,这是刻意的(见 `remaining` 的说明)。
|
|
5059
|
+
* 消费侧要么先判 `limit > 0`,要么别用 `limit - used` 反推剩余。
|
|
5060
|
+
*/
|
|
5061
|
+
interface ContextBreakdown {
|
|
5062
|
+
/** 模型的上下文窗口。查不到模型元数据时是 0 */
|
|
5063
|
+
limit: number;
|
|
5064
|
+
/** 已用 = 各段之和 */
|
|
5065
|
+
used: number;
|
|
5066
|
+
/** 剩余 = limit - used。limit 为 0 时也是 0(「不知道」不是「满了」) */
|
|
5067
|
+
remaining: number;
|
|
5068
|
+
segments: ContextSegment[];
|
|
5069
|
+
/**
|
|
5070
|
+
* 给用户的建议,例如「MCP server github 一家占了 18%,考虑改成 Deferred 曝光」。
|
|
5071
|
+
*
|
|
5072
|
+
* 只在**真有可操作动作**时给。「你的历史很长」不算建议 —— 用户对此无能为力,
|
|
5073
|
+
* 而压缩器本来就会处理它。
|
|
5074
|
+
*/
|
|
5075
|
+
hints: string[];
|
|
5076
|
+
}
|
|
5077
|
+
|
|
3794
5078
|
/**
|
|
3795
5079
|
* 后台任务的契约(方案 36)。
|
|
3796
5080
|
*
|
|
3797
5081
|
* 「后台任务」= 一条不等它跑完就返回的命令。宿主要展示它们(TUI 的状态栏计数、
|
|
3798
5082
|
* `/tasks` 列表、Web 的抽屉),所以形状住在 protocol。
|
|
3799
5083
|
*/
|
|
5084
|
+
/**
|
|
5085
|
+
* 这一格是哪一类作业(方案 62,2026-08-26)。
|
|
5086
|
+
*
|
|
5087
|
+
* ⚠️ **它和 infra 那份 `JobKind` 必须逐字相同,而两边只能各存一份** ——
|
|
5088
|
+
* `protocol` 零依赖、`infra` 也没有任何内部依赖(`scripts/check-layers.mjs` 的
|
|
5089
|
+
* `ALLOWED` 表里两个都是空的),所以谁都 import 不到谁。
|
|
5090
|
+
*
|
|
5091
|
+
* 漂移不是靠叮嘱守的:**桥在 `plugin-terminal`**(它同时依赖两边),那儿有一条
|
|
5092
|
+
* 编译期等式断言,任一边多一档或改一个字面量就当场编译不过。判据全文在
|
|
5093
|
+
* `plugin-terminal/src/background.ts` 的 `KIND_MIRROR` 上。
|
|
5094
|
+
*/
|
|
5095
|
+
type BackgroundTaskKind = 'command' | 'shell' | 'agent';
|
|
3800
5096
|
/**
|
|
3801
5097
|
* 任务状态。
|
|
3802
5098
|
*
|
|
3803
5099
|
* `killed` 和 `exited` 分开,因为它们对模型意味着完全不同的事:前者是**我们**
|
|
3804
5100
|
* 停的(`task_stop` 或宿主退出),后者是命令自己结束的(退出码才有意义)。
|
|
3805
5101
|
* 合成一个 `done` 的话,模型看到一个非零退出码分不清是命令失败了还是被我们杀了。
|
|
5102
|
+
*
|
|
5103
|
+
* ## 2026-08-26(方案 62):从四档变六档,多的两档是持久会话那一族的
|
|
5104
|
+
*
|
|
5105
|
+
* `closed`(主动关掉)和 `reaped`(空闲超时被回收)原来只活在 infra 那份
|
|
5106
|
+
* `JobStatus` 上,到网线这一层被 `taskStatus()` 并成 `killed`。持久会话搬上
|
|
5107
|
+
* 宿主界面之后那次合并**变成了一句假话**,而且正好是最要紧的那一句:
|
|
5108
|
+
* `reaped` 在文案里写着「里面的工作目录、venv、REPL 都没了」
|
|
5109
|
+
* (`locales/zh.yaml` 的 `reaped`,那一段自己标着「这里最要紧的一条」),
|
|
5110
|
+
* 而屏幕上说「已停止」的东西看起来还能接着用。
|
|
5111
|
+
*
|
|
5112
|
+
* ⚠️ **别把这两档和 `killed` 并回去。** 判据不是「多两档更精确」,是
|
|
5113
|
+
* **产出方各自的措辞已经发出去了** —— 逐字同 infra `JobStatus` 上那一段。
|
|
3806
5114
|
*/
|
|
3807
|
-
type BackgroundTaskStatus = 'running' | 'exited' | 'killed' | 'failed';
|
|
5115
|
+
type BackgroundTaskStatus = 'running' | 'exited' | 'killed' | 'failed' | 'closed' | 'reaped';
|
|
3808
5116
|
interface BackgroundTaskInfo {
|
|
3809
5117
|
/** 短 id,模型要在对话里引用它,所以是 `t1` 这种而不是 uuid */
|
|
3810
5118
|
id: string;
|
|
3811
|
-
|
|
5119
|
+
/**
|
|
5120
|
+
* 哪一类(方案 62)。**新必填字段** —— 宿主要按它分支,
|
|
5121
|
+
* 因为下面 {@link BackgroundTaskInfo.command} 只在 `'command'` 那一档才有。
|
|
5122
|
+
*/
|
|
5123
|
+
kind: BackgroundTaskKind;
|
|
5124
|
+
/**
|
|
5125
|
+
* 一行标签,**各 kind 各自的「这是什么」**:命令行 / 持久会话的工作目录 /
|
|
5126
|
+
* 子 agent 的目标。
|
|
5127
|
+
*
|
|
5128
|
+
* ## 为什么是一个合成字段,而不是三个可选字段
|
|
5129
|
+
*
|
|
5130
|
+
* 消费方(`/tasks` 那一行、检视面板那一格、`epoch -p` 的收尾摘要)要的正是
|
|
5131
|
+
* 「一行说清楚它是什么」。判据逐字同 infra `JobInfo.label` —— 这一格就是
|
|
5132
|
+
* **把那个已经设计好的形状搬到网线上**,不是新设计一个。
|
|
5133
|
+
*/
|
|
5134
|
+
label: string;
|
|
5135
|
+
/**
|
|
5136
|
+
* 命令行。**只有 `kind === 'command'` 时才有。**
|
|
5137
|
+
*
|
|
5138
|
+
* ## ⚠️ 它没有变成 `label` 的别名,那是刻意的
|
|
5139
|
+
*
|
|
5140
|
+
* 「留着当别名」看起来最不破坏,但在持久会话那一档上 `command` 会是**一个工作
|
|
5141
|
+
* 目录** —— 于是屏幕上同时出现两句互相打脸的话(判据同
|
|
5142
|
+
* `WireMcpReconnectResponse.ok` 上那条)。这正是 2026-08-26 之前**不做**这件事的
|
|
5143
|
+
* 全部理由:那时的结论是「`command` 那一格填不出实话」,所以宿主口子干脆只报
|
|
5144
|
+
* 后台命令一类。
|
|
5145
|
+
*
|
|
5146
|
+
* 所以现在的形状是:`kind` 分支 + `label` 通用 + `command` **只在老那一档有**。
|
|
5147
|
+
* 老宿主读 `.command` 在老那一档仍然拿到同一个值,新那一档拿到 `undefined`
|
|
5148
|
+
* 而不是一句假话。新代码请一律读 `label`。
|
|
5149
|
+
*/
|
|
5150
|
+
command?: string;
|
|
3812
5151
|
cwd?: string;
|
|
3813
5152
|
/**
|
|
3814
5153
|
* 子 shell 的 pid。
|
|
@@ -4932,20 +6271,99 @@ interface WireSendMessageResponse {
|
|
|
4932
6271
|
* 形状见 {@link WireCommandExpansion}。
|
|
4933
6272
|
*/
|
|
4934
6273
|
command?: WireCommandExpansion;
|
|
6274
|
+
/**
|
|
6275
|
+
* 这条消息里 `@:` 引用到的那几段会话,**逐段一条收据**(方案 53 PR-4)。
|
|
6276
|
+
*
|
|
6277
|
+
* **一段都没引用时整个键缺席**(同上面那个 `command`),所以它不在下面那份
|
|
6278
|
+
* 必填清单里。
|
|
6279
|
+
*
|
|
6280
|
+
* ## 为什么收据必须回在这一发上
|
|
6281
|
+
*
|
|
6282
|
+
* 引用的解析在**服务端**(可读性判定在引擎里,浏览器不许自己读
|
|
6283
|
+
* `sessions.db` —— 判据在 `EpochRuntime.sessionReferences` 上),而这一发之后
|
|
6284
|
+
* 浏览器不会再为这条消息去问一次:SSE 上流的是模型那一侧的事件,
|
|
6285
|
+
* 用户自己那条消息是本地乐观回显的(`state/reducer.ts` 的 `submit`)。
|
|
6286
|
+
* 不回收据的话,界面只知道「用户打了 `@:xxx`」,说不出**它有没有生效**,
|
|
6287
|
+
* 而那五个错误码(匹配不到 / 引用自己 / 超上限 / 判定不放行 / 预算用满)
|
|
6288
|
+
* 恰好全是「看起来发出去了、实际没附内容」的形态。
|
|
6289
|
+
*
|
|
6290
|
+
* ⚠️ **它不带一个字的转录文本**,判据在 {@link WireSessionReference} 上。
|
|
6291
|
+
*/
|
|
6292
|
+
references?: readonly WireSessionReference[];
|
|
6293
|
+
/**
|
|
6294
|
+
* 这条消息里 `@路径` 附上了哪几个文件,**逐个一条收据**(方案 63)。
|
|
6295
|
+
*
|
|
6296
|
+
* **一个都没附时整个键缺席**,判据逐字同上面那两个键。
|
|
6297
|
+
*
|
|
6298
|
+
* ## 为什么这一份也必须回在这一发上
|
|
6299
|
+
*
|
|
6300
|
+
* 理由和上面那条 `references` 是同一条,而且这一半更急:`@文件` 那四个失败档
|
|
6301
|
+
* (被权限拦了 / 是二进制 / 读不到 / 超了上限)**全都是「看起来发出去了、
|
|
6302
|
+
* 实际没附内容」的形态**,而其中「被权限拦了」还是一句用户必须听见的话 ——
|
|
6303
|
+
* 这条路上刻意**不弹审批**(判据在 `core/src/context/mentions.ts` 的
|
|
6304
|
+
* 「权限没放行时不弹审批,只是不附」那一节),所以屏幕上那张卡是他唯一的交代。
|
|
6305
|
+
* 不回收据的话,一个 `deny` 规则挡住的 `@.env` 和一次正常附上长得一模一样。
|
|
6306
|
+
*
|
|
6307
|
+
* ⚠️ **它不带一个字的文件正文**,判据在 {@link WireFileReference} 上。
|
|
6308
|
+
*/
|
|
6309
|
+
files?: readonly WireFileReference[];
|
|
4935
6310
|
}
|
|
4936
|
-
declare const WIRE_SEND_MESSAGE_RESPONSE_FIELDS: WireFields<WireSendMessageResponse>;
|
|
4937
|
-
/** `aborted: false` 表示这一轮本来就没在跑,不是失败 */
|
|
4938
|
-
interface WireAbortResponse {
|
|
4939
|
-
aborted: boolean;
|
|
4940
|
-
}
|
|
4941
|
-
declare const WIRE_ABORT_FIELDS: WireFields<WireAbortResponse>;
|
|
4942
6311
|
/**
|
|
4943
|
-
*
|
|
6312
|
+
* 一段会话引用**在网线上的样子** —— 就是那个部件**摘掉正文**。
|
|
4944
6313
|
*
|
|
4945
|
-
* `
|
|
4946
|
-
* (SSE 的 `session-state` 要等下一次状态变化才发)。前端首屏必须用它。
|
|
6314
|
+
* 写成 `Omit<EpochSessionPart, 'type' | 'text'>` 而不是再抄一遍七个字段:
|
|
4947
6315
|
*
|
|
4948
|
-
*
|
|
6316
|
+
* - 抄一遍的下场是部件上加一格(比如将来的 `startedAt`)而这儿忘了加,
|
|
6317
|
+
* 于是界面永远看不到它,**而两边都编译得过**;
|
|
6318
|
+
* - 摘掉 `text` 是**契约上的一句话**:这条线上不许出现另一段会话的转录。
|
|
6319
|
+
* 正文是给模型的 —— 它已经进了这条消息的 `parts`(而且落库那一侧刻意不存,
|
|
6320
|
+
* 见 `core/src/session/payload.ts` 的 `persistParts`),
|
|
6321
|
+
* 而浏览器拿它只能把几十 KB 的别人家转录再复制一份到网线上。
|
|
6322
|
+
* 写成 `Omit` 之后,谁想把正文发给浏览器得先来改这一行 ——
|
|
6323
|
+
* 那正好是该被人看见的一次改动;
|
|
6324
|
+
* - 摘掉 `type` 是因为它在这儿是恒等式(`'session'`),
|
|
6325
|
+
* 而在 `parts` 数组里它是判别标签。
|
|
6326
|
+
*
|
|
6327
|
+
* ⚠️ 这一条顺带把「界面上那张卡要不要能展开」答了:**没有可展开的正文**。
|
|
6328
|
+
* 判据全文在 `web/src/mention/reference-card.tsx` 的文件头。
|
|
6329
|
+
*
|
|
6330
|
+
* ⚠️ `omitted` 那五个码是稳定契约,真源是 `SESSION_REFERENCE_ERROR_CODES`
|
|
6331
|
+
* (`message.ts`)。界面按**码**分档说话,别按话分档。
|
|
6332
|
+
*/
|
|
6333
|
+
type WireSessionReference = Omit<EpochSessionPart, 'type' | 'text'>;
|
|
6334
|
+
declare const WIRE_SESSION_REFERENCE_FIELDS: WireFields<WireSessionReference>;
|
|
6335
|
+
/**
|
|
6336
|
+
* 一个文件附件**在网线上的样子** —— 就是那个部件**摘掉正文**(方案 63)。
|
|
6337
|
+
*
|
|
6338
|
+
* 写成 `Omit<EpochFilePart, 'type' | 'text'>`,三条判据逐字同
|
|
6339
|
+
* {@link WireSessionReference}(那一份是这一份的先例,先读它):
|
|
6340
|
+
*
|
|
6341
|
+
* - 摘掉 `text` 是**契约上的一句话**:这条线上不许出现文件正文。正文是给模型的
|
|
6342
|
+
* ——它已经进了这条消息的 `parts`。发给浏览器只能把同一份内容在网线上再抄一遍,
|
|
6343
|
+
* 而浏览器拿它没有任何用处(用户手上就有那个文件,点开编辑器就能看);
|
|
6344
|
+
* - 摘掉 `type` 是因为它在这儿是恒等式(`'file'`);
|
|
6345
|
+
* - 写成 `Omit` 而不是抄一遍四个字段,是为了让部件上加一格(比如将来的 `mtime`)
|
|
6346
|
+
* 时这儿不会**忘了加而两边都编译得过**。
|
|
6347
|
+
*
|
|
6348
|
+
* ⚠️ **`path` 之外每一格都可缺席,而这正是它要表达的东西**:二进制、超限、
|
|
6349
|
+
* 权限没放行的那几档**只有路径**。界面按 `omitted` 那个**码**分档说话,
|
|
6350
|
+
* 别按话分档 —— 真源是 {@link FileOmitReason}。
|
|
6351
|
+
*/
|
|
6352
|
+
type WireFileReference = Omit<EpochFilePart, 'type' | 'text'>;
|
|
6353
|
+
declare const WIRE_FILE_REFERENCE_FIELDS: WireFields<WireFileReference>;
|
|
6354
|
+
declare const WIRE_SEND_MESSAGE_RESPONSE_FIELDS: WireFields<WireSendMessageResponse>;
|
|
6355
|
+
/** `aborted: false` 表示这一轮本来就没在跑,不是失败 */
|
|
6356
|
+
interface WireAbortResponse {
|
|
6357
|
+
aborted: boolean;
|
|
6358
|
+
}
|
|
6359
|
+
declare const WIRE_ABORT_FIELDS: WireFields<WireAbortResponse>;
|
|
6360
|
+
/**
|
|
6361
|
+
* `GET /api/sessions/:id/approvals` —— 重连之后补拉挂起的审批。
|
|
6362
|
+
*
|
|
6363
|
+
* `state` **不是**附赠字段:刷新一次页面之后,「现在到底有没有东西在跑」只有它答得上来
|
|
6364
|
+
* (SSE 的 `session-state` 要等下一次状态变化才发)。前端首屏必须用它。
|
|
6365
|
+
*
|
|
6366
|
+
* 每一行就是 SSE 上那个 `WireApprovalRequest`(**带 `type`**),不另造一个形状 ——
|
|
4949
6367
|
* 前端补拉回来的东西可以和 SSE 推过来的走同一条处理路径。
|
|
4950
6368
|
*/
|
|
4951
6369
|
interface WireApprovalListResponse {
|
|
@@ -5838,6 +7256,69 @@ interface WireCreateWorkspaceResponse {
|
|
|
5838
7256
|
entry: WireWorkspaceDirEntry;
|
|
5839
7257
|
}
|
|
5840
7258
|
declare const WIRE_CREATE_WORKSPACE_RESPONSE_FIELDS: WireFields<WireCreateWorkspaceResponse>;
|
|
7259
|
+
/**
|
|
7260
|
+
* `GET /api/sessions/:id/context` —— **这一段会话**此刻的预算构成。
|
|
7261
|
+
*
|
|
7262
|
+
* 包一层而不是裸发 {@link ContextBreakdown},判据同本文件开头那条列表规矩:
|
|
7263
|
+
* 将来要加「上次压缩发生在哪一轮」之类的同级事实时,响应形状不用整个换掉。
|
|
7264
|
+
*
|
|
7265
|
+
* ⚠️ **`breakdown` 可以是 `null`,那不是错误。** 这个进程认得这段会话、但它此刻
|
|
7266
|
+
* 没有活着的循环(只读会话、provider 没起来)时就是这一档 —— 那时正确的答案是
|
|
7267
|
+
* 「不知道」,不是 404(会话真的在),也不是一份全零的构成(那是在编一个数)。
|
|
7268
|
+
* 界面据此**整块不画**(决定 20 ①),而不是画一张空的条形图。
|
|
7269
|
+
*/
|
|
7270
|
+
interface WireContextResponse {
|
|
7271
|
+
breakdown: ContextBreakdown | null;
|
|
7272
|
+
}
|
|
7273
|
+
declare const WIRE_CONTEXT_FIELDS: WireFields<WireContextResponse>;
|
|
7274
|
+
/**
|
|
7275
|
+
* `POST /api/sessions/:id/compact` 的请求体。
|
|
7276
|
+
*
|
|
7277
|
+
* `instruction` 是「这次帮我留住什么」,逐字对应 TUI `/compact <说明>` 的那段参数
|
|
7278
|
+
* (`tui/src/commands/session.ts`)。**不给和给空串是同一件事** ——
|
|
7279
|
+
* 底下 `AgentSession.compact()` 收的就是 `string | undefined`。
|
|
7280
|
+
*/
|
|
7281
|
+
interface WireCompactRequest {
|
|
7282
|
+
instruction?: string;
|
|
7283
|
+
}
|
|
7284
|
+
/**
|
|
7285
|
+
* `POST /api/sessions/:id/compact` —— 压完之后的账。
|
|
7286
|
+
*
|
|
7287
|
+
* 四个字段**逐字照抄** `runtime/src/agent-session.ts` 的 `compact()` 返回值,
|
|
7288
|
+
* 一个都不重新命名:这条路上服务端不做任何判断,它只是把引擎那句话搬过网线。
|
|
7289
|
+
* 在这儿改个名字的代价是同一件事在两侧有两个说法,而那正是这个文件存在的理由。
|
|
7290
|
+
*
|
|
7291
|
+
* ## ⚠️ 「没压动」是一个正常结果,不是错误
|
|
7292
|
+
*
|
|
7293
|
+
* `ran: false` 有两种来路(历史太短、压缩器没装上),`ran: true` 但
|
|
7294
|
+
* `after === before` 是第三种(摘要生成失败时压缩器原样返回消息)。**三种都回
|
|
7295
|
+
* 200**,因为这次请求确实办完了 —— 而且第三种**钱是真花了**,用户该知道花在了哪。
|
|
7296
|
+
* 界面必须把这三档和「已压缩」分开说,报一个假的「已压缩」是在骗人
|
|
7297
|
+
* (判据全文在 TUI 那条命令的实现上,两边共用同一组结论)。
|
|
7298
|
+
*
|
|
7299
|
+
* ## ⚠️ 压缩**不动落盘的那份记录**
|
|
7300
|
+
*
|
|
7301
|
+
* 它换掉的是内存里那份对话(`AgentSession.history`)—— 也就是「下一轮送进模型的
|
|
7302
|
+
* 是什么」。`GET /messages` 读的是会话库,压完之后**一行都不会少**。
|
|
7303
|
+
* 所以调用方压完**不该**去重拉消息:拉回来的和压之前逐字相同,而那一下
|
|
7304
|
+
* 会让人以为压缩没生效。
|
|
7305
|
+
*/
|
|
7306
|
+
interface WireCompactResponse {
|
|
7307
|
+
/** 压缩器**跑了没有**。`false` 时 `before === after`,理由在 `reason` 里 */
|
|
7308
|
+
ran: boolean;
|
|
7309
|
+
/** 压之前内存里的消息条数 */
|
|
7310
|
+
before: number;
|
|
7311
|
+
/** 压之后的条数。`ran` 为真时仍可能等于 `before`,见上面那一节 */
|
|
7312
|
+
after: number;
|
|
7313
|
+
/**
|
|
7314
|
+
* 没跑的原因,**引擎渲染好的一句话**,原样透传。
|
|
7315
|
+
*
|
|
7316
|
+
* 不在服务端重新编:两种来路(太短 / 没装上)的下一步完全不同,
|
|
7317
|
+
* 而它们的措辞已经过了引擎那侧的 catalog。
|
|
7318
|
+
*/
|
|
7319
|
+
reason?: string;
|
|
7320
|
+
}
|
|
7321
|
+
declare const WIRE_COMPACT_FIELDS: WireFields<WireCompactResponse>;
|
|
5841
7322
|
|
|
5842
7323
|
/**
|
|
5843
7324
|
* 能力页的载荷契约 —— `GET /api/sessions/:id/capabilities`(方案 42 PR-1)。
|
|
@@ -5893,6 +7374,35 @@ interface WireAgentRoleSummary {
|
|
|
5893
7374
|
* 用 `null` 而不是省略这个键,是因为省略在 JSON 里和「忘了填」长得一模一样。
|
|
5894
7375
|
*/
|
|
5895
7376
|
tools: readonly string[] | null;
|
|
7377
|
+
/**
|
|
7378
|
+
* 技能索引白名单(`AgentRole.skills`,2026-08-27)。**`null` = 不限**(全量
|
|
7379
|
+
* 索引),空数组 = 索引里一条都不列 —— 缺省语义和上面 `tools` 逐字相同,
|
|
7380
|
+
* 用 `null` 而不是省略这个键的理由也逐字相同。
|
|
7381
|
+
*
|
|
7382
|
+
* ## 为什么补这一格:这一屏原来看不见一个身份的技能白名单
|
|
7383
|
+
*
|
|
7384
|
+
* `skills` 2026-08-26 就在引擎里生效了,2026-08-27 又长出了界面上那张表单
|
|
7385
|
+
* (`WireRoleAddRequest.skills`)—— 而这一份摘要上一直没有它。于是**填了白名单
|
|
7386
|
+
* 建出来的那张卡一个字都不提它**,手写 md 建的身份同样看不见:两条路缺的是
|
|
7387
|
+
* 同一样东西,也就是这一格。
|
|
7388
|
+
*
|
|
7389
|
+
* ⚠️ 它**不是** {@link skillsCut} 的另一个说法,两格答的是两个问题:
|
|
7390
|
+
*
|
|
7391
|
+
* | 这一格 | 答的是 |
|
|
7392
|
+
* | ------------ | ---------------------------------------------- |
|
|
7393
|
+
* | `skills` | 这个身份**自己声明**了哪几条(`null` = 没声明) |
|
|
7394
|
+
* | `skillsCut` | 同名覆盖时**被上一层砍掉**了哪几条 |
|
|
7395
|
+
*
|
|
7396
|
+
* 合成一格的话,「我写了三条」和「我写了五条掉了两条」在网线上长得一样。
|
|
7397
|
+
*
|
|
7398
|
+
* ## ⚠️ 界面上别把它画成一道墙
|
|
7399
|
+
*
|
|
7400
|
+
* 被这一格挡在索引外的技能,这个身份**照样 `skill_view` 得到正文** ——
|
|
7401
|
+
* 收窄的宾语是上下文预算,不是可达性(判据全文在 `AgentRole.skills` 上,
|
|
7402
|
+
* 由 `core/__tests__/skill-in-prompt.test.ts` 那条钉着)。措辞的口径逐字同
|
|
7403
|
+
* `web.ui.cap.role_add_skills_hint`:**只管索引里列不列得出**。
|
|
7404
|
+
*/
|
|
7405
|
+
skills: readonly string[] | null;
|
|
5896
7406
|
/** 轮次上限。`null` = 跟全局(`max(4, floor(父 maxTurns / 2))`) */
|
|
5897
7407
|
maxTurns: number | null;
|
|
5898
7408
|
/**
|
|
@@ -5904,6 +7414,29 @@ interface WireAgentRoleSummary {
|
|
|
5904
7414
|
* 本来就是那几枚划掉的工具芯片(设计稿的 `[data-cut]`)。
|
|
5905
7415
|
*/
|
|
5906
7416
|
toolsCut: readonly string[];
|
|
7417
|
+
/**
|
|
7418
|
+
* 同名覆盖被收窄时**掉了哪几条技能**(`AgentRole.skills`,2026-08-27)。
|
|
7419
|
+
* 没收窄就是空数组。
|
|
7420
|
+
*
|
|
7421
|
+
* ## ⚠️ 为什么是**第二格**,不是往 `toolsCut` 里混
|
|
7422
|
+
*
|
|
7423
|
+
* 这一格是补一笔明写着「加第一个带 `skills` 的内置角色**之前必须先补**」的账
|
|
7424
|
+
* (记账全文在
|
|
7425
|
+
* [RECORD-skill-index-narrowing](../../../docs/verify/VERIFY_RECORD-skill-index-narrowing.md) §四第 3 条)。
|
|
7426
|
+
* 那笔账的**内容**就是这一句:`toolsCut` 被 web 画成划掉的**工具**芯片
|
|
7427
|
+
* (`[data-cut]`),把技能名混进去的表现是界面上多出几枚**根本不存在的工具**,
|
|
7428
|
+
* 而没有任何东西会红 —— 两边都是 `string[]`。
|
|
7429
|
+
*
|
|
7430
|
+
* 所以刀口下在这儿而不是把 `toolsCut` 泛化成 `cut: {kind, names}[]`:后者要动
|
|
7431
|
+
* 一个已经在用的字段,而它的读者(`experts.tsx` 那两处)今天一个字没错。
|
|
7432
|
+
* core 那一侧的 `RoleMergeNotice` 加的是一格 `kind` —— **投影这一层按 `kind`
|
|
7433
|
+
* 分流**,`server/src/capability.ts` 的 `toWireRole()` 那两行 `find` 就是分流点。
|
|
7434
|
+
*
|
|
7435
|
+
* ⚠️ **今天三个内置角色一条 `skills` 都没有,所以这一格恒空。** 那不代表它是
|
|
7436
|
+
* 死代码:它守的正是「加第一个带技能白名单的内置角色那一天」——
|
|
7437
|
+
* 那天没有这一格的话,收窄会**静默**发生。
|
|
7438
|
+
*/
|
|
7439
|
+
skillsCut: readonly string[];
|
|
5907
7440
|
}
|
|
5908
7441
|
declare const WIRE_AGENT_ROLE_FIELDS: WireFields<WireAgentRoleSummary>;
|
|
5909
7442
|
/**
|
|
@@ -5933,8 +7466,45 @@ interface WireSkillSummary {
|
|
|
5933
7466
|
* 是估算不是实测:真值取决于具体模型的分词器,而这里没有分词器。
|
|
5934
7467
|
* 引擎自己的预算账(`context-breakdown.ts`)用的就是同一个估法,
|
|
5935
7468
|
* 所以这个数和引擎看到的是同一个数 —— 只是它们一起是估的。
|
|
7469
|
+
*
|
|
7470
|
+
* ⚠️ **这个数说的是「它那一行**如果*进索引*要多少钱」,不是「它一定在付」** ——
|
|
7471
|
+
* 后半句归 {@link residency},2026-08-27 补的就是它。别在界面上单独印这一格,
|
|
7472
|
+
* 判据在那一格上。
|
|
5936
7473
|
*/
|
|
5937
7474
|
tokens: number;
|
|
7475
|
+
/**
|
|
7476
|
+
* 这一条**此刻**在不在 `<available_skills>` 里 —— 也就是上面那个
|
|
7477
|
+
* {@link tokens} 到底付没付(2026-08-27)。
|
|
7478
|
+
*
|
|
7479
|
+
* ## 为什么补这一格:那一栏行尾原来在说一句会变假的话
|
|
7480
|
+
*
|
|
7481
|
+
* 能力页每一行行尾印的是「这条技能每一轮的常驻开销」。那句话在两道口子都
|
|
7482
|
+
* 关着时是真的,而它们各自都能把它变成假话:
|
|
7483
|
+
*
|
|
7484
|
+
* | 值 | 谁挡的 | 用户的下一步 |
|
|
7485
|
+
* | --------- | --------------------------------------------- | ----------------------------------------- |
|
|
7486
|
+
* | `indexed` | 没人挡,它真的每一轮都在 | 没有下一步 |
|
|
7487
|
+
* | `capped` | `skills.maxIndexed` 封了顶(**进程级**) | 那一格调大或去掉 |
|
|
7488
|
+
* | `role` | 这段会话身份的 `skills` 白名单(**会话级**) | 换个身份开会话,或改那份角色 md |
|
|
7489
|
+
*
|
|
7490
|
+
* 三档不是三种颜色,是三种下一步 —— 形状和判据逐字同 {@link WireMcpConnector.state}。
|
|
7491
|
+
* 合成一个 `indexed: boolean` 的话,界面只说得出「它不在」,说不出去哪儿改。
|
|
7492
|
+
*
|
|
7493
|
+
* ## ⚠️ `role` 这一档**不是一把锁**
|
|
7494
|
+
*
|
|
7495
|
+
* 被它挡住的技能,模型照样 `skill_view` 得到正文 —— 收窄的宾语是**上下文预算**,
|
|
7496
|
+
* 不是可达性(判据全文在 `agent-role.ts` 的 `AgentRole.skills`)。所以界面上
|
|
7497
|
+
* 别给它画锁 / 画禁行标:那会让用户以为自己配出了一道边界,
|
|
7498
|
+
* 而任何带 `file_read` 的角色都能直接读那份 `SKILL.md`。
|
|
7499
|
+
*
|
|
7500
|
+
* ## 它是**这个会话**的答案,所以这条端点是会话作用域的
|
|
7501
|
+
*
|
|
7502
|
+
* `capped` 那一半是进程级的,`role` 那一半跟着会话走
|
|
7503
|
+
* (服务端拿的是 `LiveSession.role`,也就是**底座身份**,不是那枚 chip 挑的
|
|
7504
|
+
* 逐条身份 —— 「常驻」的宾语是整段会话)。`GET /api/sessions/:id/capabilities`
|
|
7505
|
+
* 早就是会话作用域了,判据在 `server/src/capability.ts` 文件头最后一节。
|
|
7506
|
+
*/
|
|
7507
|
+
residency: 'indexed' | 'capped' | 'role';
|
|
5938
7508
|
/** `host` 同 {@link WireAgentRoleSummary.source} 那一格:宿主 ship 的技能目录 */
|
|
5939
7509
|
scope: 'user' | 'project' | 'plugin' | 'host';
|
|
5940
7510
|
/** `workflow` / `rule` / `pattern` / `checklist` */
|
|
@@ -6363,7 +7933,7 @@ declare const WIRE_SKILL_IMPORT_FIELDS: WireFields<WireSkillImportResponse>;
|
|
|
6363
7933
|
/**
|
|
6364
7934
|
* `POST /api/roles` 的请求体 —— 稿子上有、身份那一栏一直没有的那张「新建」。
|
|
6365
7935
|
*
|
|
6366
|
-
* ##
|
|
7936
|
+
* ## 六格,和 frontmatter 逐格对上,一格不多
|
|
6367
7937
|
*
|
|
6368
7938
|
* 角色文件是 Markdown + YAML frontmatter,认得的键只有四个
|
|
6369
7939
|
* (`name` / `description` / `tools` / `maxTurns`,真源是 `loader.ts` 的
|
|
@@ -6372,6 +7942,19 @@ declare const WIRE_SKILL_IMPORT_FIELDS: WireFields<WireSkillImportResponse>;
|
|
|
6372
7942
|
* 十几格而表单只解决「第一台加不进来」;这边总共就四个键,砍掉任何一个都会让
|
|
6373
7943
|
* 「从界面建的身份」和「手写的身份」变成两种东西。
|
|
6374
7944
|
*
|
|
7945
|
+
* > 📮 **2026-08-26 这句话过期了一半:** `FrontmatterSchema` 那时起认**五个**键
|
|
7946
|
+
* > —— 技能收窄那一轮加了 `skills`(`AgentRole.skills`),「只有四个」成了假话。
|
|
7947
|
+
* > 当时 `skills` **刻意不在请求体里**,理由逐字是:「加了就是一个界面写得出、
|
|
7948
|
+
* > 引擎读得到、但**没有一张表单会填它**的字段,那比不加更坏。」
|
|
7949
|
+
* >
|
|
7950
|
+
* > ✅ **2026-08-27 补齐了,那条理由因此到期 —— 但它没被推翻,是被满足了。**
|
|
7951
|
+
* > 这一轮同时加了请求体那一格**和 `role-add.tsx` 上那张表单里的那一格**,
|
|
7952
|
+
* > 两个一起落地。⚠️ 所以那句话的**规矩**照旧有效:往这张表里加第六格的人,
|
|
7953
|
+
* > 要么连表单一起加,要么别加。
|
|
7954
|
+
* >
|
|
7955
|
+
* > 于是「六格,和 frontmatter 逐格对上,一格不多」重新是逐字真的:
|
|
7956
|
+
* > `name` / `description` / `tools` / `maxTurns` / `skills` 五个键 + 正文 = `prompt`。
|
|
7957
|
+
*
|
|
6375
7958
|
* ## ⚠️ 落点恒为**用户级**(`~/.epoch/agents/<name>.md`),请求体里没有这一格
|
|
6376
7959
|
*
|
|
6377
7960
|
* 不给「写用户级还是项目级」的选择:项目级那一层跟着工作区走(决定 18),
|
|
@@ -6417,6 +8000,32 @@ interface WireRoleAddRequest {
|
|
|
6417
8000
|
* 「引用了不存在的工具」,那时屏幕上没有任何东西指向这个输入框。
|
|
6418
8001
|
*/
|
|
6419
8002
|
tools?: readonly string[];
|
|
8003
|
+
/**
|
|
8004
|
+
* 技能索引白名单(2026-08-27)。**不给 = 不限**(全量索引),给空数组 =
|
|
8005
|
+
* 一条都不列 —— 缺省语义和上面 `tools` 逐字相同,判据在 `AgentRole.skills` 上。
|
|
8006
|
+
*
|
|
8007
|
+
* ## ⚠️ 和 `tools` 那一格**唯一的不同**:服务端**不当场校验这几个名字**
|
|
8008
|
+
*
|
|
8009
|
+
* 工具那一格认不出名字就 400,理由是「用户此刻正在敲它,而服务端此刻手上就有
|
|
8010
|
+
* 那张工具表」。这一格照抄那条会是错的,因为**两张表的性质不同**:
|
|
8011
|
+
*
|
|
8012
|
+
* | | 什么时候定的 | 「此刻不认识」意味着什么 |
|
|
8013
|
+
* | ------ | ---------------------- | ---------------------------------- |
|
|
8014
|
+
* | 工具表 | **装配期**,一次定死 | 这个名字打错了 |
|
|
8015
|
+
* | 技能表 | **会话中途还会变** | 可能只是还没装 / 还没被学出来 |
|
|
8016
|
+
*
|
|
8017
|
+
* 技能表会变这件事不是推测,是 `createSkillViewTool` 的 JSDoc 上现成的判据:
|
|
8018
|
+
* `SkillLearner` 学一条就往用户级目录里落一份 SKILL.md。把「此刻没装」判成
|
|
8019
|
+
* 400,等于拒掉一个**明天就成立**的名字,而用户手上没有任何办法让它今天成立。
|
|
8020
|
+
*
|
|
8021
|
+
* 打错的那一档没有被放过:启动诊断 `unknownRoleSkills()` 照旧逐个报出来
|
|
8022
|
+
* (`role_unknown_skills`)—— 那条路对「今天没有、明天会有」的名字恰好是对的
|
|
8023
|
+
* 语气(一句警告,不是一次拒绝)。
|
|
8024
|
+
*
|
|
8025
|
+
* ⚠️ **别顺手补一道 400。** 补之前先回答:一个用户想给身份预留一条还没装的
|
|
8026
|
+
* 技能名,他该怎么办。
|
|
8027
|
+
*/
|
|
8028
|
+
skills?: readonly string[];
|
|
6420
8029
|
/** 轮次上限。不给 = 跟全局。范围由 `FrontmatterSchema` 说了算(1–200 的整数) */
|
|
6421
8030
|
maxTurns?: number;
|
|
6422
8031
|
}
|
|
@@ -6461,6 +8070,395 @@ interface WireRoleAddResponse {
|
|
|
6461
8070
|
}
|
|
6462
8071
|
declare const WIRE_ROLE_ADD_RESPONSE_FIELDS: WireFields<WireRoleAddResponse>;
|
|
6463
8072
|
|
|
8073
|
+
/**
|
|
8074
|
+
* 插件那一页的契约 —— 六条端点(方案 59 §六 E2,2026-08-21)。
|
|
8075
|
+
*
|
|
8076
|
+
* ```
|
|
8077
|
+
* GET /api/plugins 装着的 + 市场里有什么 + pendingRestart
|
|
8078
|
+
* POST /api/plugins/install/preview {ref} → 将安装什么,一个字节都不写
|
|
8079
|
+
* POST /api/plugins/install {ref, token} → 真装
|
|
8080
|
+
* POST /api/plugins/update/preview {name} → 更新会带来什么
|
|
8081
|
+
* POST /api/plugins/update {name, token} → 真更新
|
|
8082
|
+
* POST /api/plugins/uninstall {name} → 卸载
|
|
8083
|
+
* ```
|
|
8084
|
+
*
|
|
8085
|
+
* 底座是 `EpochRuntime.plugins`(方案 59 PR-4 落的那一片,判据全文在
|
|
8086
|
+
* `runtime/src/plugin-control.ts` 的文件头)。**这一层一条策略判断都没有** ——
|
|
8087
|
+
* 谁能装、装什么、要不要重启,三个问题的答案全在那一片里,网线这一侧只是把它
|
|
8088
|
+
* 转出来。⚠️ 唯一的例外是下面第二节那道闸,而它答的是一个 runtime 那一层**问不到**
|
|
8089
|
+
* 的问题(「网线另一头是不是这台机器」)。
|
|
8090
|
+
*
|
|
8091
|
+
* ## 为什么不塞进 [wire-capability.ts](./wire-capability.ts)
|
|
8092
|
+
*
|
|
8093
|
+
* 判据逐字同 [wire-agent-role.ts](./wire-agent-role.ts) 从那儿分出来那次:那个文件
|
|
8094
|
+
* 已经压过 500 行线,而**刀口按题目下不按行数** —— 那份答的是「能力页那三栏读什么」,
|
|
8095
|
+
* 这一份答的是「怎么往这台机器上装一包扩展物」,而后者带着一整套预览形状 +
|
|
8096
|
+
* 七档拒绝 + 一格 `pendingRestart`。
|
|
8097
|
+
*
|
|
8098
|
+
* ---
|
|
8099
|
+
*
|
|
8100
|
+
* # 一、⚠️ **只收 `<市场>/<插件>`,收不了路径** —— 这是这条路的安全性质本体
|
|
8101
|
+
*
|
|
8102
|
+
* {@link WirePluginInstallRequest} 上**没有 `source` 这个东西**,只有一个
|
|
8103
|
+
* {@link WirePluginInstallRequest.ref}。于是「指一条任意路径装一个插件」这件事
|
|
8104
|
+
* 在这个形状下**说不出口** —— 不是靠 review 盯住的,是编译期的事
|
|
8105
|
+
* (同 `HostAgentRole` 少一个 `source`、同 runtime 那一层 `preview(ref)` 的入参)。
|
|
8106
|
+
*
|
|
8107
|
+
* 逐条对着今天那条最像的路(`POST /api/skills/import`):
|
|
8108
|
+
*
|
|
8109
|
+
* | 判据 | `skills/import` 今天 | 这一条 |
|
|
8110
|
+
* | ---------------- | ------------------------- | -------------------------- |
|
|
8111
|
+
* | 能不能传字节 | 不能(只收路径) | 不能 |
|
|
8112
|
+
* | 能不能指任意路径 | **能**(收一个任意 path) | **不能**(只能从清单里挑) |
|
|
8113
|
+
* | 清单是谁放的 | — | 宿主主进程 / 用户自己敲的 |
|
|
8114
|
+
*
|
|
8115
|
+
* ⚠️ 「清单」= `~/.epoch/marketplaces.json` 里那些,也就是**宿主预置的**加上
|
|
8116
|
+
* **用户自己 `epoch plugin marketplace add` 加的**。要装清单外的东西,路仍然在,
|
|
8117
|
+
* 只是**在命令行上** —— 那一问的闸门只该有一份。
|
|
8118
|
+
*
|
|
8119
|
+
* # 二、⚠️ 闸门:**绑在回环之外时那五条 POST 一律 403**
|
|
8120
|
+
*
|
|
8121
|
+
* 判据同 [wire-agent-role.ts](./wire-agent-role.ts) 那道,但**这条路比它还远一格**,
|
|
8122
|
+
* 三条:
|
|
8123
|
+
*
|
|
8124
|
+
* 1. **装一个插件会带进 hook 和 `mcp.json`,那两样起子进程。** 一份 `hooks.json`
|
|
8125
|
+
* 里的 `PreToolUse` 就是一条 shell 命令,`mcp.json` 里的一台就是一个可执行
|
|
8126
|
+
* 文件的启动项 —— 下一程启动时它们照着跑。往 `~/.epoch/agents/` 落一份 md
|
|
8127
|
+
* 只影响模型看到的字,这一条影响的是**这台机器上会跑起什么进程**;
|
|
8128
|
+
* 2. **`install/preview` 那条也吃这道闸,虽然它一个字节都不写。**
|
|
8129
|
+
* `allowRemote: true` 的宿主上,预览一条远程条目会 spawn `git clone --depth 1`
|
|
8130
|
+
* 到 staging(判据在 core 的 `install.ts` 那张表)—— 那是一次**由网线发起的
|
|
8131
|
+
* 子进程**,不是一次只读。五条一起挡,比按「写不写盘」切一刀诚实;
|
|
8132
|
+
* 3. **不能拿「反正他能让 agent 跑命令」开绿灯**,判据逐字同 `wire-agent-role.ts`
|
|
8133
|
+
* 第三节:`plan` 档和只读会话下 agent 一个字节都落不了盘,而这条端点落得了。
|
|
8134
|
+
*
|
|
8135
|
+
* ⚠️ **界面那一侧还有一道,但它是给人的**:`GET /api/config` 上带着 `lanExposed`,
|
|
8136
|
+
* 那一页据此在**按下去之前**把「这一档下装不了」画出来(决定 20 ①)。
|
|
8137
|
+
* 两道各答一半 —— 别因为界面上画了就把服务端这道摘掉,那一格是客户端自己改得动
|
|
8138
|
+
* 的布尔。
|
|
8139
|
+
*
|
|
8140
|
+
* ⚠️ **`GET /api/plugins` 刻意不挡**,而这和 `GET /api/mcp/config` 刻意挡**不矛盾**:
|
|
8141
|
+
* 那份文件里的 `env` 是明文密钥,这一份里是插件名、版本、目录路径 —— 和
|
|
8142
|
+
* `GET /api/sessions/:id/capabilities` 早就在发的那些(技能名、角色名、工作区根)
|
|
8143
|
+
* 同一档。挡掉它的唯一后果是那一页在 LAN 那一档下变成一片空白,
|
|
8144
|
+
* 而**用户最需要的恰恰是在那一档下也看得见「我装了什么、生效了没有」**。
|
|
8145
|
+
*
|
|
8146
|
+
* # 三、⚠️ `pendingRestart`:装完**不会生效**,而这一格是那句话的全部凭据
|
|
8147
|
+
*
|
|
8148
|
+
* 扩展物在启动时加载(`loadPlugins` 一程跑一次),所以一次成功的 install /
|
|
8149
|
+
* update / uninstall 之后 {@link WirePluginsResponse.pendingRestart} 变 `true`
|
|
8150
|
+
* 并且**此后不再变回去**。判据、以及「为什么重建一次就够(加载器一个字节 JS
|
|
8151
|
+
* 都不执行)」,全文在 `runtime/src/plugin-control.ts` 文件头第二节。
|
|
8152
|
+
*
|
|
8153
|
+
* **界面上必须能看出这个中间态**,而它需要**两格一起看**才成立:
|
|
8154
|
+
*
|
|
8155
|
+
* | 那一格 | 答的是 |
|
|
8156
|
+
* | ----------------------------------------- | ---------------------------------- |
|
|
8157
|
+
* | {@link WirePluginsResponse.pendingRestart} | **这一程**动过插件、还没生效 |
|
|
8158
|
+
* | {@link WirePluginEntry.active} | **这一个**此刻真的在生效吗 |
|
|
8159
|
+
*
|
|
8160
|
+
* 少了前者,用户点完那一下之后没有任何东西提示他还差一步;少了后者,一个刚装完
|
|
8161
|
+
* 的插件在列表里和一个正在生效的**长得一模一样**(那正是「点了半个反应」的形态)。
|
|
8162
|
+
* ⚠️ 而 `active: false` 有两种成因,界面要分开说:**这一程刚装上**(要重启),
|
|
8163
|
+
* 或者**它被停用 / 版本不符 / 目录没了**(那时启动诊断里有一条指名道姓的)——
|
|
8164
|
+
* 前者的判据就是 `pendingRestart`,所以那两格必须一起下发。
|
|
8165
|
+
*
|
|
8166
|
+
* # 四、这一整片可以整个不存在(503 `no-plugin-control`)
|
|
8167
|
+
*
|
|
8168
|
+
* `EpochRuntime.plugins` 在宿主没给 `hostMarketplaces` 时是 `null`,而那不是
|
|
8169
|
+
* 「底下那层起不来了」,是**「宿主没打算给用户这条路」**(判据在 `build.ts` 那一格
|
|
8170
|
+
* 的 JSDoc 上:它是一个真的新攻击面,所以是选进来的,不是白送的)。
|
|
8171
|
+
*
|
|
8172
|
+
* 六条端点那时一律 **503 `no-plugin-control`**,判据同 `no-goal-store`:
|
|
8173
|
+
* 那一档是一句关于**这个部署**的真话,不是一份「装了 0 个插件」的空列表 ——
|
|
8174
|
+
* 两者在界面上是两句完全不同的话(后者会让用户去找那个不存在的「安装」按钮)。
|
|
8175
|
+
*/
|
|
8176
|
+
/**
|
|
8177
|
+
* 一个插件是从哪儿装来的。**照抄 core 的 `PluginSourceType`,不是第二套枚举** ——
|
|
8178
|
+
* 界面据此说「这是一条软链 / 这是 clone 下来的」。
|
|
8179
|
+
*/
|
|
8180
|
+
type WirePluginSourceType = 'local' | 'git' | 'npm' | 'archive';
|
|
8181
|
+
/** 六类扩展物各贡献了几条。字段名照 core 的 `PluginCounts`,不自造映射 */
|
|
8182
|
+
interface WirePluginCounts {
|
|
8183
|
+
commands: number;
|
|
8184
|
+
roles: number;
|
|
8185
|
+
skills: number;
|
|
8186
|
+
/** hook 的**条数**(跨所有事件),不是事件种类数 */
|
|
8187
|
+
hooks: number;
|
|
8188
|
+
/** `settings.json` 里的 deny 条数。`allow` / `ask` 不算:它们本来就不生效 */
|
|
8189
|
+
denyRules: number;
|
|
8190
|
+
mcpServers: number;
|
|
8191
|
+
}
|
|
8192
|
+
/** 装着的一个插件在浏览器眼里的样子 */
|
|
8193
|
+
interface WirePluginEntry {
|
|
8194
|
+
name: string;
|
|
8195
|
+
version: string;
|
|
8196
|
+
/** 用户 / 宿主敲的那串来源,原样 */
|
|
8197
|
+
source: string;
|
|
8198
|
+
sourceType: WirePluginSourceType;
|
|
8199
|
+
/** 插件目录的绝对路径。**要发**,同 `WireRoleAddResponse.path`:出了事用户得知道去哪儿看 */
|
|
8200
|
+
path: string;
|
|
8201
|
+
/** 本地目录装的是软链(改源码、重启即生效),其余是真实拷贝 */
|
|
8202
|
+
linked: boolean;
|
|
8203
|
+
enabled: boolean;
|
|
8204
|
+
installedAt: number;
|
|
8205
|
+
/** 从市场装的才有,否则 `null`(**不是省掉这个键**,见 {@link counts}) */
|
|
8206
|
+
marketplace: string | null;
|
|
8207
|
+
/**
|
|
8208
|
+
* **这一程真的加载了它吗。**
|
|
8209
|
+
*
|
|
8210
|
+
* ⚠️ 这一格和 {@link WirePluginsResponse.pendingRestart} 是一对,判据在文件头
|
|
8211
|
+
* 第三节:少了它,一个刚装完的插件和一个正在生效的在列表里长得一模一样。
|
|
8212
|
+
*/
|
|
8213
|
+
active: boolean;
|
|
8214
|
+
/**
|
|
8215
|
+
* 它带来了什么。**只有 {@link active} 那些有**,其余是 `null` ——
|
|
8216
|
+
* 没加载的插件我们没有数过它,给一个全 0 的对象会被读成「它什么都不带」。
|
|
8217
|
+
*
|
|
8218
|
+
* ⚠️ 缺席时下发 `null` 而不是省掉这个键,判据同 `WireSkillImportEntry.conflict`:
|
|
8219
|
+
* 写成必填之后「服务端忘了转这一格」当场编译不过。
|
|
8220
|
+
*/
|
|
8221
|
+
counts: WirePluginCounts | null;
|
|
8222
|
+
}
|
|
8223
|
+
/** 市场里的一条 */
|
|
8224
|
+
interface WirePluginHit {
|
|
8225
|
+
/** 哪个市场 */
|
|
8226
|
+
marketplace: string;
|
|
8227
|
+
/** `<市场>/<插件>` —— 原样发回 {@link WirePluginInstallRequest.ref} */
|
|
8228
|
+
ref: string;
|
|
8229
|
+
name: string;
|
|
8230
|
+
/** 目录里那句。没写就是空串 */
|
|
8231
|
+
description: string;
|
|
8232
|
+
category: string | null;
|
|
8233
|
+
keywords: readonly string[];
|
|
8234
|
+
/** 解析出来的来源档位;这一条的 source 看不懂时 `null` */
|
|
8235
|
+
sourceType: WirePluginSourceType | null;
|
|
8236
|
+
/**
|
|
8237
|
+
* 在**当前**开关下装得上吗。
|
|
8238
|
+
*
|
|
8239
|
+
* ⚠️ **不从结果里剔掉装不上的那些**(runtime 那一层就已经这么定了,判据在
|
|
8240
|
+
* `PluginSearchHit.installable` 上):剔掉的话界面上少一条,而用户唯一能得出
|
|
8241
|
+
* 的结论是「这个市场里没有那个东西」。该画成不可点 **+ 说出原因**。
|
|
8242
|
+
*
|
|
8243
|
+
* ## ⚠️ `false` 有两种成因,而它们**从这两格就分得出来**,不用第三格
|
|
8244
|
+
*
|
|
8245
|
+
* | `installable` | `sourceType` | 说的是 |
|
|
8246
|
+
* | ------------- | ------------ | ----------------------------------------- |
|
|
8247
|
+
* | `false` | 非 `null` | 它是远程 source,而**远程那一档关着** |
|
|
8248
|
+
* | `false` | `null` | 这一条的 `source` 我们**看不懂**(写错了)|
|
|
8249
|
+
*
|
|
8250
|
+
* 所以这份回执上**刻意没有一格 `allowRemote`**:那个开关是 runtime 那一层的
|
|
8251
|
+
* 状态(`HostMarketplaces.allowRemote`),而 `installable` 已经是它作用在**这
|
|
8252
|
+
* 一条**上的结论了。再发一格全局开关,界面就有两个地方能推出「这条装不装得上」——
|
|
8253
|
+
* 而两处分叉的表现是一条画成可点、点了却被拒的行。判据同 `server/src/plugin.ts`
|
|
8254
|
+
* 文件头那句「这一层一条策略判断都没有」。
|
|
8255
|
+
*/
|
|
8256
|
+
installable: boolean;
|
|
8257
|
+
/** 已经装过同名的了。界面据此把「安装」换成「已装上」 */
|
|
8258
|
+
installed: boolean;
|
|
8259
|
+
}
|
|
8260
|
+
/**
|
|
8261
|
+
* `GET /api/plugins` —— 那一页的首屏,**一次取齐**。
|
|
8262
|
+
*
|
|
8263
|
+
* 判据同 `GET /api/sessions/:id/capabilities` 一个端点回三栏:这一页上的两块
|
|
8264
|
+
* (装着的 / 市场里有什么)**永远一起看** —— 「市场里那一条我是不是已经装了」
|
|
8265
|
+
* 这个问题要两块都在手上才答得出({@link WirePluginHit.installed} 就是那个答案)。
|
|
8266
|
+
*
|
|
8267
|
+
* ⚠️ **没有 `?q=`,搜索在前端做**:市场目录是加进来那一刻的本地快照
|
|
8268
|
+
* (`epoch plugin search` 要能在飞机上跑),全量本来就已经在手上了 ——
|
|
8269
|
+
* 再发一发只是把同一份东西又要一遍。口径同能力页那三栏的搜索框。
|
|
8270
|
+
*/
|
|
8271
|
+
interface WirePluginsResponse {
|
|
8272
|
+
/** 盘上那份记录(`plugins.json`),**不是这一程加载出来的那张表** */
|
|
8273
|
+
installed: readonly WirePluginEntry[];
|
|
8274
|
+
/** 已加的市场里全部条目。空关键词的答案是「全部」而不是「零条」 */
|
|
8275
|
+
hits: readonly WirePluginHit[];
|
|
8276
|
+
/**
|
|
8277
|
+
* **这一程动过插件、而那些改动还没生效。** 判据全文见文件头第三节。
|
|
8278
|
+
*
|
|
8279
|
+
* ⚠️ 它答的是「这一程」,不是「盘上有没有变化」:另一个进程装的插件不会让它
|
|
8280
|
+
* 变 true —— 这个进程确实没变过。
|
|
8281
|
+
*/
|
|
8282
|
+
pendingRestart: boolean;
|
|
8283
|
+
}
|
|
8284
|
+
declare const WIRE_PLUGINS_FIELDS: WireFields<WirePluginsResponse>;
|
|
8285
|
+
/**
|
|
8286
|
+
* `POST /api/plugins/install/preview` 的请求体。
|
|
8287
|
+
*
|
|
8288
|
+
* ⚠️ **只有一个 `ref`,而这就是这条路的安全性质本体**(文件头第一节):
|
|
8289
|
+
* 这里**没有** `source` / `path` / `content` 那三格,所以「往任意路径装一个插件」
|
|
8290
|
+
* 在这个形状下说不出口。**别在这儿加一个 `source` 重载。**
|
|
8291
|
+
*/
|
|
8292
|
+
interface WirePluginPreviewRequest {
|
|
8293
|
+
/** `<市场>/<插件>`。认不出这个形状就是 `unknown-ref`,而一条任意路径永远认不出来 */
|
|
8294
|
+
ref: string;
|
|
8295
|
+
}
|
|
8296
|
+
declare const WIRE_PLUGIN_PREVIEW_FIELDS: WireFields<WirePluginPreviewRequest>;
|
|
8297
|
+
/**
|
|
8298
|
+
* `POST /api/plugins/install` 的请求体。
|
|
8299
|
+
*
|
|
8300
|
+
* ⚠️ `token` **必传**,而且是服务端对着**这一次真正要装的那份预览**重算着对的
|
|
8301
|
+
* (判据在 `runtime/src/plugin-install.ts` 文件头)。缺了它这条端点就退化成
|
|
8302
|
+
* 「给我一个市场引用我就装」——「用户确认过的那一份」和「真装上的那一份」
|
|
8303
|
+
* 之间那条缝就回来了。
|
|
8304
|
+
*/
|
|
8305
|
+
interface WirePluginInstallRequest extends WirePluginPreviewRequest {
|
|
8306
|
+
token: string;
|
|
8307
|
+
}
|
|
8308
|
+
declare const WIRE_PLUGIN_INSTALL_FIELDS: WireFields<WirePluginInstallRequest>;
|
|
8309
|
+
/**
|
|
8310
|
+
* `POST /api/plugins/update/preview` 的请求体 —— 按**装着的那个名字**,不是 ref。
|
|
8311
|
+
*
|
|
8312
|
+
* 来源取自那条安装记录(`epoch plugin update` 也是这么干的):一个插件装完之后
|
|
8313
|
+
* 「它从哪儿来」是盘上的事实,让浏览器再报一遍就是给同一件事第二个答案。
|
|
8314
|
+
*/
|
|
8315
|
+
interface WirePluginNameRequest {
|
|
8316
|
+
name: string;
|
|
8317
|
+
}
|
|
8318
|
+
declare const WIRE_PLUGIN_NAME_FIELDS: WireFields<WirePluginNameRequest>;
|
|
8319
|
+
/**
|
|
8320
|
+
* `POST /api/plugins/update` 的请求体。
|
|
8321
|
+
*
|
|
8322
|
+
* ⚠️ **更新也要过预览这一道**,不是多余的谨慎:新版本可以加 hook(会跑 shell
|
|
8323
|
+
* 命令),一个当初只带三条命令的插件更新之后可能带上一条 `PreToolUse`。
|
|
8324
|
+
* 判据逐字同 CLI 的 `epoch plugin update` 为什么在非交互终端下拒绝无 `--yes` 运行。
|
|
8325
|
+
*/
|
|
8326
|
+
interface WirePluginUpdateRequest extends WirePluginNameRequest {
|
|
8327
|
+
token: string;
|
|
8328
|
+
}
|
|
8329
|
+
declare const WIRE_PLUGIN_UPDATE_FIELDS: WireFields<WirePluginUpdateRequest>;
|
|
8330
|
+
/**
|
|
8331
|
+
* 一次拒绝的档位。**每一档对一个不同的下一步** —— 照抄 runtime 的
|
|
8332
|
+
* `PluginRefuseReason`,一个字不改也不合并任何两档(合并的表现是界面拿同一句话
|
|
8333
|
+
* 去答两个问题)。
|
|
8334
|
+
*
|
|
8335
|
+
* | 档 | 说的是 | 用户的下一步 |
|
|
8336
|
+
* | ----------------- | ------------------------------------------------ | ---------------- |
|
|
8337
|
+
* | `unknown-ref` | 不是 `<市场>/<插件>`,或那个市场 / 插件不存在 | 换一条 |
|
|
8338
|
+
* | `remote-disabled` | 它是远程 source,而远程那一档关着 | 去开那个开关 |
|
|
8339
|
+
* | `conflict` | 已经装过同名的 | 先卸载 |
|
|
8340
|
+
* | `stale-token` | 你确认的和现在要装的对不上,**一个字节都没写** | 重看一次预览 |
|
|
8341
|
+
* | `not-installed` | update / uninstall 收到一个没装过的名字 | 先装上 |
|
|
8342
|
+
* | `nothing-to-do` | **不是出错**:软链装的插件一直是最新的 | 什么都不用做 |
|
|
8343
|
+
* | `failed` | 其余(拉不到 / 清单坏了 / 写记录时被抢先) | 看 `detail` |
|
|
8344
|
+
*/
|
|
8345
|
+
type WirePluginRefuseReason = 'unknown-ref' | 'remote-disabled' | 'conflict' | 'stale-token' | 'not-installed' | 'nothing-to-do' | 'failed';
|
|
8346
|
+
/** 一次 hook 盘点:这个事件上有几条 */
|
|
8347
|
+
interface WirePluginHookTally {
|
|
8348
|
+
type: string;
|
|
8349
|
+
count: number;
|
|
8350
|
+
}
|
|
8351
|
+
/**
|
|
8352
|
+
* 「将安装什么」那一屏的内容。
|
|
8353
|
+
*
|
|
8354
|
+
* ## ⚠️ 这一屏本身就是闸门的一半,不是一个装饰性的确认
|
|
8355
|
+
*
|
|
8356
|
+
* 判据照 `epoch plugin install` 的**安装前预览**:装之前那次确认才是这条路上
|
|
8357
|
+
* 给**人**的那道闸门。只给一个「安装」按钮不给这一屏,等于把它拿掉 ——
|
|
8358
|
+
* 而这一包东西里有两样会**起子进程**({@link hooks} / {@link mcpServers}),
|
|
8359
|
+
* 用户在按下去之前有权知道。
|
|
8360
|
+
*
|
|
8361
|
+
* 六类扩展物**全部列名字**(不只是条数),而且列的是**已加前缀**的那个名字 ——
|
|
8362
|
+
* 那才是用户之后真正会敲 / 会在权限规则里看见的那一个(判据在 core 的
|
|
8363
|
+
* `PluginInventory.mcpServers` 上)。
|
|
8364
|
+
*/
|
|
8365
|
+
interface WirePluginPreview {
|
|
8366
|
+
name: string;
|
|
8367
|
+
version: string;
|
|
8368
|
+
/** 清单里那句。没写就是空串 */
|
|
8369
|
+
description: string;
|
|
8370
|
+
/** 解析之后的来源原文(`github:acme/x` / 一条绝对路径) */
|
|
8371
|
+
source: string;
|
|
8372
|
+
sourceType: WirePluginSourceType;
|
|
8373
|
+
/** 压缩包源核对过的校验和;没有就是 `null` */
|
|
8374
|
+
checksum: string | null;
|
|
8375
|
+
author: string | null;
|
|
8376
|
+
homepage: string | null;
|
|
8377
|
+
/** 已加前缀的命令名(`my-toolkit:review`) */
|
|
8378
|
+
commands: readonly string[];
|
|
8379
|
+
roles: readonly string[];
|
|
8380
|
+
skills: readonly string[];
|
|
8381
|
+
/** ⚠️ 非空 = 这一包会跑 shell 命令。界面上那一行要醒目 */
|
|
8382
|
+
hooks: readonly WirePluginHookTally[];
|
|
8383
|
+
denyRules: number;
|
|
8384
|
+
/** ⚠️ 已加前缀(`my-toolkit__github`)。非空 = 这一包会起子进程 */
|
|
8385
|
+
mcpServers: readonly string[];
|
|
8386
|
+
/** `settings.json` 里被忽略的桶(`allow` / `ask`)。它们**不生效**,要说出来 */
|
|
8387
|
+
ignoredBuckets: readonly string[];
|
|
8388
|
+
/** 有没有 `tools/` 目录。第一版不跑 JS 工具,探出来只为了说一句话 */
|
|
8389
|
+
hasJsTools: boolean;
|
|
8390
|
+
/** 已经装过同名的了(这次会被拒)。没撞就是 `null` */
|
|
8391
|
+
conflict: {
|
|
8392
|
+
name: string;
|
|
8393
|
+
version: string;
|
|
8394
|
+
} | null;
|
|
8395
|
+
}
|
|
8396
|
+
/**
|
|
8397
|
+
* `POST /api/plugins/install/preview` / `.../update/preview` 的回执。
|
|
8398
|
+
*
|
|
8399
|
+
* ⚠️ **失败也是 200**(除了那道 403 和请求体本身坏掉)。判据同
|
|
8400
|
+
* `WireSkillImportPreviewResponse`:`remote-disabled` / `unknown-ref` 那几档
|
|
8401
|
+
* 带着一句**点得出下一步**的 `detail`,而 `{error:{code,message}}` 那个形状里
|
|
8402
|
+
* 那句话会被读成「这次请求发坏了」—— 它其实是一个如实的答案。
|
|
8403
|
+
*/
|
|
8404
|
+
interface WirePluginPreviewResponse {
|
|
8405
|
+
ok: boolean;
|
|
8406
|
+
/** 成了才有 */
|
|
8407
|
+
preview: WirePluginPreview | null;
|
|
8408
|
+
/** 成了才有。原样发回 {@link WirePluginInstallRequest.token} */
|
|
8409
|
+
token: string | null;
|
|
8410
|
+
/** 没成才有 */
|
|
8411
|
+
reason: WirePluginRefuseReason | null;
|
|
8412
|
+
/** 没成才有。**服务端渲染好的一句人话**,界面原样上屏 */
|
|
8413
|
+
detail: string | null;
|
|
8414
|
+
}
|
|
8415
|
+
declare const WIRE_PLUGIN_PREVIEW_RESPONSE_FIELDS: WireFields<WirePluginPreviewResponse>;
|
|
8416
|
+
/**
|
|
8417
|
+
* `POST /api/plugins/install` / `.../update` / `.../uninstall` 的回执。
|
|
8418
|
+
*
|
|
8419
|
+
* ⚠️ **`ok: true` 的意思是「盘上变了」,不是「能用了」** —— 后者要看
|
|
8420
|
+
* {@link pendingRestart}。这两格分开是这条路的正题,判据见文件头第三节。
|
|
8421
|
+
*
|
|
8422
|
+
* ⚠️ 失败也是 200,判据同 {@link WirePluginPreviewResponse}。
|
|
8423
|
+
* `stale-token` 那一档尤其要说清:**一个字节都没写**,重看一次预览就行。
|
|
8424
|
+
*/
|
|
8425
|
+
interface WirePluginActionResponse {
|
|
8426
|
+
ok: boolean;
|
|
8427
|
+
/**
|
|
8428
|
+
* 成了那一档:装 / 更新完之后**盘上那一条重新读出来**的样子。卸载那条路上是
|
|
8429
|
+
* `null`(它已经不在表里了 —— 回一条记录是句假话)。
|
|
8430
|
+
*
|
|
8431
|
+
* ⚠️ **是重新读出来的,不是把安装结局回显一遍**,而这一条不是洁癖:
|
|
8432
|
+
* {@link WirePluginEntry.active} 那一格只有 runtime 那一层算得出(它手里才有
|
|
8433
|
+
* 「这一程加载了谁」那张表),而**那一格正是这条路的正题**。
|
|
8434
|
+
* 尤其别在服务端硬写一个 `active: false` —— 更新一个**本来就在生效**的插件时
|
|
8435
|
+
* 它是 `true`(生效着的是旧版本),而那时那句话就成了假的。
|
|
8436
|
+
*/
|
|
8437
|
+
entry: WirePluginEntry | null;
|
|
8438
|
+
/**
|
|
8439
|
+
* **这一发之后**「动过还没生效」是不是立起来了。
|
|
8440
|
+
*
|
|
8441
|
+
* ⚠️ **必发,而且成败两支都发**:界面靠它在**这一屏上**立刻说出那句话,
|
|
8442
|
+
* 而不是等下一次 `GET /api/plugins`。一个装完之后要用户自己刷新才看得见
|
|
8443
|
+
* 中间态的界面,和不说没有区别。
|
|
8444
|
+
*/
|
|
8445
|
+
pendingRestart: boolean;
|
|
8446
|
+
/**
|
|
8447
|
+
* 服务端那句人话 —— 没成时是**拒绝的原因**(七档各一句,在 runtime / core 那侧
|
|
8448
|
+
* 渲染),卸载成功时是 core 那句回执。
|
|
8449
|
+
*
|
|
8450
|
+
* ⚠️ **装 / 更新成功那两档是 `null`,而那是刻意的**:那两档屏幕上要说的话
|
|
8451
|
+
* (「装上了什么、带来了什么、还没生效」)全都在 {@link entry} 和
|
|
8452
|
+
* {@link pendingRestart} 这两格数据里,而它是一句**界面文案** ——
|
|
8453
|
+
* 让服务端拼一句中文发上来,就绕过了浏览器那份 catalog(同 `wire-settings`
|
|
8454
|
+
* 那条「服务端的中文一律不上网线」)。
|
|
8455
|
+
*/
|
|
8456
|
+
detail: string | null;
|
|
8457
|
+
/** 没成才有 */
|
|
8458
|
+
reason: WirePluginRefuseReason | null;
|
|
8459
|
+
}
|
|
8460
|
+
declare const WIRE_PLUGIN_ACTION_FIELDS: WireFields<WirePluginActionResponse>;
|
|
8461
|
+
|
|
6464
8462
|
/**
|
|
6465
8463
|
* 一条诊断。**原样来自 `parseMcpConfig` 的 `issues`**,服务端不改写也不筛。
|
|
6466
8464
|
*
|
|
@@ -6673,6 +8671,134 @@ interface WireMcpApplyResponse {
|
|
|
6673
8671
|
}
|
|
6674
8672
|
declare const WIRE_MCP_APPLY_RESPONSE_FIELDS: WireFields<WireMcpApplyResponse>;
|
|
6675
8673
|
|
|
8674
|
+
/**
|
|
8675
|
+
* 这个进程记下了哪些审批决定,以及**逐条撤销** ——
|
|
8676
|
+
* `POST /api/sessions/:id/approval-cache`(2026-08-27)。
|
|
8677
|
+
*
|
|
8678
|
+
* ## 为什么有这一格
|
|
8679
|
+
*
|
|
8680
|
+
* 审批弹窗上点错一下,那个工具在这个进程里就死了:`allow-always` 落
|
|
8681
|
+
* `~/.epoch/approvals.json`(用户至少删得掉那个文件),而 `deny` 只在内存里、
|
|
8682
|
+
* `ttlMs: 0` 永不过期 —— 在这一格之前,**除了重启进程,一次点错的「拒绝」
|
|
8683
|
+
* 收不回来**,而屏幕上只会一直印「操作被拦截 [权限]: 已缓存拒绝」。
|
|
8684
|
+
* 判据全文在 core 的 [approval-cache.ts](../../core/src/permission/approval-cache.ts)
|
|
8685
|
+
* 的 `revoke()` 上,那张 📮 的原文在 `permission/manager.ts` 的
|
|
8686
|
+
* `checkRulesAndCache` JSDoc 里。
|
|
8687
|
+
*
|
|
8688
|
+
* ## ⚠️ 读那一半**不在这条路上**,它搭在 `GET .../security` 里
|
|
8689
|
+
*
|
|
8690
|
+
* `WirePermissionStatus.cached`([wire-security.ts](./wire-security.ts) 第二节)
|
|
8691
|
+
* 就是这张表,那个文件从这里 import {@link WireCachedApproval}。
|
|
8692
|
+
* 搭过去而不是在这儿开一条
|
|
8693
|
+
* `GET`,两条判据:
|
|
8694
|
+
*
|
|
8695
|
+
* 1. 安全中心那一屏的纪律是**一次取齐**(`wire-security.ts` 的文件头)。
|
|
8696
|
+
* 这张表和它上面那张规则表要在同一屏、同一眼里读 —— 分两发拉的话,
|
|
8697
|
+
* 屏幕上会同时挂着 T1 时刻的规则和 T2 时刻的缓存;
|
|
8698
|
+
* 2. 审批缓存和 `rules` / `shadows` / `audit` 一样是**进程级**的事实
|
|
8699
|
+
* (core 的 `permission/shared.ts`),它本来就该和那三样躺在同一个块里。
|
|
8700
|
+
*
|
|
8701
|
+
* 所以这个文件里只有**写**那一半,以及被两边共用的那一行的形状。
|
|
8702
|
+
*
|
|
8703
|
+
* ## ⚠️ 名字不许简称成 `approvals`,那个词已经被占了
|
|
8704
|
+
*
|
|
8705
|
+
* `GET /api/sessions/:id/approvals` 是**挂起的审批请求**(重连后补拉那一条),
|
|
8706
|
+
* `POST /api/approvals/:requestId` 是回答其中一条。那是「现在有人在问你」,
|
|
8707
|
+
* 这一条是「你以前答过什么」—— 两件事共用一个词的话,
|
|
8708
|
+
* `/api/sessions/:id/approvals` 上会同时挂着一个队列和一张历史表。
|
|
8709
|
+
* 所以这一路逐字叫 `approval-cache`,和引擎侧那个类同名。
|
|
8710
|
+
*/
|
|
8711
|
+
/**
|
|
8712
|
+
* 缓存里存得下的决定。**没有 `allow-once`** —— 那一档刻意不入缓存
|
|
8713
|
+
* (进了就变成「永久允许」了,判据在 core 的 `ApprovalCache.record()` 上),
|
|
8714
|
+
* 所以它在这张表上永远不会出现,写进联合等于给界面留一个画不出来的分支。
|
|
8715
|
+
*
|
|
8716
|
+
* 三档在界面上是三句不同的话,不能合成「允许 / 拒绝」两档:
|
|
8717
|
+
*
|
|
8718
|
+
* | 值 | 用户当时点的 | 撤销之后会怎样 |
|
|
8719
|
+
* | --------------- | ---------------- | ------------------------------------- |
|
|
8720
|
+
* | `deny` | 拒绝 | 同一个操作**重新弹窗**,不是静默放行 |
|
|
8721
|
+
* | `allow-session` | 允许(本次会话) | 同上;它本来也会随进程消失 |
|
|
8722
|
+
* | `allow-always` | 总是允许 | 同上,**而且盘上那份也删掉** |
|
|
8723
|
+
*/
|
|
8724
|
+
type WireCachedDecision = 'deny' | 'allow-session' | 'allow-always';
|
|
8725
|
+
/**
|
|
8726
|
+
* 面板上的一行 —— 「这个进程记下了什么」。
|
|
8727
|
+
*
|
|
8728
|
+
* ⚠️ **这不是审计流水的另一种写法。** 流水(`WireAuditRow`)记的是
|
|
8729
|
+
* 「判过什么」,一次判定一行、只增不减;这张表记的是「还记着什么」,
|
|
8730
|
+
* 一条决定一行、撤一条少一行。同一次「用户点了拒绝」在两处各留一行,
|
|
8731
|
+
* 而它们的下一步完全不同:流水那行是历史,这一行是**现在还在生效的东西**。
|
|
8732
|
+
*/
|
|
8733
|
+
interface WireCachedApproval {
|
|
8734
|
+
/**
|
|
8735
|
+
* 撤销时回发的那个 id。**引擎侧那条记录自己的 id**,不是下标 ——
|
|
8736
|
+
* 下标会在别的标签页撤掉一条之后指向另一行,而那时用户按下的「撤销」
|
|
8737
|
+
* 会撤掉一个他没看着的决定。
|
|
8738
|
+
*/
|
|
8739
|
+
id: string;
|
|
8740
|
+
/** 记在哪个工具名下(`terminal` / `file_write` / …;工具名缺失时是操作类型) */
|
|
8741
|
+
toolName: string;
|
|
8742
|
+
/**
|
|
8743
|
+
* 归一化之后的目标。**全路径 / 全命令,不脱敏** —— 判据逐字同
|
|
8744
|
+
* `WireAuditRow.target`:洗过之后用户没法判断这条决定该不该撤,
|
|
8745
|
+
* 而那正是他打开这一节的原因。
|
|
8746
|
+
*/
|
|
8747
|
+
target: string;
|
|
8748
|
+
/**
|
|
8749
|
+
* `exact` = 只对这一个目标;`prefix` = 对这个目录及其子树。
|
|
8750
|
+
*
|
|
8751
|
+
* **必须印出来**:同一个 `target` 在两档下管的范围差着一整棵目录树,
|
|
8752
|
+
* 而用户判断「这条该不该撤」靠的正是这个。
|
|
8753
|
+
*/
|
|
8754
|
+
scope: 'exact' | 'prefix';
|
|
8755
|
+
decision: WireCachedDecision;
|
|
8756
|
+
/** `Date.now()`,记下这条决定的时刻。时区归浏览器,时刻归 wire */
|
|
8757
|
+
at: number;
|
|
8758
|
+
}
|
|
8759
|
+
declare const WIRE_CACHED_APPROVAL_FIELDS: WireFields<WireCachedApproval>;
|
|
8760
|
+
/**
|
|
8761
|
+
* `POST /api/sessions/:id/approval-cache` 的请求体。
|
|
8762
|
+
*
|
|
8763
|
+
* **一次一条,按 id**,不收「把这个工具的都撤了」那种批量:批量要回答
|
|
8764
|
+
* 「第三条失败了前两条呢」(判据同 `WireSettingsWriteRequest`),
|
|
8765
|
+
* 而这条路上根本不需要 —— 面板上每一行自己带一个按钮。
|
|
8766
|
+
*
|
|
8767
|
+
* ⚠️ **没有「全清」那一档,那是刻意的。** 引擎侧 `ApprovalCache.clear()` 一直
|
|
8768
|
+
* 都在,但它会把用户攒了一整程的「总是允许」一起清掉 —— 于是「收回一个误点」
|
|
8769
|
+
* 的代价变成「后面每一次都重新问一遍」。真要那一档的时候它是**另一条路**
|
|
8770
|
+
* (另一个动词、另一次确认),不是这个请求体上多一个布尔。
|
|
8771
|
+
*/
|
|
8772
|
+
interface WireApprovalCacheRevokeRequest {
|
|
8773
|
+
id: string;
|
|
8774
|
+
}
|
|
8775
|
+
declare const WIRE_APPROVAL_CACHE_REVOKE_REQUEST_FIELDS: WireFields<WireApprovalCacheRevokeRequest>;
|
|
8776
|
+
/**
|
|
8777
|
+
* `POST /api/sessions/:id/approval-cache` 的响应。
|
|
8778
|
+
*
|
|
8779
|
+
* ## 撤一个不存在的 id 是 **200 + `revoked: false`**,不是 404
|
|
8780
|
+
*
|
|
8781
|
+
* 判据同 `WirePermissionSetResponse` 的「本来就是那一档不算错误」:挡的是
|
|
8782
|
+
* **一张开了很久的标签页**,它手里那份列表是另一个标签页撤掉那条之前的。
|
|
8783
|
+
* 用户要的结果(那条决定不再生效)**已经成立**,回一条错误只会让他去重试一件
|
|
8784
|
+
* 已经如愿的事。真正的错在别处 —— 权限层整个没起来时是 503,那一档下这一节
|
|
8785
|
+
* 压根不该画出来。
|
|
8786
|
+
*
|
|
8787
|
+
* ## 回**撤完之后的整张表**,界面直接采信,不自己推算
|
|
8788
|
+
*
|
|
8789
|
+
* 判据同 `WirePermissionSetResponse.state`。在浏览器里做一次
|
|
8790
|
+
* `list.filter(r => r.id !== id)` 看起来一样,但它答不了「刚才那一下顺带清掉了
|
|
8791
|
+
* 两条过期的 `allow-session`」(`listApprovals()` 每次都先 `prune()`)——
|
|
8792
|
+
* 于是屏幕上会留着两行早就不生效的决定,而用户会去撤它们。
|
|
8793
|
+
*/
|
|
8794
|
+
interface WireApprovalCacheRevokeResponse {
|
|
8795
|
+
/** 这一发真的撤掉了一条吗。`false` = 那个 id 不在表里(见上) */
|
|
8796
|
+
revoked: boolean;
|
|
8797
|
+
/** 撤完之后**这个进程**还记着哪些。空数组是常态 */
|
|
8798
|
+
cached: readonly WireCachedApproval[];
|
|
8799
|
+
}
|
|
8800
|
+
declare const WIRE_APPROVAL_CACHE_REVOKE_RESPONSE_FIELDS: WireFields<WireApprovalCacheRevokeResponse>;
|
|
8801
|
+
|
|
6676
8802
|
/**
|
|
6677
8803
|
* 为什么是这个隔离级别。**结构化的原因码**,替掉那句中文 `detail`。
|
|
6678
8804
|
*
|
|
@@ -6694,6 +8820,10 @@ type WireSandboxReason = 'os-isolated' | 'backend-unavailable' | 'backend-missin
|
|
|
6694
8820
|
* ⚠️ 是实测值不是配置值:`describeIsolation()` 每次都去探一遍后端。所以界面上
|
|
6695
8821
|
* 探测不到时要**如实说探测不到**,不写「已启用」—— 那是这一屏最贵的一句假话
|
|
6696
8822
|
* (设计稿在沙箱那一节把它单独圈出来了)。
|
|
8823
|
+
*
|
|
8824
|
+
* ⚠️ **`terminalEnabled` 那一格是这上面唯一的配置值**(2026-08-21 补的,方案 46
|
|
8825
|
+
* §11.3 第四条的后半格),理由写在那格的 JSDoc 上。同 {@link WirePermissionStatus}
|
|
8826
|
+
* 里 `level`(实测)+ `configured`(配置)的摆法:一个块里两个轴,各占一行。
|
|
6697
8827
|
*/
|
|
6698
8828
|
interface WireSandboxStatus {
|
|
6699
8829
|
/** `os` = 真被 OS 关起来了;`process` = 只有进程隔离(能读写整盘、能联网) */
|
|
@@ -6736,6 +8866,33 @@ interface WireSandboxStatus {
|
|
|
6736
8866
|
* 都在里面,而**这是最坏的一种误读,因为它让人放心地去把 `terminal` 放宽**。
|
|
6737
8867
|
*/
|
|
6738
8868
|
excludes: readonly string[];
|
|
8869
|
+
/**
|
|
8870
|
+
* **配置值,不是实测值** —— 这一块里唯一的例外(见文件头)。
|
|
8871
|
+
*
|
|
8872
|
+
* 它答的是「这次 `terminal` 的命令包不包」:`config.yaml` 的
|
|
8873
|
+
* `sandbox.terminal`,默认开(方案 46 §11.3 第一条)。和上面那几格实测的
|
|
8874
|
+
* 隔离能力是**两个正交的轴** —— 关掉它之后 `level` / `backend` / `reason`
|
|
8875
|
+
* 一个字都不变(这台机器的能力没变),`covers` 也照旧列着 `terminal`:
|
|
8876
|
+
* 那张清单答的是「谁的路接在隔离层上」(一份静态常量),
|
|
8877
|
+
* **不是「这次真包了没有」**。两句话合在一格里的话,一个关掉开关的 macOS
|
|
8878
|
+
* 用户和一个 Windows 用户看到的是同一句话 —— 而前者把开关打开就有沙箱,
|
|
8879
|
+
* 后者打开也没有。
|
|
8880
|
+
*
|
|
8881
|
+
* 所以界面上这一格**单独占一行**,而且摆在 `covers` 前面(判据逐字同
|
|
8882
|
+
* `epoch doctor` 的「开关」那一行,方案 46 §11.3 第四条 —— 两个出口是
|
|
8883
|
+
* 同一句话)。
|
|
8884
|
+
*
|
|
8885
|
+
* ⚠️ 服务端总是发**已经折算好的布尔**(`!== false`),不是 raw 配置值:
|
|
8886
|
+
* 缺省朝「开」是这条线上每一跳的规矩(判据在 `EpochConfig.sandbox`,一句话:
|
|
8887
|
+
* 漏填的表现是开关失灵,不是沙箱静默消失)。
|
|
8888
|
+
*
|
|
8889
|
+
* 标成可选不是「可以省」:它给的是**老版本的假件**(web 的 dev 假服务端、
|
|
8890
|
+
* 嵌入宿主的测试夹具)一个不用同步改的机会 —— 读它的一侧缺了按「开」读
|
|
8891
|
+
* (`!== false`),方向和这一跳的缺省一致,同 infra `SandboxPolicy.enabled?`
|
|
8892
|
+
* 那一格的理由。真正的生产者在 server 的 `toWireSandbox`,它的第二参是
|
|
8893
|
+
* **必填的**,漏传当场编译不过 —— 编译期门禁在生产者那一侧,不在消费者上。
|
|
8894
|
+
*/
|
|
8895
|
+
terminalEnabled?: boolean;
|
|
6739
8896
|
}
|
|
6740
8897
|
declare const WIRE_SANDBOX_STATUS_FIELDS: WireFields<WireSandboxStatus>;
|
|
6741
8898
|
/**
|
|
@@ -6851,6 +9008,28 @@ interface WirePermissionStatus {
|
|
|
6851
9008
|
rules: readonly WirePermissionRuleRow[];
|
|
6852
9009
|
shadows: readonly WirePermissionShadow[];
|
|
6853
9010
|
managed: WireManagedLock;
|
|
9011
|
+
/**
|
|
9012
|
+
* 这个进程记下了哪些**弹窗答案**(审批缓存,2026-08-27)。空数组是常态。
|
|
9013
|
+
*
|
|
9014
|
+
* ⚠️ **和上面那张 `rules` 是两张表,界面上必须分得开。** 那一张是配置文件里
|
|
9015
|
+
* 写下来的策略,这一张是用户在弹窗上点出来的记忆;判定时**规则的 deny 排在
|
|
9016
|
+
* 缓存前面**(core 的 `checkRulesAndCache`)。合成一张表、或者只给缓存那张
|
|
9017
|
+
* 配一个「撤销」按钮而不说清它撤的是哪一层,后果很具体:用户撤掉一条 deny
|
|
9018
|
+
* 之后发现还是被拦(拦他的是规则那张表),而屏幕上刚刚说「已撤销」——
|
|
9019
|
+
* 比没有那个按钮更坏。
|
|
9020
|
+
*
|
|
9021
|
+
* ⚠️ **它和 {@link WireAuditLog} 也不是一回事**:流水记「判过什么」(只增
|
|
9022
|
+
* 不减的历史),这张表记「还记着什么」(撤一条少一行)。
|
|
9023
|
+
*
|
|
9024
|
+
* 跟着 `rules` / `shadows` / `managed` / `audit` 一档,**是进程级的**、
|
|
9025
|
+
* 不跟着会话分家(判据在 core 的 `permission/shared.ts`)—— 界面上那句话
|
|
9026
|
+
* 得是「这个进程记下的」。
|
|
9027
|
+
*
|
|
9028
|
+
* 写那一半在 [wire-approval-cache.ts](./wire-approval-cache.ts)。搭在这一份
|
|
9029
|
+
* 载荷里而不是另开一条 `GET`,判据在那个文件的文件头(一句话:这一屏的纪律是
|
|
9030
|
+
* **一次取齐**,分两发拉的话屏幕上会同时挂着两个时刻的东西)。
|
|
9031
|
+
*/
|
|
9032
|
+
cached: readonly WireCachedApproval[];
|
|
6854
9033
|
}
|
|
6855
9034
|
declare const WIRE_PERMISSION_STATUS_FIELDS: WireFields<WirePermissionStatus>;
|
|
6856
9035
|
/**
|
|
@@ -8342,55 +10521,538 @@ interface WireScheduleRecordingResponse {
|
|
|
8342
10521
|
truncated: boolean;
|
|
8343
10522
|
}
|
|
8344
10523
|
declare const WIRE_SCHEDULE_RECORDING_FIELDS: WireFields<WireScheduleRecordingResponse>;
|
|
10524
|
+
/**
|
|
10525
|
+
* `GET /api/schedules/pending`。
|
|
10526
|
+
*
|
|
10527
|
+
* 三个界面读的是这一发:dock 上那一条「自动化待确认」、侧栏「自动化」入口上
|
|
10528
|
+
* 那个点、TUI 启动时那一行(TUI 那条不走 HTTP,它在同一个进程里直接调
|
|
10529
|
+
* `collectPendingApprovals`)。**投影本身在
|
|
10530
|
+
* [schedule-pending.ts](./schedule-pending.js)**,那儿写着两道筛和「同一条任务
|
|
10531
|
+
* 只报最近那一次」的判据,这里只是它的信封。
|
|
10532
|
+
*
|
|
10533
|
+
* ## ⚠️ 为什么不搭 `GET /api/schedules` 那一发的车
|
|
10534
|
+
*
|
|
10535
|
+
* 那一发够用:`schedules` 里有 `allowRules`、`recentRuns` 里有 `pendingApprovals`,
|
|
10536
|
+
* 浏览器自己就能算。**但读它的人不是那一屏** —— 是**侧栏和 dock**,也就是
|
|
10537
|
+
* 每一次首屏都要问一次的东西。那一发回的是「全部任务 + 最近 100 条运行 + 一次
|
|
10538
|
+
* 平台探测 + 表单缺省」,为一个角标拉那么一份,代价会落在每一次刷新上。
|
|
10539
|
+
*
|
|
10540
|
+
* 反过来也不合并进 `GET /api/config`(首屏那一发):那一份是**进程级的常量**
|
|
10541
|
+
* (品牌 / 口径 / 诊断),一次都不按时间变;而这个数会因为「另一个进程里的
|
|
10542
|
+
* 执行器刚跑了一次」而变。混进去之后,「刷新才更新」会从一句如实的话变成
|
|
10543
|
+
* 一个看不出来的坑。
|
|
10544
|
+
*
|
|
10545
|
+
* ## ⚠️ 它**不轮询**,而这一条要如实说给用户听
|
|
10546
|
+
*
|
|
10547
|
+
* 一次定时运行发生在**另一个进程**里(OS 调度器起的执行器),服务进程压根不知道
|
|
10548
|
+
* 它跑过 —— 所以这个数只在「取数那一刻」是真的,SSE 那条路在这件事上没有素材
|
|
10549
|
+
* (不是漏接:没有任何一帧能报它)。界面因此在两个时刻拉它:首屏,以及每次自己
|
|
10550
|
+
* 动过手之后(按了「加进授权清单」、跑了一次)。
|
|
10551
|
+
*/
|
|
10552
|
+
interface WireSchedulePendingResponse {
|
|
10553
|
+
/** 最久的在前。一条都没有时空数组 —— 那时侧栏那个点不画 */
|
|
10554
|
+
items: readonly SchedulePendingItem[];
|
|
10555
|
+
}
|
|
10556
|
+
declare const WIRE_SCHEDULE_PENDING_FIELDS: WireFields<WireSchedulePendingResponse>;
|
|
8345
10557
|
|
|
8346
10558
|
/**
|
|
8347
|
-
*
|
|
10559
|
+
* 目标那一格的网线形状([方案 52](../../../docs/verify/VERIFY_RECORD-52-goal.md) PR-4 的
|
|
10560
|
+
* 「上网线」那一半):
|
|
8348
10561
|
*
|
|
8349
|
-
*
|
|
10562
|
+
* ```
|
|
10563
|
+
* GET /api/sessions/:id/goal 这段会话的目标 + 表单那几格上限与缺省
|
|
10564
|
+
* POST /api/sessions/:id/goal 建一个(**人专用**)
|
|
10565
|
+
* PATCH /api/sessions/:id/goal 五个动作之一:改正文 / 改预算 / 按停 / 放行 / 报完成
|
|
10566
|
+
* DELETE /api/sessions/:id/goal 清掉
|
|
10567
|
+
* GET /api/goals/blocked 哪几段会话此刻卡着(dock 第四档,**进程级**)
|
|
10568
|
+
* ```
|
|
8350
10569
|
*
|
|
8351
|
-
*
|
|
8352
|
-
* 压缩发生时他只知道「压了」,不知道「为什么这么快就压了」。
|
|
8353
|
-
* 而答案常常很具体:一个 MCP server 的工具定义吃掉了四分之一窗口。
|
|
10570
|
+
* ## 这是一份**结构镜像**,不是把 core 的 `Goal` 导出去
|
|
8354
10571
|
*
|
|
8355
|
-
*
|
|
10572
|
+
* 口径逐字照 [wire-schedule.ts](./wire-schedule.js):那个文件只装两样东西 ——
|
|
10573
|
+
* 包一层的响应壳,以及**界面要而库里没有的那几格**。这一份多一样活,因为
|
|
10574
|
+
* 定时任务那一份的真身(`ScheduleDefinition`)2026-08-17 就已经住在 protocol 里了,
|
|
10575
|
+
* 而 `Goal` 住在 core(`core/src/goal/types.ts`),而 **`packages/web` 只依赖
|
|
10576
|
+
* protocol + view,它够不着 core**(`check-layers.mjs` 是硬闸门)。
|
|
10577
|
+
*
|
|
10578
|
+
* 于是照 `WireScheduleIssueCode` 那处的老办法:**抄一份 + 一条钉住「逐项相等」的
|
|
10579
|
+
* 用例**(`core/__tests__/goal-wire.test.ts`)。抄的代价是两份会走散,而那条用例
|
|
10580
|
+
* 就是不让它走散的那道闸 —— 它两个方向都断(互相可赋值 ⟺ 两个联合逐项相等),
|
|
10581
|
+
* 所以「一边多一个态」和「一边少一个态」都会当场红在 `pnpm typecheck` 上。
|
|
10582
|
+
*
|
|
10583
|
+
* ## 镜像和真身刻意**不同**的三处,每一处都是收窄
|
|
10584
|
+
*
|
|
10585
|
+
* | 真身(core 的 `Goal`) | 这一份 | 为什么 |
|
|
10586
|
+
* | ---------------------- | -------------- | --------------------------------------------- |
|
|
10587
|
+
* | `id` | **没有** | 那是 `goals` 表的主键,浏览器拿它什么都做不了 |
|
|
10588
|
+
* | `sessionId` | **没有** | 会话级那条路上它在 URL 里;跨会话那条另有一格 |
|
|
10589
|
+
* | 四态都能写 | **写只有三态** | `blocked` 不在写那条路上,见下面第四节 |
|
|
10590
|
+
*
|
|
10591
|
+
* 前两条的判据同 `CheckpointSummaryView` 那处「没有 `cursor`」:镜像上没有这个
|
|
10592
|
+
* 字段是**编译期生效**的 —— 投影里写不出 `goal.id`,写了当场不过。
|
|
10593
|
+
*
|
|
10594
|
+
* ## ⚠️ 网线上发的是**码**,不是句子
|
|
10595
|
+
*
|
|
10596
|
+
* `phase` / `block.code` / `WireGoalRefusal` 三样全是 kebab-case 的码。两条判据,
|
|
10597
|
+
* 缺一条都不够(逐字同 `wire-schedule.ts`):
|
|
10598
|
+
*
|
|
10599
|
+
* 1. 服务端的中文一律不上网线 —— 浏览器切成英文之后服务端渲染好的字符串换不了;
|
|
10600
|
+
* 2. **同一条拒绝的措辞在 CLI 和这一格上本来就该不一样**:CLI 说
|
|
10601
|
+
* 「先 `/goal clear`」,而这一格上没有 `/goal clear` 这个东西。
|
|
10602
|
+
*
|
|
10603
|
+
* ⚠️ 唯一的例外是 `block.message`,而它**不是我们渲染的文案**:它是模型(或策略层)
|
|
10604
|
+
* 写的一句自然语言,会落库、要被 grep,所以它跟着写它的那一刻走、不跟界面语言走
|
|
10605
|
+
* (判据在 `core/src/goal/wording.ts` 和 52 §4.3)。原样过网线,不翻。
|
|
8356
10606
|
*/
|
|
8357
|
-
/**
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
10607
|
+
/**
|
|
10608
|
+
* 四态。**真源在 core 的 `GoalPhase`,这是网线上那一份**,见文件头。
|
|
10609
|
+
*
|
|
10610
|
+
* | 态 | 谁能转进来 | 语义 |
|
|
10611
|
+
* | ---------- | -------------- | ---------------------------------- |
|
|
10612
|
+
* | `active` | 人(建、放行) | 在跑,预算还有 |
|
|
10613
|
+
* | `paused` | 人 | 人主动按停,预算不动 |
|
|
10614
|
+
* | `blocked` | 模型 / 策略层 | 撞上了具体的东西,**必须说是什么** |
|
|
10615
|
+
* | `complete` | 模型(带依据) | 完成判据满足了 |
|
|
10616
|
+
*
|
|
10617
|
+
* ⚠️ **预算耗尽转 `blocked` 而不是 `complete`**(GOALS.md §四):转 `complete`
|
|
10618
|
+
* 正是今天 `maxTurns` 的病 —— 撞上了之后分不出「干完了」还是「跑飞了」。
|
|
10619
|
+
* 界面上这四档因此必须是**四种画法**,不许把 `blocked` 和 `complete` 合成
|
|
10620
|
+
* 一句「结束了」。
|
|
10621
|
+
*/
|
|
10622
|
+
type WireGoalPhase = 'active' | 'paused' | 'blocked' | 'complete';
|
|
10623
|
+
/**
|
|
10624
|
+
* 全集。**顺序也钉着** —— 那条「逐项相等」的用例比的是数组,不是集合,
|
|
10625
|
+
* 判据逐字同 `WIRE_SCHEDULE_ISSUE_CODES`:顺序对不上通常意味着有人在一边插了
|
|
10626
|
+
* 一条而另一边追加在末尾,那时两份的内容还相等,但下一次插入就会真的分叉。
|
|
10627
|
+
*/
|
|
10628
|
+
declare const WIRE_GOAL_PHASES: readonly WireGoalPhase[];
|
|
10629
|
+
/** 从网线上读回来的字符串是不是一个合法的 phase */
|
|
10630
|
+
declare function isWireGoalPhase(value: string): value is WireGoalPhase;
|
|
10631
|
+
/**
|
|
10632
|
+
* 卡住的理由。**两个字段都必有,而且各有不可替代的用途**(GOALS.md §三):
|
|
10633
|
+
*
|
|
10634
|
+
* - `code` 让**程序**能路由(`needs-approval` 走审批、`needs-credential` 走凭据)
|
|
10635
|
+
* - `message` 让**人**能看懂(「差一条 `terminal(pnpm publish)` 的授权」)
|
|
10636
|
+
*
|
|
10637
|
+
* 只有 `message` 程序没法分类;只有 `code` 人看不懂。
|
|
10638
|
+
*
|
|
10639
|
+
* ⚠️ **`code` 的取值是开放的** kebab-case(策略层会长出新分类),所以这里是
|
|
10640
|
+
* `string` 而不是一个联合。界面按已知的那几个给图标 / 措辞,**认不出来的那些
|
|
10641
|
+
* 必须照旧画得出来**(原样印那个码 + `message`)—— 收成闭集的代价是每加一种
|
|
10642
|
+
* 阻塞都要改一次协议,而漏改的表现是那一档在屏幕上静默消失。
|
|
10643
|
+
* 已知的那张表在 [docs/GOALS.md](../../../docs/GOALS.md)。
|
|
10644
|
+
*/
|
|
10645
|
+
interface WireGoalBlock {
|
|
10646
|
+
code: string;
|
|
10647
|
+
message: string;
|
|
8365
10648
|
}
|
|
10649
|
+
declare const WIRE_GOAL_BLOCK_FIELDS: WireFields<WireGoalBlock>;
|
|
10650
|
+
/**
|
|
10651
|
+
* 网线上那一份目标。**`id` 和 `sessionId` 都没有**,理由见文件头那张表。
|
|
10652
|
+
*
|
|
10653
|
+
* `block` / `completeEvidence` 两格的**在与不在本身就是信息**,别在服务端补一个
|
|
10654
|
+
* 空串:`phase === 'blocked'` 时 `block` 必有、其余态必无(core 的不变式),
|
|
10655
|
+
* 界面据此不用为「blocked 但没有理由」这种说不出口的状态留一条分支。
|
|
10656
|
+
*/
|
|
10657
|
+
interface WireGoal {
|
|
10658
|
+
/** 完成判据本身。**只有人能改** */
|
|
10659
|
+
objective: string;
|
|
10660
|
+
phase: WireGoalPhase;
|
|
10661
|
+
/** `phase === 'blocked'` 时必有,其余态必无 */
|
|
10662
|
+
block?: WireGoalBlock;
|
|
10663
|
+
/** 预算:最多几个**被接纳的轮次**(GOALS.md §四,不是模型请求次数) */
|
|
10664
|
+
maxRounds: number;
|
|
10665
|
+
roundsUsed: number;
|
|
10666
|
+
/** `phase === 'complete'` 时必有:凭什么算完成 */
|
|
10667
|
+
completeEvidence?: string;
|
|
10668
|
+
/** 毫秒时间戳 */
|
|
10669
|
+
createdAt: number;
|
|
10670
|
+
updatedAt: number;
|
|
10671
|
+
}
|
|
10672
|
+
declare const WIRE_GOAL_FIELDS: WireFields<WireGoal>;
|
|
8366
10673
|
/**
|
|
8367
|
-
*
|
|
10674
|
+
* 建 / 改目标那个表单该按什么尺子来。
|
|
8368
10675
|
*
|
|
8369
|
-
*
|
|
8370
|
-
*
|
|
8371
|
-
*
|
|
8372
|
-
*
|
|
10676
|
+
* 值全部来自 core 的 `DEFAULT_GOAL_MAX_ROUNDS` / `MAX_GOAL_MAX_ROUNDS` /
|
|
10677
|
+
* `MAX_OBJECTIVE_CHARS` / `MAX_REASON_CHARS`,服务端原样转发。判据逐字同
|
|
10678
|
+
* `WireScheduleDefaults`:**不让表单自己写死一份** —— 那几个数一改,
|
|
10679
|
+
* 界面上那个 `maxlength` 和预填的 10 就是一个和引擎不一样的值,而它编译得过、
|
|
10680
|
+
* 跑得通、只在用户正好写到第 2001 个字的那一刻表现成「保存了但被截断了」。
|
|
8373
10681
|
*
|
|
8374
|
-
*
|
|
8375
|
-
*
|
|
8376
|
-
*
|
|
10682
|
+
* ⚠️ 它们**不是校验**,是**尺子**:真正判「这个预算合不合法」的是
|
|
10683
|
+
* `GoalService`(回一个 `bad-budget`),界面这一侧只用它们提前把话说出来
|
|
10684
|
+
* (`max=999` 那个 `input`、那句「上限 2000 字」)。两处判据分叉时红的是
|
|
10685
|
+
* 服务端那一份,而它才是写库的那一道 —— 同 `WireScheduleIssueCode` 的分工。
|
|
8377
10686
|
*/
|
|
8378
|
-
interface
|
|
8379
|
-
/**
|
|
8380
|
-
|
|
8381
|
-
/**
|
|
8382
|
-
|
|
8383
|
-
/**
|
|
8384
|
-
|
|
8385
|
-
|
|
10687
|
+
interface WireGoalLimits {
|
|
10688
|
+
/** 建的时候不给预算就是这个数(今天是 10) */
|
|
10689
|
+
defaultMaxRounds: number;
|
|
10690
|
+
/** 预算的硬上限(今天是 999)。下限恒为 1,不必发 */
|
|
10691
|
+
maxRounds: number;
|
|
10692
|
+
/** 目标正文的字数上限。它是**完成判据**不是计划正文 */
|
|
10693
|
+
maxObjectiveChars: number;
|
|
10694
|
+
/** `complete` 的依据 / `blocked` 的 message 各自的字数上限 */
|
|
10695
|
+
maxReasonChars: number;
|
|
10696
|
+
}
|
|
10697
|
+
declare const WIRE_GOAL_LIMITS_FIELDS: WireFields<WireGoalLimits>;
|
|
10698
|
+
/**
|
|
10699
|
+
* `GET /api/sessions/:id/goal`。
|
|
10700
|
+
*
|
|
10701
|
+
* **一发拿齐目标和尺子**,判据同 `GET /api/schedules` 那一发把 `defaults` 捎上:
|
|
10702
|
+
* 这一格上「看一眼」和「改一下」是同一屏(没有目标时它就是那个建的表单),
|
|
10703
|
+
* 拆两发的话表单要么先画一个没有 `maxlength` 的输入框、要么多等一个来回。
|
|
10704
|
+
*
|
|
10705
|
+
* ⚠️ **`goal: null` 是「这段会话没有目标」,不是「问不出来」。** 后者压根不走
|
|
10706
|
+
* 这条 200 —— 这个进程没有目标存储(会话库起不来)时整条端点回 503
|
|
10707
|
+
* `no-goal-store`,判据在 `server/src/goal/handlers.ts` 的文件头。
|
|
10708
|
+
* 两者混成一个 `null` 的代价很具体:界面会对着一台没有会话库的机器画出
|
|
10709
|
+
* 「这个会话还没有目标,来定一条」,而按下去必然失败。
|
|
10710
|
+
*/
|
|
10711
|
+
interface WireGoalResponse {
|
|
10712
|
+
goal: WireGoal | null;
|
|
10713
|
+
limits: WireGoalLimits;
|
|
10714
|
+
}
|
|
10715
|
+
declare const WIRE_GOAL_RESPONSE_FIELDS: WireFields<WireGoalResponse>;
|
|
10716
|
+
/**
|
|
10717
|
+
* `POST /api/sessions/:id/goal` —— 建一个。
|
|
10718
|
+
*
|
|
10719
|
+
* ⚠️ **只有人走得到这条路,而这件事不是这一层守的。** 目标是完成判据,而完成
|
|
10720
|
+
* 判据是用来约束模型的 —— 让它自己写等于让它自己出考卷(GOALS.md §五)。
|
|
10721
|
+
* 模型手里那个 `goal` 工具的 schema 里**根本没有建目标这个 action**,
|
|
10722
|
+
* 所以「只有人能建」靠的是没有第二条路,不是在这儿加一个 `by: 'human'` 参数
|
|
10723
|
+
* (那等于承认存在一条模型走得到的路,只是我们在运行期拒了它)。
|
|
10724
|
+
*
|
|
10725
|
+
* `maxRounds` 不给就是 {@link WireGoalLimits.defaultMaxRounds}。**这一格可选是
|
|
10726
|
+
* 对的**,和 `WireScheduleCreateRequest.maxBudgetUsd` 那处「必填、没有默认值」
|
|
10727
|
+
* 刻意相反:那一格的假默认会变成一笔真的钱,这一格的默认值花的是轮次 ——
|
|
10728
|
+
* 而 10 轮跑完的下场是一条 `blocked(budget-exhausted)` 的记录,不是账单。
|
|
10729
|
+
*/
|
|
10730
|
+
interface WireGoalCreateRequest {
|
|
10731
|
+
objective: string;
|
|
10732
|
+
maxRounds?: number;
|
|
10733
|
+
}
|
|
10734
|
+
declare const WIRE_GOAL_CREATE_FIELDS: WireFields<WireGoalCreateRequest>;
|
|
10735
|
+
/**
|
|
10736
|
+
* `PATCH /api/sessions/:id/goal` —— **一条请求恰好一个动作**。
|
|
10737
|
+
*
|
|
10738
|
+
* ## 为什么是判别联合,不是 `WireScheduleUpdateRequest` 那种「没给的不动」
|
|
10739
|
+
*
|
|
10740
|
+
* 那一份改的是**一行任务的字段**,一次 UPDATE 落盘,漏给哪一格就是不动那一格。
|
|
10741
|
+
* 这一份改的是**五个动词**(`GoalService` 上五个方法),每一个各有自己的
|
|
10742
|
+
* 前置条件和拒绝码。写成「字段都可选」的话:
|
|
10743
|
+
*
|
|
10744
|
+
* 1. `{objective, maxRounds}` 一起给要在服务端调两次,而第二次被拒时前一次
|
|
10745
|
+
* **已经写进库了** —— 一次 PATCH 半成功,而回执里说不清成了哪一半;
|
|
10746
|
+
* 2. `phase` 那一格会长成一个能写四个值的口子,于是浏览器写得出
|
|
10747
|
+
* `phase: 'blocked'` —— 见下面那条 ⚠️。
|
|
10748
|
+
*
|
|
10749
|
+
* ## ⚠️ 这条路上**表达不出 `blocked`**,而这是本文件最要紧的一处收窄
|
|
10750
|
+
*
|
|
10751
|
+
* `blocked` 是模型 / 策略层的态(GOALS.md §三 那张「谁能转进来」的表)。
|
|
10752
|
+
* 让浏览器报得出 `blocked` 就等于让它编一个 `code` + 一句 `message` 出来,
|
|
10753
|
+
* 而那个 `code` 是**给程序路由用的键** —— dock 第四档按它决定给「加进授权清单」
|
|
10754
|
+
* 还是「去那段会话」。编一个 `needs-approval` 出来的方向是**提权**。
|
|
10755
|
+
*
|
|
10756
|
+
* 所以四态里只有三态在这个联合里(`active` = 放行、`paused` = 按停、
|
|
10757
|
+
* `complete` = 报完成),第四态**在类型上说不出口**。判据同 52 §5.1 结论 3:
|
|
10758
|
+
* 「不许去反推一条规则出来:那是猜,而猜错的方向是提权。」
|
|
10759
|
+
*/
|
|
10760
|
+
type WireGoalUpdateRequest =
|
|
10761
|
+
/** 改措辞。**不重置轮次、也不改状态** */
|
|
10762
|
+
{
|
|
10763
|
+
action: 'edit';
|
|
10764
|
+
objective: string;
|
|
10765
|
+
}
|
|
10766
|
+
/**
|
|
10767
|
+
* 改预算。⚠️ **两个方向都会顺手改 phase**(52 §3.1 第 1 条):调到已用轮次
|
|
10768
|
+
* 之下 → 当场转 `blocked(budget-exhausted)`;给一个 `budget-exhausted` 的
|
|
10769
|
+
* 目标加预算 → 直接放回 `active`。界面因此**必须拿回执里那份目标覆盖本地那份**,
|
|
10770
|
+
* 不许只把输入框里那个数写回去。
|
|
10771
|
+
*/
|
|
10772
|
+
| {
|
|
10773
|
+
action: 'budget';
|
|
10774
|
+
maxRounds: number;
|
|
10775
|
+
}
|
|
10776
|
+
/** 人按停。轮次不再推进,目标留着 */
|
|
10777
|
+
| {
|
|
10778
|
+
action: 'pause';
|
|
10779
|
+
}
|
|
10780
|
+
/** 人放行。`paused` / `blocked` 都能恢复;预算耗尽那一档要先加预算 */
|
|
10781
|
+
| {
|
|
10782
|
+
action: 'resume';
|
|
10783
|
+
}
|
|
10784
|
+
/** 人替它报完成。**依据必填** —— 那段话是人回来核对的唯一凭据 */
|
|
10785
|
+
| {
|
|
10786
|
+
action: 'complete';
|
|
10787
|
+
evidence: string;
|
|
10788
|
+
};
|
|
10789
|
+
/** 五个动作的名字。界面和请求体校验共用这一份,别在两处各列一遍 */
|
|
10790
|
+
declare const WIRE_GOAL_ACTIONS: readonly WireGoalUpdateRequest['action'][];
|
|
10791
|
+
/**
|
|
10792
|
+
* 一次改动被拒的原因。**真源在 core 的 `GoalRefusal`,这是网线上那一份**。
|
|
10793
|
+
*
|
|
10794
|
+
* 每一个码都对应用户**下一步该做的一件不同的事**,这是拆码的唯一判据
|
|
10795
|
+
* (逐字照 core 那个类型上的注释):`busy` 要先清掉现在那个,`no-goal` 要先建
|
|
10796
|
+
* 一个,`empty-objective` 要重写一句。所以界面上这十条是**十句不同的话**,
|
|
10797
|
+
* 合成一句「操作失败」等于把这十条判据一起扔了。
|
|
10798
|
+
*/
|
|
10799
|
+
type WireGoalRefusal =
|
|
10800
|
+
/** 已经有一个还没结束的目标 */
|
|
10801
|
+
'busy'
|
|
10802
|
+
/** 这个会话现在没有目标 */
|
|
10803
|
+
| 'no-goal'
|
|
10804
|
+
/** 目标正文是空的 */
|
|
10805
|
+
| 'empty-objective'
|
|
10806
|
+
/** 目标正文超过 {@link WireGoalLimits.maxObjectiveChars} */
|
|
10807
|
+
| 'objective-too-long'
|
|
10808
|
+
/** 预算不是 1..{@link WireGoalLimits.maxRounds} 之间的整数 */
|
|
10809
|
+
| 'bad-budget'
|
|
10810
|
+
/** `blocked` 少了 `code` 或者它不是 kebab-case。**这条到不了浏览器**,见下 */
|
|
10811
|
+
| 'bad-block-code'
|
|
10812
|
+
/** `blocked` 少了 `message`。同上 */
|
|
10813
|
+
| 'empty-block-message'
|
|
10814
|
+
/** 报完成少了依据 */
|
|
10815
|
+
| 'empty-evidence'
|
|
10816
|
+
/** 这个态上做不了这件事(`complete` 的目标不能按停,等等) */
|
|
10817
|
+
| 'wrong-phase'
|
|
10818
|
+
/** SQLite 写失败。**不吞** —— goal 的全部意义是留下痕迹 */
|
|
10819
|
+
| 'unavailable';
|
|
10820
|
+
/**
|
|
10821
|
+
* 全集,顺序钉着,判据同 {@link WIRE_GOAL_PHASES}。
|
|
10822
|
+
*
|
|
10823
|
+
* ⚠️ 里面那两条 `bad-block-code` / `empty-block-message` **在今天的网线上到不了
|
|
10824
|
+
* 浏览器**(`blocked` 压根不在 {@link WireGoalUpdateRequest} 里)。仍然照抄进来,
|
|
10825
|
+
* 两个理由:这一份是 core 那个联合的镜像,**镜像少一条就不是镜像了**(那条
|
|
10826
|
+
* 「逐项相等」的用例断的正是它);而界面那张文案表按码穷举,少两条的话下一次
|
|
10827
|
+
* 真有人给 `blocked` 开一条路时,屏幕上会是一个没有文案的空白拒绝。
|
|
10828
|
+
*/
|
|
10829
|
+
declare const WIRE_GOAL_REFUSALS: readonly WireGoalRefusal[];
|
|
10830
|
+
/**
|
|
10831
|
+
* 建 / 改的回执。
|
|
10832
|
+
*
|
|
10833
|
+
* ## ⚠️ 被拒是 **200 + `ok: false`**,不是 4xx
|
|
10834
|
+
*
|
|
10835
|
+
* 判据同 `WireScheduleSaveResponse` 那条「校验不过是 200 + `issues`」:请求本身
|
|
10836
|
+
* 完全合法,是**这个值 / 这个态**不接受 —— 而那句话要出现在用户刚敲字的那一格
|
|
10837
|
+
* 旁边,不是一条「请求发坏了、再试一次」的横幅。真正的 4xx 只留给
|
|
10838
|
+
* 「这条 URL 上的会话不存在」(404)和「请求体不是那个形状」(400)。
|
|
10839
|
+
*
|
|
10840
|
+
* ⚠️ **`unavailable` 也走这条 200**,而它看起来最像一个 5xx。刻意的:它是
|
|
10841
|
+
* {@link WireGoalRefusal} 这个**闭集**里的一员,而界面按码穷举出一张文案表 ——
|
|
10842
|
+
* 把闭集里的一个成员挪到另一个 HTTP 状态上,等于让浏览器为同一个联合写两条
|
|
10843
|
+
* 处理路径,而那两条里被走到的那条永远是常见的那几个码。
|
|
10844
|
+
* 真正「这个进程压根没有目标存储」那一档不走这条路:整条端点 503 `no-goal-store`。
|
|
10845
|
+
*
|
|
10846
|
+
* ⚠️ **`goal` 必带(可以是 `null`),不是可选。** `conformsToWire()` 查的是
|
|
10847
|
+
* 「列出来的字段在不在」,写成可选就等于这一格从来没被那道闸检查过。
|
|
10848
|
+
* 被拒时它是 `null`:**不回一份「当前那个目标」** —— 那样界面分不出
|
|
10849
|
+
* 「这是我刚改成的」还是「这是我没改动的」,而这两者的下一步不同。
|
|
10850
|
+
*/
|
|
10851
|
+
interface WireGoalWriteResponse {
|
|
10852
|
+
ok: boolean;
|
|
10853
|
+
/** 写完之后那一份;`ok: false` 时 `null` */
|
|
10854
|
+
goal: WireGoal | null;
|
|
10855
|
+
/** `ok: false` 时必有 */
|
|
10856
|
+
reason?: WireGoalRefusal;
|
|
10857
|
+
}
|
|
10858
|
+
declare const WIRE_GOAL_WRITE_FIELDS: WireFields<WireGoalWriteResponse>;
|
|
10859
|
+
/**
|
|
10860
|
+
* `DELETE /api/sessions/:id/goal` 的回执 —— 它没有「写完之后那份目标」。
|
|
10861
|
+
*
|
|
10862
|
+
* `cleared: false` + `ok: true` = **本来就没有**(幂等,不是失败)。这两格分开
|
|
10863
|
+
* 而不是一个布尔:界面对「清掉了」和「本来就没有」说的不是同一句话,而
|
|
10864
|
+
* 「清了个不存在的东西」根本不该报错 —— 用户的意图已经达成了。
|
|
10865
|
+
*/
|
|
10866
|
+
interface WireGoalClearResponse {
|
|
10867
|
+
ok: boolean;
|
|
10868
|
+
cleared: boolean;
|
|
10869
|
+
reason?: WireGoalRefusal;
|
|
10870
|
+
}
|
|
10871
|
+
declare const WIRE_GOAL_CLEAR_FIELDS: WireFields<WireGoalClearResponse>;
|
|
10872
|
+
/**
|
|
10873
|
+
* 一段卡住了的会话。
|
|
10874
|
+
*
|
|
10875
|
+
* ## 组合而不是摊平 —— 判据同 `WireScheduleRow`
|
|
10876
|
+
*
|
|
10877
|
+
* `goal` 原样嵌着,外面那两格是**这一条通知要的宾语**。摊平之后
|
|
10878
|
+
* `GET /api/sessions/:id/goal` 的响应和这一份会长得像同一个东西,而它们答的是
|
|
10879
|
+
* 两个问题(「我正看着的这段会话怎么样」/「**别的**哪几段卡着」)。
|
|
10880
|
+
*
|
|
10881
|
+
* ## `sessionTitle` 就是「界面要而库里没有的那一格」
|
|
10882
|
+
*
|
|
10883
|
+
* `goals` 表里没有标题(它只认 `session_id`),而 dock 第四档的宾语是**会话标题**
|
|
10884
|
+
* ——GOALS.md §三 那张收敛表上写死的。**服务端 join 而不是浏览器再拉一发会话
|
|
10885
|
+
* 列表**:那样这条通知的宾语来自另一个瞬间,于是一段刚被改名的会话在 dock 上
|
|
10886
|
+
* 会印着旧名字,而没有任何东西会报错。
|
|
10887
|
+
*
|
|
10888
|
+
* 标题是空串时**原样发空串,不在服务端兜一个「未命名会话」**:那句话是界面文案
|
|
10889
|
+
* (`web.ui.topbar.untitled`),服务端渲染好一份发过来,浏览器切成英文就换不了了。
|
|
10890
|
+
*/
|
|
10891
|
+
interface WireBlockedGoal {
|
|
10892
|
+
sessionId: string;
|
|
10893
|
+
/** 会话标题。空串 = 还没有标题,措辞归界面 */
|
|
10894
|
+
sessionTitle: string;
|
|
10895
|
+
/** `phase` 恒为 `blocked`,`block` 因此恒在 —— 但类型上仍然是那一份 `WireGoal` */
|
|
10896
|
+
goal: WireGoal;
|
|
10897
|
+
}
|
|
10898
|
+
declare const WIRE_BLOCKED_GOAL_FIELDS: WireFields<WireBlockedGoal>;
|
|
10899
|
+
/**
|
|
10900
|
+
* `GET /api/goals/blocked`。
|
|
10901
|
+
*
|
|
10902
|
+
* ## ⚠️ 为什么单开一条,而不是搭会话列表那一发的车
|
|
10903
|
+
*
|
|
10904
|
+
* 判据同 `GET /api/schedules/pending`:读它的是 **dock**,也就是每一次首屏都要
|
|
10905
|
+
* 问一次的东西。会话列表那一发回的是「最近 50 段会话 + 每段的挂起计数 + 活性」,
|
|
10906
|
+
* 而这一条要的是「哪几段的目标卡着」——两件事的真源不同(一个在 Hub 的内存里,
|
|
10907
|
+
* 一个在 `goals` 表里),合并的下场是那一发的形状为一个角标变宽一格。
|
|
10908
|
+
*
|
|
10909
|
+
* 也不合并进 `GET /api/config`(首屏那一发):那一份是**进程级的常量**
|
|
10910
|
+
* (品牌 / 口径 / 诊断),一次都不按时间变;而这个数会因为「另一段会话刚撞上
|
|
10911
|
+
* 预算」而变。
|
|
10912
|
+
*
|
|
10913
|
+
* ## ⚠️ 它**不订阅**,而这一条要如实说给用户听
|
|
10914
|
+
*
|
|
10915
|
+
* 事件流上**没有「目标变了」这一帧**(52 落地那一轮一个事件都没加,那是刻意的:
|
|
10916
|
+
* 目标是状态加策略,不是循环结构)。所以这个数只在「取数那一刻」为真,
|
|
10917
|
+
* 界面在三个时刻拉它:首屏、这段会话的回合回到 idle、以及用户自己动过手之后。
|
|
10918
|
+
*
|
|
10919
|
+
* ⚠️ 于是**别的**会话在后台撞上预算这件事,要等下一次上面那三个时刻之一才看得见。
|
|
10920
|
+
* 如实记着 —— 不是漏接:真要即时,前置是在事件流上加一帧,而那是独立的一次改动。
|
|
10921
|
+
*/
|
|
10922
|
+
interface WireBlockedGoalsResponse {
|
|
10923
|
+
/** **最久没动的在前**(`updatedAt` 升序)。一段都没卡住时空数组 */
|
|
10924
|
+
items: readonly WireBlockedGoal[];
|
|
10925
|
+
}
|
|
10926
|
+
declare const WIRE_BLOCKED_GOALS_FIELDS: WireFields<WireBlockedGoalsResponse>;
|
|
10927
|
+
|
|
10928
|
+
/**
|
|
10929
|
+
* `@` 补全面板要列什么 —— 两条候选端点的载荷。
|
|
10930
|
+
*
|
|
10931
|
+
* | 载荷 | 端点 | 落地 |
|
|
10932
|
+
* | --------------------------------- | ---------------------------------------- | ---------- |
|
|
10933
|
+
* | {@link WireSessionCandidate} | `GET /api/sessions/:id/reference-candidates` | 方案 53 PR-4 |
|
|
10934
|
+
* | {@link WireFileCandidate} | `GET /api/sessions/:id/file-candidates` | 方案 63 |
|
|
10935
|
+
*
|
|
10936
|
+
* 两份**不合并成一个「候选」形状**,判据同下面那节(一份清单 vs 一次动作的收据)
|
|
10937
|
+
* 的同一条道理:会话候选有六个字段要印(标题 / 时间 / 条数 / 在哪个工作区),
|
|
10938
|
+
* 文件候选只有一个路径,而合成一个联合之后每个消费方都得先分流一次才能画一行。
|
|
10939
|
+
* 面板那一侧本来就是**一次只开一组**(`@:` 是会话、`@` 后面不是 `:` 就是文件,
|
|
10940
|
+
* 前缀互斥 —— 判据在 `web/src/mention/trigger.ts` 上)。
|
|
10941
|
+
*
|
|
10942
|
+
* ## 这一份和「上一条消息引用到了什么」是两个形状
|
|
10943
|
+
*
|
|
10944
|
+
* 后者是 [wire-rest.ts](./wire-rest.js) 的 `WireSessionReference`(挂在
|
|
10945
|
+
* `POST /messages` 的 202 上),**刻意不合并**:这一份是一份清单(可以为空,
|
|
10946
|
+
* 空就是面板不画);那一份是一次动作的收据,带着那五个稳定错误码。
|
|
10947
|
+
* 合成一个形状的话,「候选里那一行为什么带着一个 `omitted`」没人答得上来。
|
|
10948
|
+
*
|
|
10949
|
+
* 两份住在两个文件里还有一条更硬的理由:`wireFields` 住在 `wire-rest.ts`,
|
|
10950
|
+
* 而那一份的消费方就在它自己文件里 —— 反过来让 `wire-rest.ts` import 这一份
|
|
10951
|
+
* 会造一个环,而 `import/no-cycle` 在 oxlint 里是 error(判据在
|
|
10952
|
+
* `server/src/context.ts` 的文件头)。
|
|
10953
|
+
*
|
|
10954
|
+
* ## ⚠️ 这份形状里**没有一个字的转录文本**,而且不许加(方案 53 §5.2)
|
|
10955
|
+
*
|
|
10956
|
+
* 判据在 core 的 `SessionCandidate` 上,逐字照搬到这儿:补全面板是**边打边显示**
|
|
10957
|
+
* 的,它一旦带上转录文本,打 `@:密码` 就会在屏幕上列出所有提到过密码的会话及其
|
|
10958
|
+
* 片段 —— 而当前这个会话可能正在被别人看着(结对编程、录屏、演示)。
|
|
10959
|
+
*
|
|
10960
|
+
* `sessions` 表上那个 `preview` 列(首条消息的前几十字)**同样不在这里**:
|
|
10961
|
+
* 它是转录文本的投影,加进来等于把那条边界从「面板不搜正文」偷偷降级成
|
|
10962
|
+
* 「面板不搜正文但会把正文显示出来」。
|
|
10963
|
+
*
|
|
10964
|
+
* ⚠️ **过滤也不在浏览器里做。** `?q=` 原样递给 core 的
|
|
10965
|
+
* `listSessionCandidates`,那条 SQL 只碰 id / cwd / 标题,而且带着可读性判定的
|
|
10966
|
+
* WHERE(方案 48 那一处)。把「拉一池子回来再在前端筛」当成省一次请求的优化,
|
|
10967
|
+
* 代价是那一池子之外的会话**永远搜不到,而且屏幕上没有任何提示**
|
|
10968
|
+
* —— TUI 那一侧今天就挂着这笔账(`SESSION_CANDIDATE_POOL`,40 条封顶)。
|
|
10969
|
+
*/
|
|
10970
|
+
/**
|
|
10971
|
+
* 补全面板里的一条会话候选。
|
|
10972
|
+
*
|
|
10973
|
+
* **是 core 的 `SessionCandidate` 的结构镜像**(`packages/web` 只依赖
|
|
10974
|
+
* protocol + view,够不着 core),字段逐个对应,一个不多。
|
|
10975
|
+
*/
|
|
10976
|
+
interface WireSessionCandidate {
|
|
10977
|
+
sessionId: string;
|
|
10978
|
+
/** 会话标题。**空标题在引擎侧已经兜了 sessionId**(方案 53 §5.3),所以永远非空 */
|
|
10979
|
+
title: string;
|
|
8386
10980
|
/**
|
|
8387
|
-
*
|
|
10981
|
+
* 那段会话的工作目录。
|
|
8388
10982
|
*
|
|
8389
|
-
*
|
|
8390
|
-
*
|
|
10983
|
+
* 递下来是因为**它参与匹配**(§5.2 允许按 cwd 过滤),而不是为了显示一条绝对
|
|
10984
|
+
* 路径 —— 面板上只印它的最后一段(同 TUI 那一侧的 `toCandidateView`)。
|
|
8391
10985
|
*/
|
|
8392
|
-
|
|
10986
|
+
cwd?: string;
|
|
10987
|
+
/** 最后活动时间(epoch 毫秒)。面板上那句「几天前」用它 */
|
|
10988
|
+
updatedAt: number;
|
|
10989
|
+
/** 一共多少条消息(含被压掉的) */
|
|
10990
|
+
messageCount: number;
|
|
10991
|
+
/** 是不是当前工作区的会话。排序靠它,面板上也要标出来 */
|
|
10992
|
+
sameWorkspace: boolean;
|
|
10993
|
+
}
|
|
10994
|
+
declare const WIRE_SESSION_CANDIDATE_FIELDS: WireFields<WireSessionCandidate>;
|
|
10995
|
+
/**
|
|
10996
|
+
* `GET /api/sessions/:id/reference-candidates` 的响应。
|
|
10997
|
+
*
|
|
10998
|
+
* 包一层而不是裸数组,同这一层别的清单端点:将来加游标不会把响应形状整个换掉。
|
|
10999
|
+
*
|
|
11000
|
+
* **一段都没有时是空数组,不是 404 也不是错误** —— 一台只跑过一段会话的机器上
|
|
11001
|
+
* 这就是正常状态(当前这一段自己不在候选里,见 core 的 `listSessionCandidates`),
|
|
11002
|
+
* 而界面据此整个不画那个面板(决定 20 ①)。
|
|
11003
|
+
*/
|
|
11004
|
+
interface WireSessionCandidatesResponse {
|
|
11005
|
+
candidates: readonly WireSessionCandidate[];
|
|
11006
|
+
}
|
|
11007
|
+
declare const WIRE_SESSION_CANDIDATES_FIELDS: WireFields<WireSessionCandidatesResponse>;
|
|
11008
|
+
/**
|
|
11009
|
+
* 补全面板里的一条文件候选。
|
|
11010
|
+
*
|
|
11011
|
+
* **只有路径**,而这一格是刻意窄的:面板那一行要印的两截(文件名 + 它上面那层
|
|
11012
|
+
* 目录)都是从这一个字符串切出来的,让服务端多发一个 `name` 等于给同一件事
|
|
11013
|
+
* 两个说法。体积也不是小事 —— 一次响应几十条,每条多一个字段就是多一份重复。
|
|
11014
|
+
*
|
|
11015
|
+
* ⚠️ **这里没有 `bytes`,而且别加。** 加它意味着服务端要对每条候选 `stat()` 一次
|
|
11016
|
+
* ——补全面板是**边打边刷**的,那就是每按一个键几十次系统调用,换来的只是一行
|
|
11017
|
+
* 灰字。真要那个数,它已经在附上之后那张卡上了(`EpochFilePart.bytes`,
|
|
11018
|
+
* 那一刻只 stat 用户真的选中的那一个)。
|
|
11019
|
+
*/
|
|
11020
|
+
interface WireFileCandidate {
|
|
11021
|
+
/** 相对工作区根的路径,**一律 `/` 分隔**(判据同 core 的 `WorkspaceFiles.files`) */
|
|
11022
|
+
path: string;
|
|
11023
|
+
}
|
|
11024
|
+
declare const WIRE_FILE_CANDIDATE_FIELDS: WireFields<WireFileCandidate>;
|
|
11025
|
+
/**
|
|
11026
|
+
* `GET /api/sessions/:id/file-candidates` 的响应。
|
|
11027
|
+
*
|
|
11028
|
+
* ## 后面那两个数是**沉默截断的解药**,不是诊断字段
|
|
11029
|
+
*
|
|
11030
|
+
* 这条路上有两处「面板本该列出来、而它没有」的地方,两处都会让用户以为
|
|
11031
|
+
* 「这个仓库里没有这个文件」:
|
|
11032
|
+
*
|
|
11033
|
+
* - {@link unmentionable} —— 匹配上了,但 `@` 的抽取规则表达不出这个路径
|
|
11034
|
+
* (判据全文在 `isMentionableFilePath` 上);
|
|
11035
|
+
* - {@link truncated} —— 工作区文件清单本身撞了两万条的上限(core 的 `MAX_FILES`),
|
|
11036
|
+
* 于是清单之外的文件**压根没参与匹配**。
|
|
11037
|
+
*
|
|
11038
|
+
* 两个数都直接印在面板上。少印一个,那一档就退化成 TUI 那侧 40 条池子的老病:
|
|
11039
|
+
* 「永远搜不到,而且屏幕上没有任何提示」(那笔账记在方案 53 验收记录第六章)。
|
|
11040
|
+
*
|
|
11041
|
+
* ⚠️ **`candidates` 为空不是错误**:一个刚 `git init` 的空目录上这就是正常状态,
|
|
11042
|
+
* 而界面据此整个不画那个面板(决定 20 ①)。
|
|
11043
|
+
*/
|
|
11044
|
+
interface WireFileCandidatesResponse {
|
|
11045
|
+
candidates: readonly WireFileCandidate[];
|
|
11046
|
+
/**
|
|
11047
|
+
* 匹配上了、但 `@` 表达不出来因而**没进上面那张清单**的条数。
|
|
11048
|
+
*
|
|
11049
|
+
* 0 是常态。非 0 时面板要多印一行灰字 —— 见上面那节。
|
|
11050
|
+
*/
|
|
11051
|
+
unmentionable: number;
|
|
11052
|
+
/** 工作区文件清单撞了上限,清单之外的文件没参与匹配 */
|
|
11053
|
+
truncated: boolean;
|
|
8393
11054
|
}
|
|
11055
|
+
declare const WIRE_FILE_CANDIDATES_FIELDS: WireFields<WireFileCandidatesResponse>;
|
|
8394
11056
|
|
|
8395
11057
|
/**
|
|
8396
11058
|
* 凭据存储契约。
|
|
@@ -8485,4 +11147,4 @@ declare const SECRET_SERVICE = "epoch-agent";
|
|
|
8485
11147
|
*/
|
|
8486
11148
|
declare const MCP_DATA_KEY_NAME = "mcp-auth-data-key";
|
|
8487
11149
|
|
|
8488
|
-
export { ACTION_CONTEXTS, AGENT_ROLE_NAME_PATTERN, API_KEY_ENV_VARS, APPROVAL_OUTCOMES, type ActiveAgentRole, type AgentEvent, type AgentEventOf, type AgentEventType, type AgentRole, type AgentRoleScope, type AgentRoleSource, type ApprovalAnswer, type ApprovalOutcome, type ApprovalReply, type ApprovalRequest, type ArtifactKind, type AttributeValue, type Attributes, type BackgroundTaskInfo, type BackgroundTaskStatus, type BudgetBreach, COMMAND_NAME_PATTERN, type ContextBreakdown, type ContextSegment, type CustomCommandDef, type CustomCommandSource, DEFAULT_AGENT_ROLE, DEFAULT_LANG, type DecidedTrustLevel, type Diagnostic, DiagnosticSink, type DiagnosticStatus, type EpochConfig, type EpochContentPart, type EpochFilePart, type EpochHook, type EpochImagePart, type EpochMessage, type EpochMessageRole, type EpochPlugin, type EpochSkill, type EpochTextPart, type EpochTool, type EpochToolCall, type EpochToolResult, type EpochTurnFacts, type EpochUserContent, type ExpandedCommand, type FileOmitReason, type FinishReason, GEN_AI, HEADLESS_INPUT_EVENT_TYPES, HEADLESS_INPUT_FORMATS, HEADLESS_OUTPUT_FORMATS, HEADLESS_PROTOCOL_VERSION, type HeadlessEnvelope, type HeadlessEvent, type HeadlessInit, type HeadlessInputEvent, type HeadlessInputEventType, type HeadlessInputFormat, type HeadlessLifecycleEvent, type HeadlessOutputFormat, type HeadlessResult, type HeadlessResultReason, type HookContext, type HookEvent, type HookHandler, type HookManager, type HookRegistration, type HookType, type JsonSchema, KEY_ACTIONS, type KeyAction, type KeyChord, type KeyContext, type KeybindingTable, LANGS, type Lang, type LocalizedDetail, type LogFn, MAX_HEADER_CHARS, MAX_OPTIONS, MAX_QUESTIONS, MCP_DATA_KEY_NAME, METRIC, MIN_OPTIONS, type ModelRef, type ModelSelection, type ModelSelectionOrigin, NOOP_TELEMETRY, OPERATION_TYPES, type Operation, type OperationType, PERMISSION_LEVELS, PLAN_OUTCOMES, PLAN_OUTCOME_LABELS, PROVIDER_INFOS, PROVIDER_TYPES, type PermissionDecision, type PermissionLevel, type PermissionManager, type PermissionRuleLists, type PlanApprovalOutcome, type PlanProposal, type PluginContext, type ProviderInfo, type ProviderType, type QuestionAnswer, type QuestionItem, type QuestionOption, type QuestionRequest, RESERVED_CHORDS, RESERVED_COMMAND_NAMES, type RuleValue, SCHEDULE_INTERVAL_MINUTES, SCHEDULE_RUN_STATUSES, SEARCH_API_KEY_ENV, SEARCH_PROVIDER_TYPES, SECRET_SERVICE, SHELL_KINDS, SIMULTANEOUS_CONTEXTS, SYSTEM_NOTE_PREFIX, type ScheduleAllowlist, type ScheduleBackendKind, type ScheduleCycle, type ScheduleDefinition, type SchedulePendingApproval, type ScheduleRun, type ScheduleRunStatus, type ScheduleRunnerSpec, type ScheduleTrigger, type ScheduleTriggerKind, type ScheduleWeekday, type SearchProviderType, type SecretBackendId, type SecretStore, type SelectionContext, type SelectionRejection, type SelectionWarning, type SetSelectionResult, type ShellKind, type SpanLike, type Telemetry, type TokenUsage, type ToolAnnotations, type ToolArtifact, type ToolArtifactInput, type ToolContext, type ToolDefinition, ToolExposure, type ToolProvider, type ToolResult, type TrustLevel, type TrustManager, type TrustRecord, type TrustScope, type VerifyResult, WIRE_ABORT_FIELDS, WIRE_ABOUT_FIELDS, WIRE_AGENT_ROLE_FIELDS, WIRE_APPROVAL_LIST_FIELDS, WIRE_APPROVAL_ROW_FIELDS, WIRE_ARTIFACTS_FIELDS, WIRE_AUDIT_LOG_FIELDS, WIRE_AUDIT_ROW_FIELDS, WIRE_AUTH_COOKIE, WIRE_AUTH_COOKIE_ATTRS, WIRE_AUTH_TOKEN_PARAM, WIRE_BACKGROUND_TASK_FIELDS, WIRE_BLOCKED_LEVEL_FIELDS, WIRE_BUILTIN_CONNECTOR_FIELDS, WIRE_CAPABILITIES_FIELDS, WIRE_CAPABILITY_PROJECT_FIELDS, WIRE_CHECKPOINT_LIST_FIELDS, WIRE_CHECKPOINT_SUMMARY_FIELDS, WIRE_COMMANDS_FIELDS, WIRE_COMMAND_EXPANSION_FIELDS, WIRE_COMPRESSION_LINE_FIELDS, WIRE_CONFIG_FIELDS, WIRE_CREATE_SESSION_FIELDS, WIRE_CREATE_WORKSPACE_FIELDS, WIRE_CREATE_WORKSPACE_RESPONSE_FIELDS, WIRE_CUSTOM_COMMAND_FIELDS, WIRE_DELETE_SESSION_FIELDS, WIRE_DIFF_FILE_FIELDS, WIRE_ERROR_FIELDS, WIRE_FILE_CHANGE_FIELDS, WIRE_HEALTH_FIELDS, WIRE_KNOWN_WORKSPACE_FIELDS, WIRE_LANG_PARAM, WIRE_MANAGED_LOCK_FIELDS, WIRE_MCP_ADD_FIELDS, WIRE_MCP_ADD_RESPONSE_FIELDS, WIRE_MCP_APPLY_ENTRY_FIELDS, WIRE_MCP_APPLY_RESPONSE_FIELDS, WIRE_MCP_CONFIG_APPLY_FIELDS, WIRE_MCP_CONFIG_FIELDS, WIRE_MCP_CONFIG_ISSUE_FIELDS, WIRE_MCP_CONFIG_SAVE_FIELDS, WIRE_MCP_CONFIG_SAVE_RESPONSE_FIELDS, WIRE_MCP_CONNECTOR_FIELDS, WIRE_MCP_RECONNECT_FIELDS, WIRE_MESSAGE_LIST_FIELDS, WIRE_MODEL_FIELDS, WIRE_MODEL_SELECTION_FIELDS, WIRE_MODEL_SET_FIELDS, WIRE_MODEL_SUGGESTIONS_FIELDS, WIRE_PATCH_SESSION_FIELDS, WIRE_PATCH_SESSION_RESPONSE_FIELDS, WIRE_PERMISSION_RULE_FIELDS, WIRE_PERMISSION_SET_REQUEST_FIELDS, WIRE_PERMISSION_SET_RESPONSE_FIELDS, WIRE_PERMISSION_SHADOW_FIELDS, WIRE_PERMISSION_STATE_FIELDS, WIRE_PERMISSION_STATUS_FIELDS, WIRE_PLAN_ACTIONS, WIRE_PLAN_FIELDS, WIRE_PLAN_MODE_REQUEST_FIELDS, WIRE_PLAN_MODE_RESPONSE_FIELDS, WIRE_PLAN_MODE_STATE_FIELDS, WIRE_POLICY_DIR_FIELDS, WIRE_POLICY_STATUS_FIELDS, WIRE_PROVIDERS_FIELDS, WIRE_PROVIDER_MODELS_FIELDS, WIRE_PROVIDER_OPTION_FIELDS, WIRE_QUESTION_LIST_FIELDS, WIRE_QUESTION_ROW_FIELDS, WIRE_RESPOND_APPROVAL_FIELDS, WIRE_RESPOND_APPROVAL_RESPONSE_FIELDS, WIRE_RESPOND_QUESTION_FIELDS, WIRE_RESPOND_QUESTION_RESPONSE_FIELDS, WIRE_REWIND_FILE_PLAN_FIELDS, WIRE_REWIND_OUTCOME_FIELDS, WIRE_REWIND_PREVIEW_FIELDS, WIRE_REWIND_REQUEST_FIELDS, WIRE_REWIND_RESPONSE_FIELDS, WIRE_REWIND_SCOPES, WIRE_ROLE_ADD_FIELDS, WIRE_ROLE_ADD_RESPONSE_FIELDS, WIRE_SANDBOX_STATUS_FIELDS, WIRE_SCHEDULE_CAPABILITY_FIELDS, WIRE_SCHEDULE_CREATE_FIELDS, WIRE_SCHEDULE_DEFAULTS_FIELDS, WIRE_SCHEDULE_DELETE_FIELDS, WIRE_SCHEDULE_FIELDS, WIRE_SCHEDULE_FIX_FIELDS, WIRE_SCHEDULE_FIX_REQUEST_FIELDS, WIRE_SCHEDULE_ISSUE_CODES, WIRE_SCHEDULE_LIST_FIELDS, WIRE_SCHEDULE_RECORDING_FIELDS, WIRE_SCHEDULE_ROW_FIELDS, WIRE_SCHEDULE_RUNS_FIELDS, WIRE_SCHEDULE_RUN_FIELDS, WIRE_SCHEDULE_SAVE_FIELDS, WIRE_SECURITY_FIELDS, WIRE_SECURITY_WORKSPACE_FIELDS, WIRE_SEND_MESSAGE_FIELDS, WIRE_SEND_MESSAGE_RESPONSE_FIELDS, WIRE_SESSION_FINISH_FIELDS, WIRE_SESSION_LIST_FIELDS, WIRE_SESSION_RESPONSE_FIELDS, WIRE_SESSION_SUMMARY_FIELDS, WIRE_SESSION_WORKSPACE_FIELDS, WIRE_SESSION_WORKSPACE_RESPONSE_FIELDS, WIRE_SETTINGS_FIELDS, WIRE_SETTINGS_WRITE_FIELDS, WIRE_SETTINGS_WRITE_REQUEST_FIELDS, WIRE_SETTING_ROW_FIELDS, WIRE_SETTING_STEP_FIELDS, WIRE_SETTING_WRITE_FIELDS, WIRE_SETTING_WRITE_LAYERS, WIRE_SKILL_BODY_FIELDS, WIRE_SKILL_FIELDS, WIRE_SKILL_IMPORT_FIELDS, WIRE_SKILL_IMPORT_PREVIEW_FIELDS, WIRE_TASKS_FIELDS, WIRE_TOOLS_FIELDS, WIRE_TOOL_GATE_FIELDS, WIRE_TOOL_ROW_FIELDS, WIRE_WORKSPACE_DIFF_FIELDS, WIRE_WORKSPACE_DIRS_FIELDS, WIRE_WORKSPACE_DIR_ENTRY_FIELDS, WIRE_WORKSPACE_REF_FIELDS, type WireAbortResponse, type WireAbout, type WireAgentEnvelope, type WireAgentEvent, type WireAgentRoleSummary, type WireApprovalListResponse, type WireApprovalRequest, type WireArtifactsResponse, type WireAuditCode, type WireAuditLog, type WireAuditOutcome, type WireAuditRow, type WireAuthDenyReason, type WireAuthDenyStatus, type WireAuthGuard, type WireAuthRequestView, type WireAuthVerdict, type WireBackgroundTask, type WireBindWorkspaceRequest, type WireBlockedLevel, type WireBuiltinConnector, type WireCapabilitiesResponse, type WireCapabilityProject, type WireCheckpointListResponse, type WireCheckpointSummary, type WireCommandExpansion, type WireCommandsResponse, type WireCompressionLine, type WireConfigResponse, type WireCreateSessionRequest, type WireCreateSessionResponse, type WireCreateWorkspaceRequest, type WireCreateWorkspaceResponse, type WireCustomCommand, type WireDeleteSessionResponse, type WireDiffFile, type WireDiffNote, type WireDiffOmitReason, type WireDiffSource, type WireDiffStatus, type WireEnvelope, type WireErrorResponse, type WireEvent, type WireFields, type WireFileChange, type WireFixture, type WireFixtureCase, type WireHealthResponse, type WireHubEnvelope, type WireHubEvent, type WireKnownWorkspace, type WireManagedLock, type WireMcpAddRequest, type WireMcpAddResponse, type WireMcpApplyAction, type WireMcpApplyEntry, type WireMcpApplyResponse, type WireMcpConfigApplyRequest, type WireMcpConfigIssue, type WireMcpConfigResponse, type WireMcpConfigSaveRequest, type WireMcpConfigSaveResponse, type WireMcpConnector, type WireMcpReconnectResponse, type WireMessageListResponse, type WireModelCaveat, type WireModelProbeStatus, type WireModelRejection, type WireModelResponse, type WireModelSelection, type WireModelSetRequest, type WireModelSetResponse, type WireModelSource, type WireModelSuggestions, type WirePatchSessionRequest, type WirePatchSessionResponse, type WirePermissionRuleRow, type WirePermissionSetRequest, type WirePermissionSetResponse, type WirePermissionShadow, type WirePermissionState, type WirePermissionStatus, type WirePlanAction, type WirePlanModeRequest, type WirePlanModeResponse, type WirePlanModeState, type WirePlanResponse, type WirePolicyDir, type WirePolicyStatus, type WireProviderModelsRequest, type WireProviderModelsResponse, type WireProviderOption, type WireProviderSummary, type WireProvidersResponse, type WireQuestionListResponse, type WireQuestionRequest, type WireReplayMessage, type WireResetReason, type WireRespondApprovalRequest, type WireRespondApprovalResponse, type WireRespondQuestionRequest, type WireRespondQuestionResponse, type WireRewindAction, type WireRewindDrift, type WireRewindFilePlan, type WireRewindOutcome, type WireRewindPreview, type WireRewindRequest, type WireRewindResponse, type WireRewindScope, type WireRoleAddRequest, type WireRoleAddResponse, type WireSandboxReason, type WireSandboxStatus, type WireScheduleCapability, type WireScheduleCreateRequest, type WireScheduleDefaults, type WireScheduleDeleteResponse, type WireScheduleFixRequest, type WireScheduleFixResponse, type WireScheduleIssue, type WireScheduleIssueCode, type WireScheduleListResponse, type WireScheduleRecordingResponse, type WireScheduleRegisterRefusal, type WireScheduleRegisterWarning, type WireScheduleRegistration, type WireScheduleResponse, type WireScheduleRow, type WireScheduleRunResponse, type WireScheduleRunsResponse, type WireScheduleSaveResponse, type WireScheduleUpdateRequest, type WireSecurityResponse, type WireSecurityWorkspace, type WireSendMessageRequest, type WireSendMessageResponse, type WireSessionFinish, type WireSessionListResponse, type WireSessionResponse, type WireSessionSummary, type WireSessionWorkspace, type WireSessionWorkspaceResponse, type WireSettingApply, type WireSettingFutile, type WireSettingLayer, type WireSettingRow, type WireSettingStep, type WireSettingValue, type WireSettingWrite, type WireSettingWriteLayer, type WireSettingsResponse, type WireSettingsWriteRequest, type WireSettingsWriteResponse, type WireSkillBodyResponse, type WireSkillImportConflict, type WireSkillImportEntry, type WireSkillImportFailure, type WireSkillImportForm, type WireSkillImportIssue, type WireSkillImportPreviewResponse, type WireSkillImportResponse, type WireSkillSummary, type WireSummaryWorkspace, type WireTaggedEvent, type WireTasksResponse, type WireToolGate, type WireToolGateAxis, type WireToolRow, type WireToolSummary, type WireToolVerdict, type WireToolsResponse, type WireTurnState, type WireWorkspaceBinding, type WireWorkspaceDiffResponse, type WireWorkspaceDirEntry, type WireWorkspaceDirsResponse, type WireWorkspaceRef, apiKeyEnvVar, chordId, compressionThresholdTokens, conformsToWire, contentToText, diagnosticToLine, extractMentions, filePartSummary, getProviderInfo, hasFailure, isApprovalOutcome, isContextNearlyFull, isHeadlessInputFormat, isHeadlessLifecycleEvent, isHeadlessOutputFormat, isKeyAction, isLang, isOperationType, isPermissionLevel, isPlanOutcome, isProviderType, isQuestionAnswerMap, isSearchProviderType, isValidAgentRoleName, isValidCommandName, isWireHubEvent, isWirePlanAction, isWireRewindScope, isWireSettingWriteLayer, normalizeApproval, parseChord, parseModelRef, promptTokens, stripSystemNote, systemNote, totalUsageTokens, wireFields };
|
|
11150
|
+
export { ACTION_CONTEXTS, AGENT_ROLE_NAME_PATTERN, API_KEY_ENV_VARS, APPROVAL_OUTCOMES, type ActiveAgentRole, type AgentEvent, type AgentEventOf, type AgentEventType, type AgentRole, type AgentRoleScope, type AgentRoleSource, type ApprovalAnswer, type ApprovalOutcome, type ApprovalReply, type ApprovalRequest, type ArtifactKind, type AttributeValue, type Attributes, type BackgroundTaskInfo, type BackgroundTaskKind, type BackgroundTaskStatus, type BoundKey, type BudgetBreach, COMMAND_NAME_PATTERN, type Catalogs, type ContextBreakdown, type ContextSegment, type CustomCommandDef, type CustomCommandSource, DEFAULT_AGENT_ROLE, DEFAULT_LANG, type DecidedTrustLevel, type Diagnostic, DiagnosticSink, type DiagnosticStatus, type EpochConfig, type EpochContentPart, type EpochFilePart, type EpochHook, type EpochImagePart, type EpochMessage, type EpochMessageRole, type EpochPlugin, type EpochSessionPart, type EpochSkill, type EpochTextPart, type EpochTool, type EpochToolCall, type EpochToolResult, type EpochTurnFacts, type EpochUserContent, type ExpandedCommand, type FileOmitReason, type FinishReason, type FixableApproval, type FlatCatalog, GEN_AI, HEADLESS_INPUT_EVENT_TYPES, HEADLESS_INPUT_FORMATS, HEADLESS_OUTPUT_FORMATS, HEADLESS_PROTOCOL_VERSION, type HeadlessEnvelope, type HeadlessEvent, type HeadlessInit, type HeadlessInputEvent, type HeadlessInputEventType, type HeadlessInputFormat, type HeadlessLifecycleEvent, type HeadlessOutputFormat, type HeadlessResult, type HeadlessResultReason, type HookContext, type HookEvent, type HookHandler, type HookManager, type HookRegistration, type HookType, type JsonSchema, KEY_ACTIONS, type KeyAction, type KeyChord, type KeyContext, type KeySequence, type KeybindingReport, type KeybindingSource, type KeybindingTable, LANGS, type Lang, type LocalizedDetail, type LogFn, MAX_ATTACH_BYTES, MAX_ATTACH_FILES, MAX_ATTACH_TOTAL_BYTES, MAX_HEADER_CHARS, MAX_OPTIONS, MAX_QUESTIONS, MAX_SESSION_ATTACH_BYTES, MAX_SESSION_REFS, MCP_DATA_KEY_NAME, METRIC, MIN_OPTIONS, type MentionSet, type ModelRef, type ModelSelection, type ModelSelectionOrigin, NOOP_TELEMETRY, type NoticeCode, OPERATION_TYPES, type Operation, type OperationType, PERMISSION_LEVELS, PLAN_OUTCOMES, PROVIDER_INFOS, PROVIDER_TYPES, type PermissionDecision, type PermissionLevel, type PermissionManager, type PermissionRuleLists, type PlanApprovalOutcome, type PlanProposal, type PluginContext, type ProviderInfo, type ProviderType, type QuestionAnswer, type QuestionItem, type QuestionOption, type QuestionRequest, RESERVED_CHORDS, RESERVED_COMMAND_NAMES, type RejectReason, type RejectedKey, type RuleValue, SCHEDULE_INTERVAL_MINUTES, SCHEDULE_RUN_STATUSES, SCHEDULE_TEMPLATES, SEARCH_API_KEY_ENV, SEARCH_PROVIDER_TYPES, SECRET_SERVICE, SEQUENCE_CONTEXTS, SESSION_MENTION_PREFIX, SESSION_REFERENCE_ERROR_CODES, SHELL_KINDS, SIMULTANEOUS_CONTEXTS, SYSTEM_NOTE_PREFIX, type ScheduleAllowlist, type ScheduleBackendKind, type ScheduleCycle, type ScheduleDefinition, type SchedulePendingApproval, type SchedulePendingItem, type ScheduleRun, type ScheduleRunStatus, type ScheduleRunnerSpec, type ScheduleTemplate, type ScheduleTemplateId, type ScheduleTrigger, type ScheduleTriggerKind, type ScheduleWeekday, type SearchProviderType, type SecretBackendId, type SecretStore, type SelectionContext, type SelectionRejection, type SelectionWarning, type SessionAttachKind, type SessionAttachResult, type SessionOmitReason, type SessionSurfaceMessage, type SessionSurfaceView, type SetSelectionResult, type ShellKind, type SpanLike, type SubToolCall, type SubToolCallResult, type Telemetry, type TokenUsage, type ToolAnnotations, type ToolArtifact, type ToolArtifactInput, type ToolContext, type ToolDefinition, ToolExposure, type ToolProvider, type ToolResult, type Translate, type TranslateVars, type TrustLevel, type TrustManager, type TrustRecord, type TrustScope, type VerifyResult, WIRE_ABORT_FIELDS, WIRE_ABOUT_FIELDS, WIRE_AGENT_ROLE_FIELDS, WIRE_APPROVAL_CACHE_REVOKE_REQUEST_FIELDS, WIRE_APPROVAL_CACHE_REVOKE_RESPONSE_FIELDS, WIRE_APPROVAL_LIST_FIELDS, WIRE_APPROVAL_ROW_FIELDS, WIRE_ARTIFACTS_FIELDS, WIRE_AUDIT_LOG_FIELDS, WIRE_AUDIT_ROW_FIELDS, WIRE_AUTH_COOKIE, WIRE_AUTH_COOKIE_ATTRS, WIRE_AUTH_TOKEN_PARAM, WIRE_BACKGROUND_TASK_FIELDS, WIRE_BLOCKED_GOALS_FIELDS, WIRE_BLOCKED_GOAL_FIELDS, WIRE_BLOCKED_LEVEL_FIELDS, WIRE_BUILTIN_CONNECTOR_FIELDS, WIRE_CACHED_APPROVAL_FIELDS, WIRE_CAPABILITIES_FIELDS, WIRE_CAPABILITY_PROJECT_FIELDS, WIRE_CHECKPOINT_LIST_FIELDS, WIRE_CHECKPOINT_SUMMARY_FIELDS, WIRE_COMMANDS_FIELDS, WIRE_COMMAND_EXPANSION_FIELDS, WIRE_COMPACT_FIELDS, WIRE_COMPRESSION_LINE_FIELDS, WIRE_CONFIG_FIELDS, WIRE_CONTEXT_FIELDS, WIRE_CREATE_SESSION_FIELDS, WIRE_CREATE_WORKSPACE_FIELDS, WIRE_CREATE_WORKSPACE_RESPONSE_FIELDS, WIRE_CUSTOM_COMMAND_FIELDS, WIRE_DELETE_SESSION_FIELDS, WIRE_DIFF_FILE_FIELDS, WIRE_ERROR_FIELDS, WIRE_FILE_CANDIDATES_FIELDS, WIRE_FILE_CANDIDATE_FIELDS, WIRE_FILE_CHANGE_FIELDS, WIRE_FILE_REFERENCE_FIELDS, WIRE_GOAL_ACTIONS, WIRE_GOAL_BLOCK_FIELDS, WIRE_GOAL_CLEAR_FIELDS, WIRE_GOAL_CREATE_FIELDS, WIRE_GOAL_FIELDS, WIRE_GOAL_LIMITS_FIELDS, WIRE_GOAL_PHASES, WIRE_GOAL_REFUSALS, WIRE_GOAL_RESPONSE_FIELDS, WIRE_GOAL_WRITE_FIELDS, WIRE_HEALTH_FIELDS, WIRE_KNOWN_WORKSPACE_FIELDS, WIRE_LANG_PARAM, WIRE_MANAGED_LOCK_FIELDS, WIRE_MCP_ADD_FIELDS, WIRE_MCP_ADD_RESPONSE_FIELDS, WIRE_MCP_APPLY_ENTRY_FIELDS, WIRE_MCP_APPLY_RESPONSE_FIELDS, WIRE_MCP_CONFIG_APPLY_FIELDS, WIRE_MCP_CONFIG_FIELDS, WIRE_MCP_CONFIG_ISSUE_FIELDS, WIRE_MCP_CONFIG_SAVE_FIELDS, WIRE_MCP_CONFIG_SAVE_RESPONSE_FIELDS, WIRE_MCP_CONNECTOR_FIELDS, WIRE_MCP_RECONNECT_FIELDS, WIRE_MESSAGE_LIST_FIELDS, WIRE_MODEL_FIELDS, WIRE_MODEL_SELECTION_FIELDS, WIRE_MODEL_SET_FIELDS, WIRE_MODEL_SUGGESTIONS_FIELDS, WIRE_PATCH_SESSION_FIELDS, WIRE_PATCH_SESSION_RESPONSE_FIELDS, WIRE_PERMISSION_RULE_FIELDS, WIRE_PERMISSION_SET_REQUEST_FIELDS, WIRE_PERMISSION_SET_RESPONSE_FIELDS, WIRE_PERMISSION_SHADOW_FIELDS, WIRE_PERMISSION_STATE_FIELDS, WIRE_PERMISSION_STATUS_FIELDS, WIRE_PLAN_ACTIONS, WIRE_PLAN_FIELDS, WIRE_PLAN_MODE_REQUEST_FIELDS, WIRE_PLAN_MODE_RESPONSE_FIELDS, WIRE_PLAN_MODE_STATE_FIELDS, WIRE_PLUGINS_FIELDS, WIRE_PLUGIN_ACTION_FIELDS, WIRE_PLUGIN_INSTALL_FIELDS, WIRE_PLUGIN_NAME_FIELDS, WIRE_PLUGIN_PREVIEW_FIELDS, WIRE_PLUGIN_PREVIEW_RESPONSE_FIELDS, WIRE_PLUGIN_UPDATE_FIELDS, WIRE_POLICY_DIR_FIELDS, WIRE_POLICY_STATUS_FIELDS, WIRE_PROVIDERS_FIELDS, WIRE_PROVIDER_MODELS_FIELDS, WIRE_PROVIDER_OPTION_FIELDS, WIRE_QUESTION_LIST_FIELDS, WIRE_QUESTION_ROW_FIELDS, WIRE_RESPOND_APPROVAL_FIELDS, WIRE_RESPOND_APPROVAL_RESPONSE_FIELDS, WIRE_RESPOND_QUESTION_FIELDS, WIRE_RESPOND_QUESTION_RESPONSE_FIELDS, WIRE_REWIND_FILE_PLAN_FIELDS, WIRE_REWIND_OUTCOME_FIELDS, WIRE_REWIND_PREVIEW_FIELDS, WIRE_REWIND_REQUEST_FIELDS, WIRE_REWIND_RESPONSE_FIELDS, WIRE_REWIND_SCOPES, WIRE_ROLE_ADD_FIELDS, WIRE_ROLE_ADD_RESPONSE_FIELDS, WIRE_SANDBOX_STATUS_FIELDS, WIRE_SCHEDULE_CAPABILITY_FIELDS, WIRE_SCHEDULE_CREATE_FIELDS, WIRE_SCHEDULE_DEFAULTS_FIELDS, WIRE_SCHEDULE_DELETE_FIELDS, WIRE_SCHEDULE_FIELDS, WIRE_SCHEDULE_FIX_FIELDS, WIRE_SCHEDULE_FIX_REQUEST_FIELDS, WIRE_SCHEDULE_ISSUE_CODES, WIRE_SCHEDULE_LIST_FIELDS, WIRE_SCHEDULE_PENDING_FIELDS, WIRE_SCHEDULE_RECORDING_FIELDS, WIRE_SCHEDULE_ROW_FIELDS, WIRE_SCHEDULE_RUNS_FIELDS, WIRE_SCHEDULE_RUN_FIELDS, WIRE_SCHEDULE_SAVE_FIELDS, WIRE_SECURITY_FIELDS, WIRE_SECURITY_WORKSPACE_FIELDS, WIRE_SEND_MESSAGE_FIELDS, WIRE_SEND_MESSAGE_RESPONSE_FIELDS, WIRE_SESSION_CANDIDATES_FIELDS, WIRE_SESSION_CANDIDATE_FIELDS, WIRE_SESSION_FINISH_FIELDS, WIRE_SESSION_LIST_FIELDS, WIRE_SESSION_REFERENCE_FIELDS, WIRE_SESSION_RESPONSE_FIELDS, WIRE_SESSION_SUMMARY_FIELDS, WIRE_SESSION_WORKSPACE_FIELDS, WIRE_SESSION_WORKSPACE_RESPONSE_FIELDS, WIRE_SETTINGS_FIELDS, WIRE_SETTINGS_WRITE_FIELDS, WIRE_SETTINGS_WRITE_REQUEST_FIELDS, WIRE_SETTING_ROW_FIELDS, WIRE_SETTING_STEP_FIELDS, WIRE_SETTING_WRITE_FIELDS, WIRE_SETTING_WRITE_LAYERS, WIRE_SKILL_BODY_FIELDS, WIRE_SKILL_FIELDS, WIRE_SKILL_IMPORT_FIELDS, WIRE_SKILL_IMPORT_PREVIEW_FIELDS, WIRE_TASKS_FIELDS, WIRE_TOOLS_FIELDS, WIRE_TOOL_GATE_FIELDS, WIRE_TOOL_ROW_FIELDS, WIRE_WORKSPACE_DIFF_FIELDS, WIRE_WORKSPACE_DIRS_FIELDS, WIRE_WORKSPACE_DIR_ENTRY_FIELDS, WIRE_WORKSPACE_REF_FIELDS, type WireAbortResponse, type WireAbout, type WireAgentEnvelope, type WireAgentEvent, type WireAgentRoleSummary, type WireApprovalCacheRevokeRequest, type WireApprovalCacheRevokeResponse, type WireApprovalListResponse, type WireApprovalRequest, type WireArtifactsResponse, type WireAuditCode, type WireAuditLog, type WireAuditOutcome, type WireAuditRow, type WireAuthDenyReason, type WireAuthDenyStatus, type WireAuthGuard, type WireAuthRequestView, type WireAuthVerdict, type WireBackgroundTask, type WireBindWorkspaceRequest, type WireBlockedGoal, type WireBlockedGoalsResponse, type WireBlockedLevel, type WireBuiltinConnector, type WireCachedApproval, type WireCachedDecision, type WireCapabilitiesResponse, type WireCapabilityProject, type WireCheckpointListResponse, type WireCheckpointSummary, type WireCommandExpansion, type WireCommandsResponse, type WireCompactRequest, type WireCompactResponse, type WireCompressionLine, type WireConfigResponse, type WireContextResponse, type WireCreateSessionRequest, type WireCreateSessionResponse, type WireCreateWorkspaceRequest, type WireCreateWorkspaceResponse, type WireCustomCommand, type WireDeleteSessionResponse, type WireDiffFile, type WireDiffNote, type WireDiffOmitReason, type WireDiffSource, type WireDiffStatus, type WireEnvelope, type WireErrorResponse, type WireEvent, type WireFields, type WireFileCandidate, type WireFileCandidatesResponse, type WireFileChange, type WireFileReference, type WireFixture, type WireFixtureCase, type WireGoal, type WireGoalBlock, type WireGoalClearResponse, type WireGoalCreateRequest, type WireGoalLimits, type WireGoalPhase, type WireGoalRefusal, type WireGoalResponse, type WireGoalUpdateRequest, type WireGoalWriteResponse, type WireHealthResponse, type WireHubEnvelope, type WireHubEvent, type WireKnownWorkspace, type WireManagedLock, type WireMcpAddRequest, type WireMcpAddResponse, type WireMcpApplyAction, type WireMcpApplyEntry, type WireMcpApplyResponse, type WireMcpConfigApplyRequest, type WireMcpConfigIssue, type WireMcpConfigResponse, type WireMcpConfigSaveRequest, type WireMcpConfigSaveResponse, type WireMcpConnector, type WireMcpReconnectResponse, type WireMessageListResponse, type WireModelCaveat, type WireModelProbeStatus, type WireModelRejection, type WireModelResponse, type WireModelSelection, type WireModelSetRequest, type WireModelSetResponse, type WireModelSource, type WireModelSuggestions, type WirePatchSessionRequest, type WirePatchSessionResponse, type WirePermissionRuleRow, type WirePermissionSetRequest, type WirePermissionSetResponse, type WirePermissionShadow, type WirePermissionState, type WirePermissionStatus, type WirePlanAction, type WirePlanModeRequest, type WirePlanModeResponse, type WirePlanModeState, type WirePlanResponse, type WirePluginActionResponse, type WirePluginCounts, type WirePluginEntry, type WirePluginHit, type WirePluginHookTally, type WirePluginInstallRequest, type WirePluginNameRequest, type WirePluginPreview, type WirePluginPreviewRequest, type WirePluginPreviewResponse, type WirePluginRefuseReason, type WirePluginSourceType, type WirePluginUpdateRequest, type WirePluginsResponse, type WirePolicyDir, type WirePolicyStatus, type WireProviderModelsRequest, type WireProviderModelsResponse, type WireProviderOption, type WireProviderSummary, type WireProvidersResponse, type WireQuestionListResponse, type WireQuestionRequest, type WireReplayMessage, type WireResetReason, type WireRespondApprovalRequest, type WireRespondApprovalResponse, type WireRespondQuestionRequest, type WireRespondQuestionResponse, type WireRewindAction, type WireRewindDrift, type WireRewindFilePlan, type WireRewindOutcome, type WireRewindPreview, type WireRewindRequest, type WireRewindResponse, type WireRewindScope, type WireRoleAddRequest, type WireRoleAddResponse, type WireSandboxReason, type WireSandboxStatus, type WireScheduleCapability, type WireScheduleCreateRequest, type WireScheduleDefaults, type WireScheduleDeleteResponse, type WireScheduleFixRequest, type WireScheduleFixResponse, type WireScheduleIssue, type WireScheduleIssueCode, type WireScheduleListResponse, type WireSchedulePendingResponse, type WireScheduleRecordingResponse, type WireScheduleRegisterRefusal, type WireScheduleRegisterWarning, type WireScheduleRegistration, type WireScheduleResponse, type WireScheduleRow, type WireScheduleRunResponse, type WireScheduleRunsResponse, type WireScheduleSaveResponse, type WireScheduleUpdateRequest, type WireSecurityResponse, type WireSecurityWorkspace, type WireSendMessageRequest, type WireSendMessageResponse, type WireSessionCandidate, type WireSessionCandidatesResponse, type WireSessionFinish, type WireSessionListResponse, type WireSessionReference, type WireSessionResponse, type WireSessionSummary, type WireSessionWorkspace, type WireSessionWorkspaceResponse, type WireSettingApply, type WireSettingFutile, type WireSettingLayer, type WireSettingRow, type WireSettingStep, type WireSettingValue, type WireSettingWrite, type WireSettingWriteLayer, type WireSettingsResponse, type WireSettingsWriteRequest, type WireSettingsWriteResponse, type WireSkillBodyResponse, type WireSkillImportConflict, type WireSkillImportEntry, type WireSkillImportFailure, type WireSkillImportForm, type WireSkillImportIssue, type WireSkillImportPreviewResponse, type WireSkillImportResponse, type WireSkillSummary, type WireSummaryWorkspace, type WireTaggedEvent, type WireTasksResponse, type WireToolGate, type WireToolGateAxis, type WireToolRow, type WireToolSummary, type WireToolVerdict, type WireToolsResponse, type WireTurnState, type WireWorkspaceBinding, type WireWorkspaceDiffResponse, type WireWorkspaceDirEntry, type WireWorkspaceDirsResponse, type WireWorkspaceRef, apiKeyEnvVar, assertNeverPart, assistantTurnText, attachSessionSurface, chordId, collectPendingApprovals, compressionThresholdTokens, conformsToWire, contentToText, diagnosticToLine, extractAllMentions, extractMentions, extractSessionMentions, filePartSummary, flattenCatalog, getProviderInfo, hasFailure, isApprovalOutcome, isContextNearlyFull, isHeadlessInputFormat, isHeadlessLifecycleEvent, isHeadlessOutputFormat, isKeyAction, isLang, isMentionableFilePath, isOperationType, isPermissionLevel, isPlanOutcome, isProviderType, isQuestionAnswerMap, isSearchProviderType, isValidAgentRoleName, isValidCommandName, isWireGoalPhase, isWireHubEvent, isWirePlanAction, isWireRewindScope, isWireSettingWriteLayer, makeTranslate, normalizeApproval, packSessionSurface, parseChord, parseModelRef, parseSequence, perAction, placeholdersOf, promptTokens, sequenceId, sessionPartSummary, stripSystemNote, systemNote, tableOf, totalUsageTokens, translatorFor, turnDigest, unfixedRules, utf8ByteLength, wireFields };
|