@epoch-agent/protocol 0.10.1 → 0.12.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/dist/index.d.ts +2364 -1682
- package/dist/index.js +68 -12
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -2026,8 +2026,32 @@ interface EpochConfig {
|
|
|
2026
2026
|
*/
|
|
2027
2027
|
maxTurns: number;
|
|
2028
2028
|
contextLength: number;
|
|
2029
|
+
/**
|
|
2030
|
+
* 单次模型请求的输出上限,原样递给 provider 的 `maxOutputTokens`(2026-09-16)。
|
|
2031
|
+
*
|
|
2032
|
+
* **缺省 = 整格不发**,那时出站请求里没有 `max_tokens`、各家服务端按自己的
|
|
2033
|
+
* 默认值办(DeepSeek 是 4K,而它的思维链和正文共用这个预算)。判据全文
|
|
2034
|
+
* ——包括「为什么不默认套用能力表里那个数」——在 core 的
|
|
2035
|
+
* `config/schema.ts` 同名字段上。
|
|
2036
|
+
*
|
|
2037
|
+
* ⚠️ 别和 {@link EpochConfig.budget} 里的 `maxTokens` 弄混:那个是**整个会话**
|
|
2038
|
+
* 的成本闸门,这个是**单次请求**能生成多长。两者量纲不同。
|
|
2039
|
+
*/
|
|
2040
|
+
maxOutputTokens?: number;
|
|
2029
2041
|
homeDir: string;
|
|
2030
2042
|
dbPath: string;
|
|
2043
|
+
/**
|
|
2044
|
+
* 默认权限档。
|
|
2045
|
+
*
|
|
2046
|
+
* ⚠️ **这一格是 `string` 而 `EpochConfigSchema` 那一格是
|
|
2047
|
+
* `z.enum(PERMISSION_LEVELS)`** —— 两边值域对不上,写 `permission: 'yolo'`
|
|
2048
|
+
* 在类型上过得去而运行时会让整份配置退回默认值。
|
|
2049
|
+
*
|
|
2050
|
+
* 2026-09-15 试过把它收成联合(那会让这一格不再撒谎),但收紧之后
|
|
2051
|
+
* `sandbox` 等好几格跟着暴露同族不一致 —— 那是一次独立的收口,不该混在
|
|
2052
|
+
* 「让 build 过」那一笔里。`loader.ts` 那处已经改成用 schema 自己的产出
|
|
2053
|
+
* 当 fallback,所以构建不再卡在这儿。**这条注释是给做那次收口的人留的。**
|
|
2054
|
+
*/
|
|
2031
2055
|
permission: string;
|
|
2032
2056
|
/**
|
|
2033
2057
|
* 紧凑权限规则(方案 22)。三张列表,写法是 `terminal(pnpm test:*)`。
|
|
@@ -2311,22 +2335,47 @@ interface EpochConfig {
|
|
|
2311
2335
|
endpoint?: string;
|
|
2312
2336
|
};
|
|
2313
2337
|
/**
|
|
2314
|
-
* web 搜索(方案 37
|
|
2338
|
+
* web 搜索(方案 37;2026-09-15 改口径)。
|
|
2315
2339
|
*
|
|
2316
|
-
*
|
|
2317
|
-
* 不可用的功能。所以做成 provider 化,而且**没配就诚实地没有这个工具**:
|
|
2318
|
-
* 注册一个一调就报错的工具,比不注册更糟(模型会反复试)。
|
|
2340
|
+
* ## 整段不配 = **照样能搜**(这一条 2026-09-15 反过来了)
|
|
2319
2341
|
*
|
|
2320
|
-
*
|
|
2321
|
-
*
|
|
2342
|
+
* 原来的口径是「没配就诚实地没有这个工具」。它在只看 tavily / brave / searxng
|
|
2343
|
+
* 时是对的(三家都要办账号),但后果是把一个**宿主根本没意识到的漏配**
|
|
2344
|
+
* 变成了「所有终端用户都没有搜索」,而且对模型是静默的 ——
|
|
2345
|
+
* 实测它会改用浏览器抓搜索引擎结果页,把 SEO 广告写进交付物。
|
|
2346
|
+
*
|
|
2347
|
+
* 现在加了两档不用办账号的:`native`(借当前 LLM provider 的能力)和
|
|
2348
|
+
* `keyless`(免费公开端点)。于是缺省状态是**有搜索**,
|
|
2349
|
+
* 而「没有搜索」退化成一个需要显式关掉两档才能到达的用户选择。
|
|
2350
|
+
*
|
|
2351
|
+
* 「配了却缺 key 不回落」那条判据没变:显式写了就只认那一家。
|
|
2322
2352
|
*/
|
|
2323
2353
|
search?: {
|
|
2324
|
-
/**
|
|
2354
|
+
/**
|
|
2355
|
+
* 用哪家。不给 = 按凭据自动挑
|
|
2356
|
+
* (`native` → `tavily` → `brave` → `searxng` → `keyless`)。
|
|
2357
|
+
*
|
|
2358
|
+
* ⚠️ 只设了 `TAVILY_API_KEY` 而没写这一格的人,从 2026-09-15 起会走 `native`
|
|
2359
|
+
* —— 要钉住某一家就把它显式写在这里。
|
|
2360
|
+
*/
|
|
2325
2361
|
provider?: SearchProviderType;
|
|
2326
2362
|
/** `searxng` 自建实例地址(那一家没有 key,靠它定位) */
|
|
2327
2363
|
baseUrl?: string;
|
|
2328
2364
|
/** 一次返回几条,缺省 5。上限 20 —— 再多也超出模型能有效读的范围 */
|
|
2329
2365
|
maxResults?: number;
|
|
2366
|
+
/**
|
|
2367
|
+
* `native` 档的开关,缺省**开**。
|
|
2368
|
+
*
|
|
2369
|
+
* 关掉的理由通常是成本:那一档一次搜索等于一个完整的模型轮次。
|
|
2370
|
+
*/
|
|
2371
|
+
native?: boolean;
|
|
2372
|
+
/**
|
|
2373
|
+
* `keyless` 档的开关,缺省**开**。
|
|
2374
|
+
*
|
|
2375
|
+
* 关掉的理由通常是隐私:那一档会把查询词发给 Exa / Parallel。
|
|
2376
|
+
* 两档都关掉 + 三家都没配 = 回到「没有 `web_search`」那个状态。
|
|
2377
|
+
*/
|
|
2378
|
+
keyless?: boolean;
|
|
2330
2379
|
};
|
|
2331
2380
|
/**
|
|
2332
2381
|
* 内容合规过滤(core 的 `compliance/`)。
|
|
@@ -2629,16 +2678,53 @@ interface EpochConfig {
|
|
|
2629
2678
|
*/
|
|
2630
2679
|
maxTotalBytes?: number;
|
|
2631
2680
|
};
|
|
2681
|
+
/**
|
|
2682
|
+
* 会话库的保留期清理(方案 64 PR-4)。三格逐字照 hermes 的
|
|
2683
|
+
* `sessions: auto_prune / retention_days / vacuum_after_prune`。
|
|
2684
|
+
*
|
|
2685
|
+
* ## ⚠️ `autoPrune` 缺省 `false`,而这是 hermes 写下的判据不是保守
|
|
2686
|
+
*
|
|
2687
|
+
* 它那段文档逐字说的是:**会话历史是 `session_search` 跨历史召回的燃料**,
|
|
2688
|
+
* 删掉就是削弱 agent 的记忆;数百个会话才 10–15 MB,不值得。开启判据是一个
|
|
2689
|
+
* 具体数字:**约 1000 个会话 / 384 MB 的 state.db** 会让 FTS5 插入和
|
|
2690
|
+
* `/resume` 列表变慢。
|
|
2691
|
+
*
|
|
2692
|
+
* ⚠️ 关掉它**不影响手动那条路**(`epoch sessions prune`)——
|
|
2693
|
+
* 这一格管的只是「要不要在每次启动时自己跑一遍」。
|
|
2694
|
+
*/
|
|
2695
|
+
sessions?: {
|
|
2696
|
+
/** 开机时自动清理一次(至多一天一次)。**缺省关** */
|
|
2697
|
+
autoPrune?: boolean;
|
|
2698
|
+
/** 多少天没动过算旧,缺省 90(同 hermes 的 `retention_days`) */
|
|
2699
|
+
retentionDays?: number;
|
|
2700
|
+
/**
|
|
2701
|
+
* 清完之后回收磁盘(`VACUUM`),缺省 `true`。
|
|
2702
|
+
*
|
|
2703
|
+
* ⚠️ **开着也不一定会跑**:还要过「真删掉了东西」「距上次 VACUUM 超过 30 天」
|
|
2704
|
+
* 「空页超过 25%」三道 —— 它要独占整个库文件做全量重写。
|
|
2705
|
+
*/
|
|
2706
|
+
vacuumAfterPrune?: boolean;
|
|
2707
|
+
};
|
|
2632
2708
|
}
|
|
2633
|
-
/**
|
|
2634
|
-
|
|
2709
|
+
/**
|
|
2710
|
+
* 支持的搜索后端。**三档「不用再办一个账号」的**:
|
|
2711
|
+
*
|
|
2712
|
+
* - `native` —— 让当前 LLM provider 自己搜(借已有的 provider key)
|
|
2713
|
+
* - `searxng` —— 自托管无 key,内网 / 隐私敏感场景的答案
|
|
2714
|
+
* - `keyless` —— 免费公开端点组成的环,零 key 零服务器
|
|
2715
|
+
*/
|
|
2716
|
+
declare const SEARCH_PROVIDER_TYPES: readonly ["native", "tavily", "brave", "searxng", "keyless"];
|
|
2635
2717
|
type SearchProviderType = (typeof SEARCH_PROVIDER_TYPES)[number];
|
|
2636
2718
|
declare function isSearchProviderType(value: string): value is SearchProviderType;
|
|
2637
2719
|
/**
|
|
2638
|
-
* 各搜索后端的 API key
|
|
2720
|
+
* 各搜索后端的 API key 环境变量名。不需要**自己的** key 的那几档是 null。
|
|
2721
|
+
*
|
|
2722
|
+
* ⚠️ `native` 是 null **不代表它不要凭据** —— 它借的是 `provider.apiKey`,
|
|
2723
|
+
* 而那把 key 已经在 `PROVIDER_META` 那张表里登记过了。这里再登记一次会让
|
|
2724
|
+
* `API_KEY_ENV_VARS`(凭据预取用的清单)出现重复项。
|
|
2639
2725
|
*
|
|
2640
2726
|
* 和 provider 的 `apiKeyEnv` 分开一张表:那张是 LLM provider 的,
|
|
2641
|
-
*
|
|
2727
|
+
* 混进来会让那份清单多出几个不相干的名字。
|
|
2642
2728
|
*/
|
|
2643
2729
|
declare const SEARCH_API_KEY_ENV: Record<SearchProviderType, string | null>;
|
|
2644
2730
|
|
|
@@ -3301,1987 +3387,2085 @@ declare const METRIC: {
|
|
|
3301
3387
|
declare const NOOP_TELEMETRY: Telemetry;
|
|
3302
3388
|
|
|
3303
3389
|
/**
|
|
3304
|
-
*
|
|
3305
|
-
*
|
|
3306
|
-
* ## 它和审批是两件事
|
|
3390
|
+
* 会话消息契约。
|
|
3307
3391
|
*
|
|
3308
|
-
*
|
|
3309
|
-
* (走开 = 拒绝)。提问问的是「**你想要哪一种**」,答案是内容,而且
|
|
3310
|
-
* **没有安全的默认值** —— 替用户挑一个和替他点「允许」一样糟,只是失败的方式不同:
|
|
3311
|
-
* 前者让模型基于一个假答案干半小时活。
|
|
3392
|
+
* 这是我们**自己的**消息格式,不是 Vercel AI SDK 的 `ModelMessage`。
|
|
3312
3393
|
*
|
|
3313
|
-
*
|
|
3314
|
-
* `
|
|
3394
|
+
* 为什么要有这么一层:改造前 AgentLoop、message-codec、以及 AgentProvider
|
|
3395
|
+
* 这个「依赖倒置接口」全都直接用 `ModelMessage`。后果有三个:
|
|
3396
|
+
* 1. 依赖倒置是假的 —— 想塞一个脚本化的假 provider 做测试,得先 import 'ai'
|
|
3397
|
+
* 2. AI SDK 的消息形状是**联合类型 + 部件数组**,`content` 可能是 string
|
|
3398
|
+
* 也可能是 `Array<{type:'text'|'tool-call'|...}>`,于是 loop 里到处
|
|
3399
|
+
* `as Array<Record<string, unknown>>` 地摸字段
|
|
3400
|
+
* 3. SDK 换大版本(v5→v6→v7 每次都动过消息形状)要改的是整个 agent 层,
|
|
3401
|
+
* 而不是一个转换函数
|
|
3315
3402
|
*
|
|
3316
|
-
*
|
|
3403
|
+
* 形状故意和 SQLite 里持久化的 `Message`(core/session/types.ts)对齐 ——
|
|
3404
|
+
* 一份扁平 content + 可选的工具部件。两边不一致的话,存盘和回放各自要一套转换。
|
|
3317
3405
|
*
|
|
3318
|
-
*
|
|
3319
|
-
* 列进禁止触碰,就是这个意思 —— 给它加一道权限判定,等于让 `deny` 规则能把
|
|
3320
|
-
* 「问一句」拦掉,而拦掉的表现是模型继续瞎猜。
|
|
3406
|
+
* 转成 SDK 格式只发生在一个地方:core/provider/sdk-adapter.ts。
|
|
3321
3407
|
*/
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3408
|
+
|
|
3409
|
+
/** 模型请求执行的一次工具调用 */
|
|
3410
|
+
interface EpochToolCall {
|
|
3411
|
+
toolCallId: string;
|
|
3412
|
+
toolName: string;
|
|
3413
|
+
/** 模型给出的入参 */
|
|
3414
|
+
input: Record<string, unknown>;
|
|
3328
3415
|
}
|
|
3329
|
-
/**
|
|
3330
|
-
interface
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
/**
|
|
3334
|
-
|
|
3335
|
-
/**
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3416
|
+
/** 一次工具执行的结果 */
|
|
3417
|
+
interface EpochToolResult {
|
|
3418
|
+
toolCallId: string;
|
|
3419
|
+
toolName: string;
|
|
3420
|
+
/** 输出的文本形式。工具的返回值统一收成字符串,不往下传结构 */
|
|
3421
|
+
output: string;
|
|
3422
|
+
/**
|
|
3423
|
+
* 二进制产物。**只有过完四道闸门、带 `data` 的才会真的进模型上下文**
|
|
3424
|
+
* (见 core/src/context/artifact-gate.ts);否则这里只有 summary + path,
|
|
3425
|
+
* `output` 里已经含了那句 summary。
|
|
3426
|
+
*/
|
|
3427
|
+
artifacts?: ToolArtifact[];
|
|
3428
|
+
}
|
|
3429
|
+
/** 文本部件 */
|
|
3430
|
+
interface EpochTextPart {
|
|
3431
|
+
type: 'text';
|
|
3432
|
+
text: string;
|
|
3339
3433
|
}
|
|
3340
3434
|
/**
|
|
3341
|
-
*
|
|
3435
|
+
* 图片部件。
|
|
3342
3436
|
*
|
|
3343
|
-
*
|
|
3344
|
-
* `
|
|
3345
|
-
*
|
|
3346
|
-
* 它永远不用的字段,而跨进程那侧有两个候选 id,对不齐时没人说得清该信哪个。
|
|
3437
|
+
* `image` 三种形态:裸 base64、`data:` URL、`http(s)` URL。
|
|
3438
|
+
* 字段名是 `mediaType` 不是 `mimeType` —— 与 `@ai-sdk/provider-utils` 的
|
|
3439
|
+
* `ImagePart` 对齐,省掉边界上的一次改名。
|
|
3347
3440
|
*/
|
|
3348
|
-
interface
|
|
3349
|
-
|
|
3350
|
-
|
|
3441
|
+
interface EpochImagePart {
|
|
3442
|
+
type: 'image';
|
|
3443
|
+
image: string;
|
|
3444
|
+
mediaType?: string;
|
|
3351
3445
|
}
|
|
3446
|
+
/** 附件为什么只有路径没有内容 */
|
|
3447
|
+
type FileOmitReason =
|
|
3448
|
+
/** 探测到 NUL 字节,是二进制 */
|
|
3449
|
+
'binary'
|
|
3450
|
+
/** 权限判定没放行(与 `file_read` 同一条判定) */
|
|
3451
|
+
| 'denied'
|
|
3452
|
+
/** 读不到(不存在 / 没权限 / 是目录) */
|
|
3453
|
+
| 'unreadable'
|
|
3454
|
+
/** 这一条消息附的文件个数或总量超了上限 */
|
|
3455
|
+
| 'over-limit'
|
|
3352
3456
|
/**
|
|
3353
|
-
*
|
|
3457
|
+
* 会话是 resume 出来的,附件内容**不重读**。
|
|
3354
3458
|
*
|
|
3355
|
-
*
|
|
3356
|
-
*
|
|
3357
|
-
*
|
|
3459
|
+
* 内容当初是过了权限判定才附上的,而那次判定发生在另一个上下文里
|
|
3460
|
+
* (当时的权限级别、当时的信任状态)。恢复时无声地再读一遍磁盘等于让一次
|
|
3461
|
+
* 旧的授权在新的上下文里继续生效。模型仍然看得见路径,要内容就自己调
|
|
3462
|
+
* `file_read` —— 那条路有完整的审批。
|
|
3463
|
+
*/
|
|
3464
|
+
| 'not-restored';
|
|
3465
|
+
/**
|
|
3466
|
+
* 文件附件部件 —— 用户用 `@路径` 提到的文件(方案 25)。
|
|
3358
3467
|
*
|
|
3359
|
-
*
|
|
3360
|
-
*
|
|
3361
|
-
*
|
|
3362
|
-
*
|
|
3468
|
+
* ## 为什么是一种部件,而不是把内容拼进文本里
|
|
3469
|
+
*
|
|
3470
|
+
* 拼字符串看起来省事,代价有四个,每一个都要在很久以后才暴露:压缩器分不清
|
|
3471
|
+
* 「用户打的字」和「附的文件」于是压错东西、token 统计把文件算成用户输入、
|
|
3472
|
+
* artifact 化那条路完全绕过、web 宿主收到的 wire 载荷里两者无法区分。
|
|
3473
|
+
* 单独一种部件之后,这四件事各自都有地方判。
|
|
3474
|
+
*
|
|
3475
|
+
* ## `text` 缺席是常态,不是异常
|
|
3476
|
+
*
|
|
3477
|
+
* 二进制、超限、权限没放行时**只留路径**。这时候模型看到的是「用户提到了这个
|
|
3478
|
+
* 文件,但内容没给你,原因是 X」—— 它可以自己去调 `file_read`(那条路有完整的
|
|
3479
|
+
* 审批流程)。比起静默附上或者静默丢掉,这是唯一不会让人误判的形态。
|
|
3363
3480
|
*/
|
|
3364
|
-
interface
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3481
|
+
interface EpochFilePart {
|
|
3482
|
+
type: 'file';
|
|
3483
|
+
/** 用户敲的那个路径(相对工作区,`/` 分隔),原样保留用于显示 */
|
|
3484
|
+
path: string;
|
|
3485
|
+
/** 文件内容。**缺席时看 `omitted`** */
|
|
3486
|
+
text?: string;
|
|
3487
|
+
/** 没给内容的原因;给了 `text` 时不出现 */
|
|
3488
|
+
omitted?: FileOmitReason;
|
|
3489
|
+
/** 内容被截断到多少字节。给了就是截断过 —— 截断**永远不静默** */
|
|
3490
|
+
truncatedTo?: number;
|
|
3491
|
+
/** 文件原始字节数,用于展示 */
|
|
3492
|
+
bytes?: number;
|
|
3369
3493
|
}
|
|
3370
3494
|
/**
|
|
3371
|
-
*
|
|
3495
|
+
* 引用的会话为什么只有标题没有内容(方案 53 §4.2)。
|
|
3372
3496
|
*
|
|
3373
|
-
*
|
|
3374
|
-
*
|
|
3497
|
+
* 前五个是**稳定的错误码**,给宿主路由用(headless / 嵌入宿主要能按码分流,
|
|
3498
|
+
* TUI 和 Web 要各自渲染)。清单本体是 {@link SESSION_REFERENCE_ERROR_CODES},
|
|
3499
|
+
* 那才是同构位置的唯一真源。
|
|
3375
3500
|
*/
|
|
3376
|
-
|
|
3377
|
-
/**
|
|
3378
|
-
|
|
3379
|
-
/**
|
|
3380
|
-
|
|
3381
|
-
/**
|
|
3382
|
-
|
|
3501
|
+
type SessionOmitReason =
|
|
3502
|
+
/** `@:xxx` 匹配不到**唯一**的一段会话(零个,或好几个) */
|
|
3503
|
+
'invalid-reference'
|
|
3504
|
+
/** 引用的是当前这段会话自己 */
|
|
3505
|
+
| 'self-reference'
|
|
3506
|
+
/** 超过每条消息能引用的会话个数 */
|
|
3507
|
+
| 'too-many'
|
|
3508
|
+
/** 会话可读性判定不放行(方案 48 的那一处判定,见 core/session/authorization.ts) */
|
|
3509
|
+
| 'not-authorized'
|
|
3510
|
+
/** 这一条消息的附件总量已经用完 */
|
|
3511
|
+
| 'budget-exceeded'
|
|
3383
3512
|
/**
|
|
3384
|
-
*
|
|
3513
|
+
* 会话是 resume 出来的,引用的内容**不重读**。
|
|
3385
3514
|
*
|
|
3386
|
-
*
|
|
3387
|
-
*
|
|
3388
|
-
*
|
|
3515
|
+
* 判据逐字同 {@link FileOmitReason} 的 `not-restored`:内容当初是过了可读性
|
|
3516
|
+
* 判定才附上的,而那次判定发生在另一个上下文里。恢复时无声地再读一遍等于让
|
|
3517
|
+
* 一次旧的授权在新的上下文里继续生效。模型仍然看得见「用户引用过这段会话」,
|
|
3518
|
+
* 要内容就自己调 `session_search`(方案 48 那条路,有工具审批)。
|
|
3389
3519
|
*
|
|
3390
|
-
*
|
|
3391
|
-
* 两个出口(stdin / REST)各写一遍就会有一边漏掉数组那一种形态。
|
|
3520
|
+
* **它不是错误码**,所以不在 {@link SESSION_REFERENCE_ERROR_CODES} 里。
|
|
3392
3521
|
*/
|
|
3393
|
-
|
|
3394
|
-
|
|
3522
|
+
| 'not-restored';
|
|
3395
3523
|
/**
|
|
3396
|
-
*
|
|
3524
|
+
* 五个稳定的错误码(方案 53 §4.2)—— **同构位置的唯一真源**。
|
|
3397
3525
|
*
|
|
3398
|
-
*
|
|
3399
|
-
*
|
|
3400
|
-
*
|
|
3401
|
-
*
|
|
3402
|
-
* - 协议里没有 callId,TUI 只能靠「toolName + 调用顺序 FIFO」猜工具结果
|
|
3403
|
-
* 属于哪次调用,并行工具就会串台。
|
|
3404
|
-
* 判别联合让每个事件的必填字段由编译器保证,callId 成为一等字段。
|
|
3526
|
+
* 为什么要稳定的码而不是一句话:headless / 嵌入宿主要能路由这些失败
|
|
3527
|
+
* (`docs/HEADLESS.md` 的退出码那套已经是这个思路),而且 TUI 和 Web 要各自渲染。
|
|
3528
|
+
*
|
|
3529
|
+
* `not-restored` 刻意不在里面 —— 它是「会话恢复了」这个事实,不是一次失败。
|
|
3405
3530
|
*/
|
|
3406
|
-
|
|
3531
|
+
declare const SESSION_REFERENCE_ERROR_CODES: readonly SessionOmitReason[];
|
|
3407
3532
|
/**
|
|
3408
|
-
*
|
|
3533
|
+
* 引用另一段会话的部件 —— 用户用 `@:xxx` 提到的那段会话(方案 53)。
|
|
3409
3534
|
*
|
|
3410
|
-
*
|
|
3411
|
-
* 后者多两个字段——同名不同义,收成一个超集。
|
|
3535
|
+
* ## 塞进来的是那段会话的「当前面」,不是原始全量(§二)
|
|
3412
3536
|
*
|
|
3413
|
-
*
|
|
3537
|
+
* 一个跑了两小时、压缩过三次的会话,`messages` 表里有 800 条,但模型当时能看到
|
|
3538
|
+
* 的只有 60 条(其余 `active = 0`)。这里带的是**那 60 条**。三条理由:800 条
|
|
3539
|
+
* 任何预算都塞不下;那 60 条就是这段会话的结论(压缩器已经把前面浓缩进去了);
|
|
3540
|
+
* 原始全量里有已经被推翻的内容(「先试了 A 不行改用 B」,压缩之后留下的是 B)。
|
|
3414
3541
|
*
|
|
3415
|
-
*
|
|
3542
|
+
* ## 它是**快照**,不是活引用
|
|
3416
3543
|
*
|
|
3417
|
-
*
|
|
3418
|
-
*
|
|
3419
|
-
* | `cacheHitTokens` `cacheWriteTokens` | 与 `inputTokens` **不重叠** |
|
|
3420
|
-
* | `reasoningTokens` | **包含在** `outputTokens` 里 |
|
|
3421
|
-
* | `totalTokens` | 输入总量 + 输出总量 |
|
|
3544
|
+
* 引用是发消息那一刻的快照,之后源会话被归档、删除、继续聊都不影响这一条。
|
|
3545
|
+
* **别去做「引用失效」的检测** —— 那等于让一条已经发出去的消息随时间变形。
|
|
3422
3546
|
*
|
|
3423
|
-
*
|
|
3424
|
-
* **不加** `reasoningTokens`。计价同理,见 `core/src/cost/pricing.ts` 文件头。
|
|
3547
|
+
* ## `text` 缺席是常态
|
|
3425
3548
|
*
|
|
3426
|
-
*
|
|
3427
|
-
*
|
|
3428
|
-
*
|
|
3549
|
+
* 匹配不到、引用自己、超上限、判定不放行时**只留 `ref` 和原因**。这时候模型
|
|
3550
|
+
* 看到的是「用户引用了某段会话,但内容没给你,原因是 X」—— 要内容它可以自己调
|
|
3551
|
+
* `session_search`(方案 48)。这保证了 `@:` 永远不会成为一条比 `session_search`
|
|
3552
|
+
* **更宽松**的读取通道,和 `@文件` 对 `file_read` 那条判据逐字对应。
|
|
3429
3553
|
*/
|
|
3430
|
-
interface
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
reasoningTokens?: number;
|
|
3451
|
-
/**
|
|
3452
|
-
* SDK 报的总数:输入总量(含缓存两项)+ 输出总量。
|
|
3453
|
-
*
|
|
3454
|
-
* 要显示「一共多少 token」优先用这个,自己四项相加的只当兜底 ——
|
|
3455
|
-
* 前者是 provider 报的两个总量直接相加,后者要先经过我们的拆分算术
|
|
3456
|
-
* (`inputTokens` 取的是 noCache),多一层就多一个出错的地方。
|
|
3457
|
-
* provider 没上报用量时为 undefined。
|
|
3458
|
-
*/
|
|
3459
|
-
totalTokens?: number;
|
|
3460
|
-
/**
|
|
3461
|
-
* 本次用量折算的美元金额。模型没有定价数据时为 undefined,**不要填 0**。
|
|
3462
|
-
*
|
|
3463
|
-
* 0 意味着「免费」,undefined 意味着「不知道」。预算闸门对这两种情况的处理
|
|
3464
|
-
* 必须不同——未知定价不能当免费放行。
|
|
3465
|
-
*/
|
|
3466
|
-
costUsd?: number;
|
|
3554
|
+
interface EpochSessionPart {
|
|
3555
|
+
type: 'session';
|
|
3556
|
+
/** 用户敲的那个引用串(`@:` 后面那一截),原样保留用于显示 */
|
|
3557
|
+
ref: string;
|
|
3558
|
+
/** 匹配到的会话 id。**没匹配上时缺席** */
|
|
3559
|
+
sessionId?: string;
|
|
3560
|
+
/** 那段会话的标题(空标题时是 sessionId,见方案 53 §5.3) */
|
|
3561
|
+
title?: string;
|
|
3562
|
+
/** 那段会话的工作目录,用于让人 / 模型看出「这是哪个项目的会话」 */
|
|
3563
|
+
cwd?: string;
|
|
3564
|
+
/** 当前面的正文。**缺席时看 `omitted`** */
|
|
3565
|
+
text?: string;
|
|
3566
|
+
/** 没给内容的原因;给了 `text` 时不出现 */
|
|
3567
|
+
omitted?: SessionOmitReason;
|
|
3568
|
+
/** 内容被截断到多少字节。给了就是截断过 —— 截断**永远不静默** */
|
|
3569
|
+
truncatedTo?: number;
|
|
3570
|
+
/** 当前面有多少条消息(`active = 1` 的那些) */
|
|
3571
|
+
messageCount?: number;
|
|
3572
|
+
/** 那段会话一共多少条(含被压掉的)。两个数不等就说明它压缩过 */
|
|
3573
|
+
totalMessages?: number;
|
|
3467
3574
|
}
|
|
3575
|
+
type EpochContentPart = EpochTextPart | EpochImagePart | EpochFilePart | EpochSessionPart;
|
|
3576
|
+
/** 用户消息内容 —— 纯文本或多模态部件数组 */
|
|
3577
|
+
type EpochUserContent = string | EpochContentPart[];
|
|
3468
3578
|
/**
|
|
3469
|
-
*
|
|
3470
|
-
*
|
|
3471
|
-
* 住在 protocol 而不是 `core/src/cost/types.ts`:它出现在 `finish` 事件上,
|
|
3472
|
-
* 于是**跨进程的宿主也要认它** —— headless 的 `result.reason` 要靠它区分
|
|
3473
|
-
* 「这个月的额度用完了」和「这一次调用的 `--max-budget-usd` 到了」。
|
|
3474
|
-
* core 那边 re-export 这一份,不另立一个同名类型。
|
|
3475
|
-
*/
|
|
3476
|
-
type BudgetBreach =
|
|
3477
|
-
/** 金额超过 `budget.maxCostUsd`(跨会话累计) */
|
|
3478
|
-
'cost'
|
|
3479
|
-
/** token 数超过 `budget.maxTokens`(跨会话累计) */
|
|
3480
|
-
| 'tokens'
|
|
3481
|
-
/** 模型无定价数据,且 `budget.onUnknownPricing` 为 `'block'` */
|
|
3482
|
-
| 'unknown-pricing'
|
|
3483
|
-
/**
|
|
3484
|
-
* **本次调用**的金额上限到了(`RunInput.maxCallCostUsd` / `--max-budget-usd`)。
|
|
3579
|
+
* 附件在**给模型的**纯文本投影里长什么样。**不吐文件内容** —— 内容走 `parts` 那一路。
|
|
3485
3580
|
*
|
|
3486
|
-
*
|
|
3487
|
-
*
|
|
3488
|
-
*
|
|
3581
|
+
* 输出**逐字冻住**:门禁在 `packages/protocol/__tests__/message-projection.test.ts`,
|
|
3582
|
+
* 它把五种 `omitted` 原因加截断、不截断两支全钉死了。改一个字那份会当场红 ——
|
|
3583
|
+
* 那不是「用例过时了」,是你正在改模型的输入契约(判据见 {@link OMIT_LABEL})。
|
|
3489
3584
|
*/
|
|
3490
|
-
|
|
3491
|
-
/** 一次运行的收尾原因 */
|
|
3492
|
-
type FinishReason =
|
|
3493
|
-
/** 模型正常收工 */
|
|
3494
|
-
'stop'
|
|
3495
|
-
/** 用尽轮次预算 */
|
|
3496
|
-
| 'max-turns'
|
|
3497
|
-
/** 死循环检测触发 —— 连续多轮重复同一个工具调用 */
|
|
3498
|
-
| 'stalled'
|
|
3585
|
+
declare function filePartSummary(part: EpochFilePart): string;
|
|
3499
3586
|
/**
|
|
3500
|
-
*
|
|
3501
|
-
*
|
|
3502
|
-
* ⚠️ **和 `'stalled'` 刻意分开。** 那一档是「连续多轮调同一个工具」,
|
|
3503
|
-
* 用户能做的是换个说法或者自己接手;这一档整轮工具调用为零,是解码退化,
|
|
3504
|
-
* 处置是重发一次(大概率就好了)。混成一个值之后,界面上只能给出一句
|
|
3505
|
-
* 同时对两种情况都不准的话。
|
|
3587
|
+
* 历史里的图被**摘掉**之后留在原位的那一行(2026-09-12)。
|
|
3506
3588
|
*
|
|
3507
|
-
*
|
|
3508
|
-
* 那半截正文是模型真想说的;这一档是**我们**在它撞上上限之前主动停的,
|
|
3509
|
-
* 已经产出的正文留着但没有价值。两者在账上差一整个输出预算 ——
|
|
3510
|
-
* 现场那一份是 31.6 KB 里只有 12 种句子。
|
|
3511
|
-
*/
|
|
3512
|
-
| 'looping'
|
|
3513
|
-
/** 被宿主中止 */
|
|
3514
|
-
| 'aborted'
|
|
3515
|
-
/** 模型安全策略拒答 —— 是 **provider** 拒答,不是我们拦的 */
|
|
3516
|
-
| 'content-filter'
|
|
3517
|
-
/**
|
|
3518
|
-
* 我们自己的内容合规闸门拦下了(core 的 `compliance/`)。
|
|
3589
|
+
* ## 它补的是一个静默的口子,不是「又一个占位符」
|
|
3519
3590
|
*
|
|
3520
|
-
*
|
|
3521
|
-
*
|
|
3522
|
-
*
|
|
3523
|
-
*
|
|
3591
|
+
* 喂模型的历史里图片是 base64 部件(`session-store.loadHistory()` 把
|
|
3592
|
+
* `inlineImages` 写死成 true)。而「当前模型认不认图」那道闸门在
|
|
3593
|
+
* `core/agent/loop.ts` 上,读的是**这一轮用户新发的图**
|
|
3594
|
+
* (`imageParts(input.userMessage)`)—— 也就是说换到一个不认图的模型之后只要
|
|
3595
|
+
* 不再发新图,历史里那些图会原样发出去:provider 那侧要么 400、要么**一声不吭
|
|
3596
|
+
* 地吞掉**(后者实测存在,判据全文在 `core/provider/capability-probe.ts` 的
|
|
3597
|
+
* 文件头)。摘掉它们的是 `AgentLoop`,留下的那一行是这一个。
|
|
3524
3598
|
*
|
|
3525
|
-
*
|
|
3526
|
-
* 与输出侧被拦下的区别在文本里说清,不在这个枚举里分叉。
|
|
3527
|
-
*/
|
|
3528
|
-
| 'compliance'
|
|
3529
|
-
/**
|
|
3530
|
-
* provider 把这一轮的回复**按 max output tokens 截断了**(2026-09-04)。
|
|
3599
|
+
* ## 为什么不复用 `contentToText` 那个 `[图片 image/jpeg]`
|
|
3531
3600
|
*
|
|
3532
|
-
*
|
|
3601
|
+
* 那一个是**另一个消费者**的投影(`imagesCarriedAsParts: false`,喂压缩器 /
|
|
3602
|
+
* 审计 / `messages.content` 那一列),而且它有前科:图片真的送达之后,它成了
|
|
3603
|
+
* 「唯一还在撒谎的地方」(判据在 {@link ContentToTextOptions.imagesCarriedAsParts})。
|
|
3604
|
+
* 这一行要说的是另一件事 —— 图**确实没给它**,以及为什么。
|
|
3533
3605
|
*
|
|
3534
|
-
*
|
|
3535
|
-
*
|
|
3536
|
-
*
|
|
3537
|
-
* 「我先确认苏菲店的信息,再拉昨日数据。」+ 0 个工具调用,到底是模型宣布了
|
|
3538
|
-
* 下一步就收工(该由 `verify:stop` 那道闸救),还是它正要发工具调用时
|
|
3539
|
-
* 被截断(该调高 max output tokens)—— 处置完全相反。
|
|
3606
|
+
* 措辞里带上「不支持」这半句,判据同 `agent/prompt.ts` 里那条
|
|
3607
|
+
* 「不支持那一档要说清后果,别笼统一句不支持」:只写「图片未附上」的话,
|
|
3608
|
+
* 模型会去猜是不是传坏了,然后建议用户重发一次 —— 而重发多少次都一样。
|
|
3540
3609
|
*
|
|
3541
|
-
*
|
|
3542
|
-
*
|
|
3543
|
-
*
|
|
3610
|
+
* ⚠️ **不给「你可以自己去读」这类下一步**,和 `OMIT_LABEL` 的 `binary` 那档
|
|
3611
|
+
* 不同:那一档有 `file_read` 真的能拿到内容,这一档模型没有任何办法看见像素。
|
|
3612
|
+
* 给一个不存在的出口,比不给更糟。
|
|
3544
3613
|
*
|
|
3545
|
-
*
|
|
3546
|
-
* 只是它不完整。宿主该说的话是「回复被截断了」而不是「失败了」。
|
|
3614
|
+
* 逐字冻住,理由同 {@link OMIT_LABEL}(进模型 prompt、进 eval 指纹、不进 catalog)。
|
|
3547
3615
|
*/
|
|
3548
|
-
|
|
3549
|
-
/** 触达成本或 token 预算上限 */
|
|
3550
|
-
| 'budget-exceeded'
|
|
3551
|
-
/** 异常终止(详情见前一个 error 事件) */
|
|
3552
|
-
| 'error';
|
|
3553
|
-
type AgentEvent =
|
|
3616
|
+
declare function imagePartOmittedSummary(part: EpochImagePart): string;
|
|
3554
3617
|
/**
|
|
3555
|
-
*
|
|
3618
|
+
* 引用的会话在纯文本投影里长什么样。**不吐那段会话的正文** —— 同 `filePartSummary`。
|
|
3556
3619
|
*
|
|
3557
|
-
* ##
|
|
3620
|
+
* ## 这一行刻意是英文 + 码,不是中文散文
|
|
3558
3621
|
*
|
|
3559
|
-
*
|
|
3560
|
-
*
|
|
3561
|
-
*
|
|
3562
|
-
*
|
|
3622
|
+
* 它的读者有三个,**没有一个是界面**:模型(经 sdk-adapter 进上下文)、
|
|
3623
|
+
* `messages.content` 那一列(存盘 / 压缩 / 审计 / FTS)、以及宿主的日志。
|
|
3624
|
+
* 界面要展示这一部件时读的是结构化字段(`omitted` / `title` / `messageCount`),
|
|
3625
|
+
* 自己组措辞 —— TUI 那一侧的中文在宿主用 `t()` 拼,见 locales 的 `mention.*`。
|
|
3563
3626
|
*
|
|
3564
|
-
*
|
|
3565
|
-
*
|
|
3627
|
+
* > ⚠️ 「没有一个是界面」这句话 2026-08-24 之前**有个洞**:`contentToText()`
|
|
3628
|
+
* > 会把这一行塞进 tui / web 的气泡里,于是它事实上也是界面的。洞补在
|
|
3629
|
+
* > §34.4 —— 界面改走 `contentToDisplayText()`,那一份把这一行换成
|
|
3630
|
+
* > catalog 的 `message_part.session*`。所以上面那句话现在是真的了。
|
|
3566
3631
|
*
|
|
3567
|
-
*
|
|
3632
|
+
* 所以这里直接把**那五个稳定错误码**原样写进去(§4.2):宿主能 grep,模型能读,
|
|
3633
|
+
* 而且它不必跟着界面语言变 —— 一条落库的记录跟着 `EPOCH_LANG` 变语言,
|
|
3634
|
+
* 等于把同一段会话的历史劈成两半(`goal/wording.ts` 那条豁免记的是同一个坑)。
|
|
3568
3635
|
*
|
|
3569
|
-
*
|
|
3570
|
-
*
|
|
3571
|
-
|
|
3636
|
+
* 显示名用标题而不是 sessionId:后者是一串随机字符,对人和模型都没有信息量。
|
|
3637
|
+
* 标题为空的会话在解析那一侧已经用 sessionId 兜过底了(方案 53 §5.3)。
|
|
3638
|
+
*/
|
|
3639
|
+
declare function sessionPartSummary(part: EpochSessionPart): string;
|
|
3640
|
+
/**
|
|
3641
|
+
* 取部件数组的纯文本投影:图片折成一句占位,绝不吐 base64;
|
|
3642
|
+
* 文件和引用的会话同样只折成一行占位,**不吐内容**。
|
|
3572
3643
|
*
|
|
3573
|
-
*
|
|
3644
|
+
* 投影是**存盘、压缩、审计**看的那一份。把几十 KB 的文件内容塞进来,
|
|
3645
|
+
* `sessions.db` 会跟着一起胖,而回放时那份内容本来就能从 `parts` 里拿。
|
|
3574
3646
|
*
|
|
3575
|
-
*
|
|
3576
|
-
* 这是 `provider/tokenizer.ts` 文件头明写的「有意的近似」;
|
|
3577
|
-
* 2. 更硬的一条:这个数是**发之前**算的,而真值只有 provider 回的 usage 说了算,
|
|
3578
|
-
* 中途还会插进压缩、任务通知、工具结果。
|
|
3647
|
+
* ## ⚠️ 这一份**只给模型和落库**,界面不许用它(RECORD-40-i18n §34.4)
|
|
3579
3648
|
*
|
|
3580
|
-
*
|
|
3581
|
-
*
|
|
3649
|
+
* 改造前它一个人喂三类消费者,于是把界面切成 English 的人会在**自己那条消息的
|
|
3650
|
+
* 气泡里**看到 `[图片 image/png]` / `[文件 …:二进制文件,未附内容]`。
|
|
3651
|
+
* 这笔债在 §34.4 记了、§34.4 还掉,处置是**分家**:
|
|
3582
3652
|
*
|
|
3583
|
-
*
|
|
3653
|
+
* | 谁要 | 用哪一份 | 为什么 |
|
|
3654
|
+
* | ---- | -------- | ------ |
|
|
3655
|
+
* | 模型(`sdk-adapter.ts` / `agent/loop.ts` / `runtime/agent-session.ts`) | **这一个** | 模型输入契约 + eval 指纹,跟着 locale 变就漂(§33.2) |
|
|
3656
|
+
* | ↳ 其中 `agent/loop.ts` 拼**给模型的文本部件**那一次 | 这一个 + `{ imagesCarriedAsParts: true }` | 图片另有真部件带着,占位符会变成一句假话(判据见 {@link ContentToTextOptions}) |
|
|
3657
|
+
* | `messages.content` 那一列(存盘 / 压缩 / 审计 / FTS) | **这一个** | 一条落库的记录跟着 `EPOCH_LANGUAGE` 变语言,等于把同一段会话的历史劈成两半 |
|
|
3658
|
+
* | 界面(tui 的历史项、web 的气泡) | `contentToDisplayText(content, t)` | 那一份走 catalog 的 `message_part.*`,住在两个宿主里 |
|
|
3584
3659
|
*
|
|
3585
|
-
*
|
|
3586
|
-
*
|
|
3587
|
-
* 而那正是徽标要替用户回答的问题。两笔账两条事件,别合。
|
|
3660
|
+
* 界面那一份**落在宿主层**而不是这儿,判据同 §34.6 给 `view` 那 1 处的做法
|
|
3661
|
+
* (兜底文案挪给渲染端):这个包的依赖白名单是空的,`t()` 只能由有它的那一层调。
|
|
3588
3662
|
*/
|
|
3589
|
-
{
|
|
3590
|
-
type: 'run-start';
|
|
3663
|
+
interface ContentToTextOptions {
|
|
3591
3664
|
/**
|
|
3592
|
-
*
|
|
3593
|
-
*
|
|
3594
|
-
*
|
|
3665
|
+
* 这一条消息的图片**是不是另有真部件带着**(`EpochMessage.parts` 里那份
|
|
3666
|
+
* base64,最终变成 provider 的原生图片部件)。
|
|
3667
|
+
*
|
|
3668
|
+
* 默认 `false` —— 也就是改造前的行为,落库那一列要的正是它:那一行
|
|
3669
|
+
* `[图片 image/png]` 是**这一轮带过一张图**的唯一记录(`parts` 列另存,
|
|
3670
|
+
* 但 FTS / 预览 / 标题只看 content)。
|
|
3671
|
+
*
|
|
3672
|
+
* `true` 时图片那一支整个**不折占位符**。判据是那句占位符的含义 ——
|
|
3673
|
+
* 「这里本来有张图,但你看不到」。图真的在旁边时它就成了一句假话,而且是
|
|
3674
|
+
* 会被照着信的那种:2026-09-01 实测,`@ai-sdk/deepseek@3.0.20` 把图片
|
|
3675
|
+
* 静默丢了之后,模型读到的就是这一行,于是它逐字回答「我只看到
|
|
3676
|
+
* 「[图片 image/jpeg]」这个占位符,图片没有传到我这边」。**升级 SDK 让图片
|
|
3677
|
+
* 真的送达之后,这一行反而成了唯一还在撒谎的地方。**
|
|
3678
|
+
*
|
|
3679
|
+
* ⚠️ 只关图片那一支。`file` / `session` 的占位符**照旧**:它们说的是
|
|
3680
|
+
* 「内容没附上」(`omitted` / `not-restored`),那句话在任何情况下都是真的,
|
|
3681
|
+
* 而且是模型唯一能知道「有个东西没给我」的途径。
|
|
3595
3682
|
*/
|
|
3596
|
-
|
|
3597
|
-
/** 运行前估算。见上面那两条为什么它永远带「估算」二字 */
|
|
3598
|
-
estimatedTokens: number;
|
|
3599
|
-
}
|
|
3600
|
-
/** 回复正文增量 */
|
|
3601
|
-
| {
|
|
3602
|
-
type: 'text-delta';
|
|
3603
|
-
text: string;
|
|
3604
|
-
}
|
|
3605
|
-
/** 推理模型的思考过程增量 */
|
|
3606
|
-
| {
|
|
3607
|
-
type: 'reasoning-delta';
|
|
3608
|
-
text: string;
|
|
3609
|
-
}
|
|
3610
|
-
/** 模型请求调用工具(执行前) */
|
|
3611
|
-
| {
|
|
3612
|
-
type: 'tool-call';
|
|
3613
|
-
callId: string;
|
|
3614
|
-
toolName: string;
|
|
3615
|
-
input: Record<string, unknown>;
|
|
3683
|
+
imagesCarriedAsParts?: boolean;
|
|
3616
3684
|
}
|
|
3685
|
+
declare function contentToText(content: EpochUserContent, opts?: ContentToTextOptions): string;
|
|
3617
3686
|
/**
|
|
3618
|
-
*
|
|
3687
|
+
* 「上一轮实际动过手」在**给模型的**历史里长什么样(2026-08-28)。
|
|
3619
3688
|
*
|
|
3620
|
-
*
|
|
3621
|
-
* (那份已经截断、已经包过隔离标签)。加它是因为 `tool-call` 和 `tool-result`
|
|
3622
|
-
* 之间可以隔着一次三分钟的 `pnpm build` —— 在那三分钟里宿主一个事件都收不到,
|
|
3623
|
-
* 屏幕上是一行不动的 `⏳ terminal`,用户分不清是在跑还是挂了。
|
|
3689
|
+
* ## 它修的是什么
|
|
3624
3690
|
*
|
|
3625
|
-
*
|
|
3626
|
-
*
|
|
3691
|
+
* 喂模型的那份历史只有 user / assistant 两种角色的**文本**
|
|
3692
|
+
* (`runtime/src/agent-session.ts` 的累积、`session-store.ts` 的 `loadHistory()`),
|
|
3693
|
+
* 工具调用和结果一条都不在里面 —— 这是刻意的(孤立的 tool 消息会被 provider 拒掉,
|
|
3694
|
+
* 全量 transcript 是另一个体量的事)。代价此前没人算:一段五条消息的会话,
|
|
3695
|
+
* 模型看到的是**五条又长又具体的指令,配四句十来个字的「办完了」**。
|
|
3696
|
+
* 早期那几条指令的字数远大于「它已经做完了」的证据量,于是模型把它们当成还在
|
|
3697
|
+
* 生效的待办,回头去做 —— 而用户刚发的那一条被摊薄。实测在
|
|
3698
|
+
* `packages/runtime/__tests__/context-recency.test.ts`。
|
|
3627
3699
|
*
|
|
3628
|
-
*
|
|
3629
|
-
*/
|
|
3630
|
-
| {
|
|
3631
|
-
type: 'tool-output-delta';
|
|
3632
|
-
callId: string;
|
|
3633
|
-
toolName: string;
|
|
3634
|
-
text: string;
|
|
3635
|
-
}
|
|
3636
|
-
/**
|
|
3637
|
-
* 工具执行完毕。callId 与对应的 tool-call 一致。
|
|
3700
|
+
* 这一行就是补那份证据:不复原过程,只留一句「这一轮真的调了这些工具」。
|
|
3638
3701
|
*
|
|
3639
|
-
*
|
|
3640
|
-
* `output` 是喂给模型的纯文本(已含每个 artifact 的 summary),
|
|
3641
|
-
* `artifacts` 是给人看的那一份 —— 宿主不必解析 output 去猜有没有图。
|
|
3642
|
-
*/
|
|
3643
|
-
| {
|
|
3644
|
-
type: 'tool-result';
|
|
3645
|
-
callId: string;
|
|
3646
|
-
toolName: string;
|
|
3647
|
-
output: string;
|
|
3648
|
-
ok: boolean;
|
|
3649
|
-
artifacts?: ToolArtifact[];
|
|
3650
|
-
}
|
|
3651
|
-
/** 一轮 ReAct 结束(轮次从 1 开始) */
|
|
3652
|
-
| {
|
|
3653
|
-
type: 'turn-complete';
|
|
3654
|
-
turn: number;
|
|
3655
|
-
}
|
|
3656
|
-
/**
|
|
3657
|
-
* 需要用户确认。宿主**必须**调用 respond(),否则引擎会一直阻塞在这里。
|
|
3658
|
-
* 引擎侧对未答复的请求有兜底(流结束时按 deny 处理),但那意味着操作被拒。
|
|
3702
|
+
* ## 判据用**结果**而不是调用
|
|
3659
3703
|
*
|
|
3660
|
-
* `
|
|
3661
|
-
*
|
|
3662
|
-
*
|
|
3663
|
-
|
|
3664
|
-
| {
|
|
3665
|
-
type: 'approval-request';
|
|
3666
|
-
request: ApprovalRequest;
|
|
3667
|
-
respond: (outcome: ApprovalOutcome, note?: string) => void;
|
|
3668
|
-
}
|
|
3669
|
-
/**
|
|
3670
|
-
* 模型问了一个结构化问题,等用户选(方案 34)。宿主**必须**调用 `respond()`,
|
|
3671
|
-
* 否则引擎会一直阻塞在这里。
|
|
3704
|
+
* `EpochToolCall` 上有工具名,但一次跑到一半被中止的轮次里那些调用**从没执行过**
|
|
3705
|
+
* (判据逐字同 `runtime/src/session-file-changes.ts` 的边界 1)。而一条
|
|
3706
|
+
* `EpochToolResult` 存在就意味着它跑完了 —— 拿结果当输入,「执行过没有」这件事
|
|
3707
|
+
* 不需要另外判一次。
|
|
3672
3708
|
*
|
|
3673
|
-
*
|
|
3674
|
-
*
|
|
3675
|
-
*
|
|
3676
|
-
* 点「允许」一样糟。答不上来只有一种表达:`respond({ answers: {}, skipped: true })`,
|
|
3677
|
-
* 模型收到之后**自己决定**(通常是选默认继续),这一轮不算失败。
|
|
3709
|
+
* ⚠️ **它分不出成功和失败**:落盘的结果上没有 `success`(同上那份的边界 2)。
|
|
3710
|
+
* 所以措辞是「调用过」而不是「做成了」—— 一次被权限拒掉的 `file_write`
|
|
3711
|
+
* 在这里照样留一笔,而那句话仍然是真的。
|
|
3678
3712
|
*
|
|
3679
|
-
*
|
|
3680
|
-
* 是如实说「没人答」。
|
|
3681
|
-
*/
|
|
3682
|
-
| {
|
|
3683
|
-
type: 'question';
|
|
3684
|
-
request: QuestionRequest;
|
|
3685
|
-
respond: (answer: QuestionAnswer) => void;
|
|
3686
|
-
}
|
|
3687
|
-
/**
|
|
3688
|
-
* 补丁预览:这次工具调用真的改了哪个文件、改成什么样。
|
|
3713
|
+
* ## 为什么住在 protocol、且是冻住的中文
|
|
3689
3714
|
*
|
|
3690
|
-
*
|
|
3691
|
-
*
|
|
3692
|
-
*
|
|
3715
|
+
* 它的读者是**模型**,和 {@link contentToText} / {@link filePartSummary} 同一类,
|
|
3716
|
+
* 所以走同一条规矩:住在这个零依赖的包里、逐字冻住、**不跟着界面语言变**
|
|
3717
|
+
* (门禁在 `packages/protocol/__tests__/message-projection.test.ts`)。一条进了
|
|
3718
|
+
* 上下文的历史跟着 `EPOCH_LANGUAGE` 变语言,等于把同一段会话劈成两半。
|
|
3693
3719
|
*
|
|
3694
|
-
*
|
|
3695
|
-
*
|
|
3696
|
-
*
|
|
3697
|
-
*
|
|
3720
|
+
* 只有工具名,不带参数 / 路径:带路径要 `affectedPaths()` + 工作目录,而那两样
|
|
3721
|
+
* 住在 core 和装配层,拿不到的地方(`loadHistory()`)就会退化成另一种措辞 ——
|
|
3722
|
+
* 两条路产出的历史一旦不一样,症状就是「刷新之后模型看到的和刚才不是同一段」。
|
|
3723
|
+
*
|
|
3724
|
+
* ⚠️ **调用方要的多半是 {@link assistantTurnText},不是这一个** —— 那一份连
|
|
3725
|
+
* 「正文和这一行之间怎么接」一起定了,而那正是两条路会走散的地方。
|
|
3726
|
+
*
|
|
3727
|
+
* @param results 这**一轮**(不是一步)执行过的全部工具结果,按发生序
|
|
3728
|
+
* @returns 空数组时是空串 —— 这一轮没动手,那就没有这句话可说
|
|
3698
3729
|
*/
|
|
3699
|
-
|
|
3700
|
-
type: 'diff';
|
|
3701
|
-
filePath: string;
|
|
3702
|
-
oldContent?: string;
|
|
3703
|
-
newContent?: string;
|
|
3704
|
-
}
|
|
3705
|
-
/** 累计用量变化(每轮结束时发一次,宿主用来更新状态栏) */
|
|
3706
|
-
| {
|
|
3707
|
-
type: 'usage';
|
|
3708
|
-
cumulative: TokenUsage;
|
|
3709
|
-
/**
|
|
3710
|
-
* **到此刻为止、这一次 run** 的用量(方案 42 PR-2)。
|
|
3711
|
-
*
|
|
3712
|
-
* 和 `finish.runUsage` 同一笔账,只是这一份是**过程中**的快照 ——
|
|
3713
|
-
* 界面上「运行中:已消耗 N token」要的就是它。
|
|
3714
|
-
*
|
|
3715
|
-
* ⚠️ 为什么不让宿主拿 `cumulative` 相减:相减要有一个起算基线,而
|
|
3716
|
-
* 「run 开始那一刻的累计」在事件流上根本没有 —— 第一帧 `usage` 到达时,
|
|
3717
|
-
* 它的 `cumulative` 既可能是本轮用量、也可能是 `BudgetStore` 恢复的历史
|
|
3718
|
-
* 加上本轮,从流本身分辨不了。基线猜错的代价是屏幕上那个数直接差一个量级。
|
|
3719
|
-
*/
|
|
3720
|
-
run?: TokenUsage;
|
|
3721
|
-
budgetRemaining?: {
|
|
3722
|
-
costUsd?: number;
|
|
3723
|
-
tokens?: number;
|
|
3724
|
-
};
|
|
3725
|
-
}
|
|
3730
|
+
declare function turnDigest(results: readonly Pick<EpochToolResult, 'toolName'>[]): string;
|
|
3726
3731
|
/**
|
|
3727
|
-
*
|
|
3732
|
+
* 一轮的 assistant 消息在**给模型的历史**里长什么样:正文 + {@link turnDigest}。
|
|
3728
3733
|
*
|
|
3729
|
-
*
|
|
3730
|
-
* 没有它的话「触顶」在事件流上只是一句中文提示(`describeBreach()` 的产物),
|
|
3731
|
-
* 而机器宿主要分支就只能去正则匹配那句中文 —— 我们改一个字它就碎。
|
|
3734
|
+
* ## 为什么这条胶水也得住在这儿
|
|
3732
3735
|
*
|
|
3733
|
-
*
|
|
3736
|
+
* 因为它有**两个调用方,走两条路,产出必须逐字节相同**:
|
|
3734
3737
|
*
|
|
3735
|
-
*
|
|
3736
|
-
*
|
|
3737
|
-
* 「这个会话一共花了多少」。
|
|
3738
|
+
* - 直播:`runtime/src/agent-session.ts` 用 `TurnStepCollector` 收的那一轮;
|
|
3739
|
+
* - 恢复:`runtime/src/session-store.ts` 的 `loadHistory()` 从 DB 行折回来的那一轮。
|
|
3738
3740
|
*
|
|
3739
|
-
* `
|
|
3740
|
-
*
|
|
3741
|
-
*
|
|
3742
|
-
*
|
|
3743
|
-
* 从事件流本身分辨不了(`AgentLoopConfig.onTurnUsage` 那段注释骂的是同一件事)。
|
|
3741
|
+
* 两边各写一次 `text + '\n\n' + digest` 的下场是:某天一边多一个换行,
|
|
3742
|
+
* 而症状是**刷新页面之后模型看到的历史和刷新前不是同一段** —— `reloadHistory()`
|
|
3743
|
+
* 的 JSDoc 里那句「它们一旦分叉,症状就是屏幕上和模型看到的对不上」骂的正是这个。
|
|
3744
|
+
* 门禁是 `packages/runtime/__tests__/context-recency.test.ts` 里那条逐字节比对。
|
|
3744
3745
|
*
|
|
3745
|
-
*
|
|
3746
|
-
*
|
|
3747
|
-
*
|
|
3748
|
-
*
|
|
3746
|
+
* 正文为空、只有工具痕迹时**不留前导空行**:那一轮模型只调了工具没说话
|
|
3747
|
+
* (`loop.ts` 允许),这时这条消息的全部内容就是那一段。
|
|
3748
|
+
*
|
|
3749
|
+
* ⚠️ **断在半路那一轮走的是另一份投影**,判据全文在 {@link unfinishedTurnText} 上:
|
|
3750
|
+
* 收了尾 = 结论在正文里,工具只留一行名字;断在半路 = 工具结果是唯一的记录,
|
|
3751
|
+
* 得把正文一起带回去。
|
|
3752
|
+
*
|
|
3753
|
+
* @param finishReason 这一轮的收尾原因(取值域是 `FinishReason`)。
|
|
3754
|
+
* **缺省 = 没有记录**(老会话的 `finish_reason` 列是 NULL,嵌入宿主也可能
|
|
3755
|
+
* 不传)→ 一律按「收了尾」处理。往宽了判的代价是把工具正文焊进历史,
|
|
3756
|
+
* 而那是每一轮都要重发的东西。
|
|
3757
|
+
*
|
|
3758
|
+
* ⚠️ **收 `string` 而不是 `FinishReason`,这是刻意的。** 恢复那一条路是从
|
|
3759
|
+
* `messages.finish_reason` 读回来的,而它在库里就是一列 `TEXT`
|
|
3760
|
+
* (`core/src/session/db.ts`),读出来是 `string`。把这里收窄成联合类型,
|
|
3761
|
+
* 唯一的效果是逼 `loadHistory()` 写一次**没有依据的** cast ——
|
|
3762
|
+
* 那一列可以装着任何历史版本写进去的字符串,断言它属于今天这个联合
|
|
3763
|
+
* 是一句我们证明不了的话。宽收窄判:不认识的值一律走「收了尾」那一支。
|
|
3764
|
+
* @returns 空串 = 这一轮既没正文也没动手,调用方**别往历史里塞这一条**
|
|
3749
3765
|
*/
|
|
3750
|
-
|
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3766
|
+
declare function assistantTurnText(text: string, results: readonly Pick<EpochToolResult, 'toolName' | 'output'>[], finishReason?: string): string;
|
|
3767
|
+
/**
|
|
3768
|
+
* 部件类型的穷尽性守卫 —— **这是「加了一种部件却漏了一处分支」的那道会红的门禁**。
|
|
3769
|
+
*
|
|
3770
|
+
* 消费 `EpochContentPart` 的地方有五处(这里、`sdk-adapter` 的 `toSdkPart`、
|
|
3771
|
+
* `session/payload` 的 `persistParts` / `restoreParts`、以及展示层),改造前它们
|
|
3772
|
+
* 全都写成「if text / if file / 剩下的当图片」。那个形状下加一种部件**不会有
|
|
3773
|
+
* 任何东西变红**:新部件会被静默当成图片,而症状是模型收到一个 `image:` 里
|
|
3774
|
+
* 塞着会话正文的请求,provider 报 400。
|
|
3775
|
+
*/
|
|
3776
|
+
declare function assertNeverPart(part: never): never;
|
|
3777
|
+
type EpochMessageRole = 'system' | 'user' | 'assistant' | 'tool';
|
|
3778
|
+
/**
|
|
3779
|
+
* 一条会话消息。
|
|
3780
|
+
*
|
|
3781
|
+
* `content` 永远是字符串(tool 消息为空串,内容在 `toolResults` 里),
|
|
3782
|
+
* 这样「取这条消息的文本」永远不需要判类型。
|
|
3783
|
+
*
|
|
3784
|
+
* 多模态**不是**把 `content` 放宽成联合类型 —— 那正是本文件开头骂过的病,
|
|
3785
|
+
* 会让 loop / codec / 压缩器每处都得判一次类型。改成加一个 `parts` 旁路:
|
|
3786
|
+
* `content` 仍然是权威的**文本投影**(压缩器、审计、持久化都只看它),
|
|
3787
|
+
* `parts` 只在 sdk-adapter 那一处被读走。两者的关系由 `contentToText` 保证。
|
|
3788
|
+
*/
|
|
3789
|
+
interface EpochMessage {
|
|
3790
|
+
role: EpochMessageRole;
|
|
3791
|
+
content: string;
|
|
3756
3792
|
/**
|
|
3757
|
-
*
|
|
3758
|
-
*
|
|
3759
|
-
* ⚠️ 不是配置里选的那个。降级和 `switch_model` 之后两者不一样,而
|
|
3760
|
-
* 「这一轮到底是谁答的」正是那时候用户最需要知道的一件事。
|
|
3793
|
+
* 多模态部件。给了就以它为准喂模型,`content` 退化成它的文本投影。
|
|
3794
|
+
* **不落 SQLite** —— 图片走 artifact 目录,DB 里只留 content 的占位文本。
|
|
3761
3795
|
*/
|
|
3762
|
-
|
|
3796
|
+
parts?: EpochContentPart[];
|
|
3797
|
+
/** assistant 要求的工具调用;与 content 并存(模型可以边说话边调工具) */
|
|
3798
|
+
toolCalls?: EpochToolCall[];
|
|
3799
|
+
/** role 为 'tool' 时携带的执行结果 */
|
|
3800
|
+
toolResults?: EpochToolResult[];
|
|
3763
3801
|
/**
|
|
3764
|
-
*
|
|
3802
|
+
* 这一步模型的思考原文(推理模型的 `reasoning-delta` 累加)。2026-08-18。
|
|
3765
3803
|
*
|
|
3766
|
-
*
|
|
3767
|
-
*
|
|
3804
|
+
* **只给人看,不回灌模型**:`sdk-adapter.ts` 一个字都不读它。加它是因为
|
|
3805
|
+
* 刷新页面之前屏幕上有「深度思考」那一段,刷新之后整段消失 —— 而那不是
|
|
3806
|
+
* 「这一轮没想」,是我们没记。
|
|
3768
3807
|
*
|
|
3769
|
-
* ⚠️
|
|
3770
|
-
*
|
|
3771
|
-
*
|
|
3772
|
-
*
|
|
3773
|
-
* `
|
|
3808
|
+
* ⚠️ **它属于某一步,不属于整轮。** 一轮里模型可以想一次、调个工具、再想一次,
|
|
3809
|
+
* 每次的思考挂在**它自己那条 assistant 行**上,位置就是它真实的时序位置。
|
|
3810
|
+
* 拿它当「整轮的思考」拼成一根,回放时那一段会被钉到最前面,讲的却可能是
|
|
3811
|
+
* 二十步之后的事(web 的 `thoughtParts` 治过同一个病,判据在
|
|
3812
|
+
* `web/src/state/reducer.ts` 的 `trackThought()`)。
|
|
3813
|
+
*
|
|
3814
|
+
* 落盘走 `messages.reasoning` 列 —— 那一列**从会话库 v1 就在**,只是一直没人
|
|
3815
|
+
* 写过,所以这一档不需要迁移。
|
|
3774
3816
|
*/
|
|
3775
|
-
|
|
3776
|
-
breach?: BudgetBreach;
|
|
3817
|
+
reasoning?: string;
|
|
3777
3818
|
}
|
|
3819
|
+
|
|
3778
3820
|
/**
|
|
3779
|
-
*
|
|
3821
|
+
* 结构化提问契约(方案 34)—— 模型遇到分叉时能问一句,而不是瞎猜。
|
|
3780
3822
|
*
|
|
3781
|
-
* ##
|
|
3823
|
+
* ## 它和审批是两件事
|
|
3782
3824
|
*
|
|
3783
|
-
*
|
|
3784
|
-
*
|
|
3785
|
-
*
|
|
3825
|
+
* 审批问的是「**允不允许**我做这件事」,答案是四个固定出口,且**默认往严的方向落**
|
|
3826
|
+
* (走开 = 拒绝)。提问问的是「**你想要哪一种**」,答案是内容,而且
|
|
3827
|
+
* **没有安全的默认值** —— 替用户挑一个和替他点「允许」一样糟,只是失败的方式不同:
|
|
3828
|
+
* 前者让模型基于一个假答案干半小时活。
|
|
3786
3829
|
*
|
|
3787
|
-
*
|
|
3788
|
-
*
|
|
3789
|
-
* 等于往上下文里塞一条它没说过的发言;
|
|
3790
|
-
* 2. **往 system prompt 里塞** —— 那是 prompt cache 的前缀,每轮加一句会让整段
|
|
3791
|
-
* 对话的缓存逐轮失效。同一条判据 `loop.ts` 里为后台任务通知讲过一遍;
|
|
3792
|
-
* 3. **一条 `[系统]` 进历史**(`systemNote()` + `recordNote()`,样板是
|
|
3793
|
-
* `runtime` 的 `markRoleHandoff()`)—— 它是真出口,但那句话**只在刷新 /
|
|
3794
|
-
* 回放时看得见**,而且今天只有 web 认那个前缀,TUI 会把 `[系统] ` 原样露出来。
|
|
3830
|
+
* 所以这里刻意**不提供**任何「宿主答不上来时的兜底答案」。答不上来只有一种表达:
|
|
3831
|
+
* `skipped: true`(见 {@link QuestionAnswer})。
|
|
3795
3832
|
*
|
|
3796
|
-
* ##
|
|
3797
|
-
*
|
|
3798
|
-
* `text` 由**发射方**(core,它有 `t()`)翻好:`@epoch-agent/view` 只依赖
|
|
3799
|
-
* protocol、够不着 catalog,在那儿兜底就只能是中文 —— 同 `error` 那条空 message
|
|
3800
|
-
* 的兜底为什么落在两个宿主身上,是同一条判据。
|
|
3833
|
+
* ## 为什么不过权限层
|
|
3801
3834
|
*
|
|
3802
|
-
*
|
|
3803
|
-
*
|
|
3835
|
+
* 提问不碰文件、不起进程、不联网。方案 34 §三把 `core/src/permission/**`
|
|
3836
|
+
* 列进禁止触碰,就是这个意思 —— 给它加一道权限判定,等于让 `deny` 规则能把
|
|
3837
|
+
* 「问一句」拦掉,而拦掉的表现是模型继续瞎猜。
|
|
3838
|
+
*/
|
|
3839
|
+
/** 一个选项。**没有「其他」** —— 自由输入由宿主自动提供,不占模型的选项额度 */
|
|
3840
|
+
interface QuestionOption {
|
|
3841
|
+
/** 选项正文,用户看到的就是它,模型收到的答案也是它 */
|
|
3842
|
+
label: string;
|
|
3843
|
+
/** 这个选项意味着什么 / 选了会发生什么 */
|
|
3844
|
+
description: string;
|
|
3845
|
+
}
|
|
3846
|
+
/** 一个问题 */
|
|
3847
|
+
interface QuestionItem {
|
|
3848
|
+
/** 完整问句,带问号 */
|
|
3849
|
+
question: string;
|
|
3850
|
+
/** 极短标签,UI 上当 chip 用(≤ {@link MAX_HEADER_CHARS} 字) */
|
|
3851
|
+
header: string;
|
|
3852
|
+
/** {@link MIN_OPTIONS}–{@link MAX_OPTIONS} 个选项 */
|
|
3853
|
+
options: QuestionOption[];
|
|
3854
|
+
/** 多选(答案是数组)还是单选(答案是一个字符串) */
|
|
3855
|
+
multiSelect: boolean;
|
|
3856
|
+
}
|
|
3857
|
+
/**
|
|
3858
|
+
* 一次结构化提问。
|
|
3804
3859
|
*
|
|
3805
|
-
*
|
|
3806
|
-
*
|
|
3860
|
+
* **刻意没有 `requestId`**(方案初稿里有)。跨进程时的凭据由
|
|
3861
|
+
* `runtime/src/serialize.ts` 的 `toSerializable()` 生成,和 `approval-request`
|
|
3862
|
+
* 逐字同构 —— 请求内容里再带一个 id 的话,进程内的宿主(TUI)手上会有一个
|
|
3863
|
+
* 它永远不用的字段,而跨进程那侧有两个候选 id,对不齐时没人说得清该信哪个。
|
|
3807
3864
|
*/
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
text: string;
|
|
3865
|
+
interface QuestionRequest {
|
|
3866
|
+
/** 一次最多 {@link MAX_QUESTIONS} 个问题(超过就该拆成两轮对话了) */
|
|
3867
|
+
questions: QuestionItem[];
|
|
3812
3868
|
}
|
|
3813
|
-
/** 运行中出错(随后会有一个 finish: 'error') */
|
|
3814
|
-
| {
|
|
3815
|
-
type: 'error';
|
|
3816
|
-
message: string;
|
|
3817
|
-
};
|
|
3818
3869
|
/**
|
|
3819
|
-
*
|
|
3870
|
+
* 宿主的答复。
|
|
3820
3871
|
*
|
|
3821
|
-
*
|
|
3822
|
-
*
|
|
3872
|
+
* `answers` 的键是 {@link QuestionItem.question} 原文 —— 不是下标。模型写出问句
|
|
3873
|
+
* 的时候就知道自己在问什么,拿问句当键它读答案不用再对一次序;而下标一旦和
|
|
3874
|
+
* 宿主的呈现顺序错开(多问题逐个走完再一起回),错位是静默的。
|
|
3823
3875
|
*
|
|
3824
|
-
*
|
|
3825
|
-
*
|
|
3826
|
-
*
|
|
3876
|
+
* `skipped` 是方案 §2.4 那条「用户 Esc → 模型自己决定,整轮不失败」在类型上的
|
|
3877
|
+
* 落点。它和「`answers` 是空对象」**不等价**:用户答完前两问才按 Esc 时,
|
|
3878
|
+
* 前两个答案照样交出去,同时 `skipped: true` 告诉模型「剩下的没答上,别当成
|
|
3879
|
+
* 用户选了默认」。
|
|
3827
3880
|
*/
|
|
3828
|
-
|
|
3881
|
+
interface QuestionAnswer {
|
|
3882
|
+
/** question 原文 → 选中的 label(多选是数组)。用户自由输入时是他打的原文 */
|
|
3883
|
+
answers: Record<string, string | string[]>;
|
|
3884
|
+
/** 用户中途放弃(Esc / 关掉弹窗)。已答的那几个仍在 `answers` 里 */
|
|
3885
|
+
skipped?: boolean;
|
|
3886
|
+
}
|
|
3829
3887
|
/**
|
|
3830
|
-
*
|
|
3888
|
+
* 一次最多几个问题。
|
|
3831
3889
|
*
|
|
3832
|
-
*
|
|
3833
|
-
*
|
|
3834
|
-
* 第二层意思:那一轮正是这条技能**真的进了 system prompt** 的那一轮。
|
|
3890
|
+
* 超过就该拆成两轮对话 —— 四个连续弹窗已经是用户能忍的上限,而模型一次问八个
|
|
3891
|
+
* 通常意味着它没读上下文。工具会**拒绝**并把这个上限说给模型(验收 5)。
|
|
3835
3892
|
*/
|
|
3836
|
-
|
|
3893
|
+
declare const MAX_QUESTIONS = 4;
|
|
3894
|
+
/** 每个问题的选项数下界。一个选项不是问题,是通知 */
|
|
3895
|
+
declare const MIN_OPTIONS = 2;
|
|
3896
|
+
/** 每个问题的选项数上界。超过四个用户就开始扫而不是读了 */
|
|
3897
|
+
declare const MAX_OPTIONS = 4;
|
|
3898
|
+
/** `header` 的长度上界。它是 UI 上的 chip,长了会把弹窗撑破 */
|
|
3899
|
+
declare const MAX_HEADER_CHARS = 12;
|
|
3837
3900
|
/**
|
|
3838
|
-
*
|
|
3839
|
-
*
|
|
3840
|
-
* 和 `agent_run.images_unsupported` 那条错误分得很清,两者别混:
|
|
3841
|
-
*
|
|
3842
|
-
* | | 谁判的 | 什么时候 | 钱 |
|
|
3843
|
-
* | --- | --- | --- | --- |
|
|
3844
|
-
* | `images_unsupported`(`error` 帧) | **我们**(闸门 1 的输入侧) | 发请求**之前** | 没花 |
|
|
3845
|
-
* | 这一条(`notice` 帧) | **provider 自己报的 warning** | 请求回来之后 | **已经花了** |
|
|
3901
|
+
* 收窄**外部来的**答复载荷。
|
|
3846
3902
|
*
|
|
3847
|
-
*
|
|
3848
|
-
*
|
|
3849
|
-
*
|
|
3903
|
+
* 需要它的是跨进程的宿主(headless 的 `question-response`、`epoch web` 的 REST):
|
|
3904
|
+
* 那条路上答复是一段 JSON,不过校验直接喂给引擎等于让宿主随手拼个
|
|
3905
|
+
* `{"answers": 3}` 就让模型读到一个数字。进程内的宿主由类型保证,用不到它。
|
|
3850
3906
|
*
|
|
3851
|
-
*
|
|
3852
|
-
*
|
|
3907
|
+
* 和 `isApprovalOutcome` 同一条理由住在 protocol:判据只有一处,
|
|
3908
|
+
* 两个出口(stdin / REST)各写一遍就会有一边漏掉数组那一种形态。
|
|
3853
3909
|
*/
|
|
3854
|
-
|
|
|
3910
|
+
declare function isQuestionAnswerMap(value: unknown): value is Record<string, string | string[]>;
|
|
3911
|
+
|
|
3855
3912
|
/**
|
|
3856
|
-
*
|
|
3857
|
-
*
|
|
3858
|
-
* ## 没有这一帧的话,这件事是**静默**的
|
|
3859
|
-
*
|
|
3860
|
-
* `mask` 档不拒绝、也不报错:它把命中处换成 `*` 之后照常发出去。而屏幕上那条
|
|
3861
|
-
* 用户气泡显示的是**用户自己敲的原文**(宿主回显的是输入框里那份),
|
|
3862
|
-
* 模型收到的是打过码的那份 —— 两边不一样,且没有任何东西说过这件事。
|
|
3863
|
-
*
|
|
3864
|
-
* 实测过:`帮我翻译一下黑鬼这个词` 发出去变成 `帮我翻译一下**这个词`。
|
|
3865
|
-
* 用户看着自己那句完整的话,收到一个答非所问的回复,**没有任何线索**。
|
|
3866
|
-
* 这一帧就是那个线索。
|
|
3913
|
+
* Agent 事件契约 —— 引擎 → 宿主(TUI / CLI / 未来的 server)的唯一协议。
|
|
3867
3914
|
*
|
|
3868
|
-
*
|
|
3869
|
-
*
|
|
3870
|
-
*
|
|
3915
|
+
* 为什么是判别联合而不是「一个 type 字段 + 一堆可选字段」:
|
|
3916
|
+
* 旧的 StreamChunk / StreamEvent 是后者,所有载荷字段都是 optional,于是
|
|
3917
|
+
* - 三个包各写一份、字段名还不一样(input/args、success/isError),
|
|
3918
|
+
* 靠 cli 里一段手写适配缝合;漏一个字段编译器不报错,只是界面上少显示东西。
|
|
3919
|
+
* - 协议里没有 callId,TUI 只能靠「toolName + 调用顺序 FIFO」猜工具结果
|
|
3920
|
+
* 属于哪次调用,并行工具就会串台。
|
|
3921
|
+
* 判别联合让每个事件的必填字段由编译器保证,callId 成为一等字段。
|
|
3871
3922
|
*/
|
|
3872
|
-
|
|
3923
|
+
|
|
3873
3924
|
/**
|
|
3874
|
-
*
|
|
3925
|
+
* Token 用量。
|
|
3875
3926
|
*
|
|
3876
|
-
*
|
|
3877
|
-
*
|
|
3878
|
-
* `finish_reason` 列、web 那枚记号),这一帧是给**人**看的那句话 ——
|
|
3879
|
-
* 屏幕上那半截正文自己不会说「我是半截的」。
|
|
3927
|
+
* 之前 protocol 侧和 core/context/types.ts 各有一个 TokenUsage,
|
|
3928
|
+
* 后者多两个字段——同名不同义,收成一个超集。
|
|
3880
3929
|
*
|
|
3881
|
-
*
|
|
3882
|
-
* 它不是模型说的话,塞进正文就会进历史、进落盘、下一轮再进模型。
|
|
3883
|
-
*/
|
|
3884
|
-
| 'response-truncated'
|
|
3885
|
-
/**
|
|
3886
|
-
* 这一条消息发出去之前,**上下文被自动压了一次**(2026-09-10)。
|
|
3930
|
+
* ## 五个 token 字段的口径(方案 42 PR-0)
|
|
3887
3931
|
*
|
|
3888
|
-
*
|
|
3932
|
+
* **两种关系并存,别一视同仁地相加**:
|
|
3889
3933
|
*
|
|
3890
|
-
*
|
|
3891
|
-
*
|
|
3892
|
-
*
|
|
3893
|
-
*
|
|
3934
|
+
* | 字段 | 与谁的关系 |
|
|
3935
|
+
* | ------------------------------- | --------------------------- |
|
|
3936
|
+
* | `cacheHitTokens` `cacheWriteTokens` | 与 `inputTokens` **不重叠** |
|
|
3937
|
+
* | `reasoningTokens` | **包含在** `outputTokens` 里 |
|
|
3938
|
+
* | `totalTokens` | 输入总量 + 输出总量 |
|
|
3894
3939
|
*
|
|
3895
|
-
*
|
|
3896
|
-
*
|
|
3940
|
+
* 所以「一共多少 token」= `inputTokens + outputTokens + cacheHit + cacheWrite`,
|
|
3941
|
+
* **不加** `reasoningTokens`。计价同理,见 `core/src/cost/pricing.ts` 文件头。
|
|
3897
3942
|
*
|
|
3898
|
-
*
|
|
3899
|
-
* `
|
|
3900
|
-
*
|
|
3901
|
-
* 用户要知道的是同一件事,而「哪道闸判的」是我们的实现细节。
|
|
3943
|
+
* 这个口径不是我们凭空定的,是 AI SDK v7 那边就这么分的:
|
|
3944
|
+
* `inputTokenDetails.noCacheTokens + cacheReadTokens + cacheWriteTokens
|
|
3945
|
+
* === inputTokens`,而 `outputTokenDetails.textTokens = outputTokens - reasoning`。
|
|
3902
3946
|
*/
|
|
3903
|
-
|
|
3947
|
+
interface TokenUsage {
|
|
3948
|
+
/**
|
|
3949
|
+
* **非缓存**的输入 token,即 SDK 的 `inputTokenDetails.noCacheTokens`。
|
|
3950
|
+
*
|
|
3951
|
+
* ⚠️ 不是 SDK 的 `usage.inputTokens`(那个是含缓存的输入总量)。填总量会让
|
|
3952
|
+
* 缓存命中被按原价算第二遍 —— 缓存读通常只要原价的 10%,这两个数差一个量级。
|
|
3953
|
+
*/
|
|
3954
|
+
inputTokens: number;
|
|
3955
|
+
outputTokens: number;
|
|
3956
|
+
/** 缓存命中读到的输入 token,与 `inputTokens` 不重叠,单独计价 */
|
|
3957
|
+
cacheHitTokens?: number;
|
|
3958
|
+
/** 写入缓存的输入 token,与 `inputTokens` 不重叠,单独计价 */
|
|
3959
|
+
cacheWriteTokens?: number;
|
|
3960
|
+
/**
|
|
3961
|
+
* 推理模型的思考 token(SDK 的 `outputTokenDetails.reasoningTokens`)。
|
|
3962
|
+
*
|
|
3963
|
+
* ⚠️ 它**包含在 `outputTokens` 里**,和上面两个缓存字段的口径正好相反。
|
|
3964
|
+
* 只用来展示「这一轮想了多少」,**不许再加进总数、也不单独计价**,
|
|
3965
|
+
* 否则输出侧被算两遍。provider 不是推理模型 / 不上报时为 undefined。
|
|
3966
|
+
*/
|
|
3967
|
+
reasoningTokens?: number;
|
|
3968
|
+
/**
|
|
3969
|
+
* SDK 报的总数:输入总量(含缓存两项)+ 输出总量。
|
|
3970
|
+
*
|
|
3971
|
+
* 要显示「一共多少 token」优先用这个,自己四项相加的只当兜底 ——
|
|
3972
|
+
* 前者是 provider 报的两个总量直接相加,后者要先经过我们的拆分算术
|
|
3973
|
+
* (`inputTokens` 取的是 noCache),多一层就多一个出错的地方。
|
|
3974
|
+
* provider 没上报用量时为 undefined。
|
|
3975
|
+
*/
|
|
3976
|
+
totalTokens?: number;
|
|
3977
|
+
/**
|
|
3978
|
+
* 本次用量折算的美元金额。模型没有定价数据时为 undefined,**不要填 0**。
|
|
3979
|
+
*
|
|
3980
|
+
* 0 意味着「免费」,undefined 意味着「不知道」。预算闸门对这两种情况的处理
|
|
3981
|
+
* 必须不同——未知定价不能当免费放行。
|
|
3982
|
+
*/
|
|
3983
|
+
costUsd?: number;
|
|
3984
|
+
}
|
|
3904
3985
|
/**
|
|
3905
|
-
*
|
|
3906
|
-
*
|
|
3907
|
-
* ## 为什么这一次探测必须说出来
|
|
3908
|
-
*
|
|
3909
|
-
* 它是**用户没要求过的一次额外请求**:多一趟往返(约两秒)、多一笔约 225 token
|
|
3910
|
-
* 的账。屏幕上那段静默否则解释不了 —— 用户发了张图,等了两秒,什么都没有。
|
|
3911
|
-
*
|
|
3912
|
-
* 判据同上面几条:多花的钱和多等的时间,只要不是用户自己点的,就欠一句旁白。
|
|
3986
|
+
* 预算触顶的原因。
|
|
3913
3987
|
*
|
|
3914
|
-
*
|
|
3915
|
-
*
|
|
3916
|
-
*
|
|
3988
|
+
* 住在 protocol 而不是 `core/src/cost/types.ts`:它出现在 `finish` 事件上,
|
|
3989
|
+
* 于是**跨进程的宿主也要认它** —— headless 的 `result.reason` 要靠它区分
|
|
3990
|
+
* 「这个月的额度用完了」和「这一次调用的 `--max-budget-usd` 到了」。
|
|
3991
|
+
* core 那边 re-export 这一份,不另立一个同名类型。
|
|
3992
|
+
*/
|
|
3993
|
+
type BudgetBreach =
|
|
3994
|
+
/** 金额超过 `budget.maxCostUsd`(跨会话累计) */
|
|
3995
|
+
'cost'
|
|
3996
|
+
/** token 数超过 `budget.maxTokens`(跨会话累计) */
|
|
3997
|
+
| 'tokens'
|
|
3998
|
+
/** 模型无定价数据,且 `budget.onUnknownPricing` 为 `'block'` */
|
|
3999
|
+
| 'unknown-pricing'
|
|
4000
|
+
/**
|
|
4001
|
+
* **本次调用**的金额上限到了(`RunInput.maxCallCostUsd` / `--max-budget-usd`)。
|
|
3917
4002
|
*
|
|
3918
|
-
*
|
|
3919
|
-
*
|
|
3920
|
-
*
|
|
4003
|
+
* 和 `'cost'` 分开是因为处理方式不同:`'cost'` 意味着用户配的那份长期预算见底了,
|
|
4004
|
+
* 要么等下个周期要么调高上限;`'call-cost'` 只是这一次跑得比预期贵,
|
|
4005
|
+
* 换个说法再问一次就好。CI 的退出码也据此分叉。
|
|
3921
4006
|
*/
|
|
3922
|
-
| '
|
|
4007
|
+
| 'call-cost';
|
|
4008
|
+
/** 一次运行的收尾原因 */
|
|
4009
|
+
type FinishReason =
|
|
4010
|
+
/** 模型正常收工 */
|
|
4011
|
+
'stop'
|
|
4012
|
+
/** 用尽轮次预算 */
|
|
4013
|
+
| 'max-turns'
|
|
4014
|
+
/** 死循环检测触发 —— 连续多轮重复同一个工具调用 */
|
|
4015
|
+
| 'stalled'
|
|
3923
4016
|
/**
|
|
3924
|
-
*
|
|
4017
|
+
* 模型在**一次回复内**开始复读,被我们掐掉了(core 的 `repetition-detector.ts`)。
|
|
3925
4018
|
*
|
|
3926
|
-
*
|
|
4019
|
+
* ⚠️ **和 `'stalled'` 刻意分开。** 那一档是「连续多轮调同一个工具」,
|
|
4020
|
+
* 用户能做的是换个说法或者自己接手;这一档整轮工具调用为零,是解码退化,
|
|
4021
|
+
* 处置是重发一次(大概率就好了)。混成一个值之后,界面上只能给出一句
|
|
4022
|
+
* 同时对两种情况都不准的话。
|
|
3927
4023
|
*
|
|
3928
|
-
*
|
|
3929
|
-
*
|
|
3930
|
-
*
|
|
3931
|
-
*
|
|
3932
|
-
|
|
4024
|
+
* ⚠️ **也不是 `'length'`。** 那一档是 provider 按 max output tokens 切的、
|
|
4025
|
+
* 那半截正文是模型真想说的;这一档是**我们**在它撞上上限之前主动停的,
|
|
4026
|
+
* 已经产出的正文留着但没有价值。两者在账上差一整个输出预算 ——
|
|
4027
|
+
* 现场那一份是 31.6 KB 里只有 12 种句子。
|
|
4028
|
+
*/
|
|
4029
|
+
| 'looping'
|
|
4030
|
+
/** 被宿主中止 */
|
|
4031
|
+
| 'aborted'
|
|
4032
|
+
/** 模型安全策略拒答 —— 是 **provider** 拒答,不是我们拦的 */
|
|
4033
|
+
| 'content-filter'
|
|
4034
|
+
/**
|
|
4035
|
+
* 我们自己的内容合规闸门拦下了(core 的 `compliance/`)。
|
|
3933
4036
|
*
|
|
3934
|
-
*
|
|
3935
|
-
*
|
|
3936
|
-
*
|
|
4037
|
+
* ⚠️ **刻意不复用 `content-filter`。** 那个值的含义是「provider 拒答了」,
|
|
4038
|
+
* 处置和说法都不一样:那一档用户能做的是换个说法再问,这一档是内容触了
|
|
4039
|
+
* 部署方配的合规红线(词表 + 类别档位),换说法不一定有用,而且该看的是
|
|
4040
|
+
* 「哪个类别」。混成一个值之后,界面上只能给出一句同时对两种情况都不准的话。
|
|
3937
4041
|
*
|
|
3938
|
-
*
|
|
4042
|
+
* 输入侧被拦下时也是这个值 —— 那一次**请求根本没发出去**(`usage` 全零),
|
|
4043
|
+
* 与输出侧被拦下的区别在文本里说清,不在这个枚举里分叉。
|
|
4044
|
+
*/
|
|
4045
|
+
| 'compliance'
|
|
4046
|
+
/**
|
|
4047
|
+
* provider 把这一轮的回复**按 max output tokens 截断了**(2026-09-04)。
|
|
3939
4048
|
*
|
|
3940
|
-
*
|
|
3941
|
-
* 能力检查**),以及 `/model` 切到一个能力**查不到**的型号
|
|
3942
|
-
* (`provider/selection.ts` 那条硬拒绝只挡查得到不认图的)。两条路上用户都
|
|
3943
|
-
* 没有再发图,于是屏幕上不会有任何东西提到图 —— 而模型接下来那句
|
|
3944
|
-
* 「你发的那张截图我看不到」会像是它在犯病。
|
|
4049
|
+
* ## 为什么必须和 `'stop'` 分开
|
|
3945
4050
|
*
|
|
3946
|
-
*
|
|
3947
|
-
*
|
|
4051
|
+
* 分不开的时候,「模型自己收的」和「话没说完就被切了」在事后**无法区分** ——
|
|
4052
|
+
* 两者在界面上长得一模一样(都是「这一轮没有工具调用」)。而这是所有
|
|
4053
|
+
* 「模型半路停下」类问题的**第一个分诊问题**:一句
|
|
4054
|
+
* 「我先确认苏菲店的信息,再拉昨日数据。」+ 0 个工具调用,到底是模型宣布了
|
|
4055
|
+
* 下一步就收工(该由 `verify:stop` 那道闸救),还是它正要发工具调用时
|
|
4056
|
+
* 被截断(该调高 max output tokens)—— 处置完全相反。
|
|
4057
|
+
*
|
|
4058
|
+
* 实测过一次:`sessions.db` 里 687 条消息的 `finish_reason` 列**全是 NULL**,
|
|
4059
|
+
* 而 `AgentLoop` 当时只判了 `'content-filter'`,没有任何一处判 `'length'`。
|
|
4060
|
+
* 那一轮到底是哪一种,事后答不出来。
|
|
4061
|
+
*
|
|
4062
|
+
* ⚠️ **这一档不是「出错了」。** 请求成功、已经计费、那半截正文是真的,
|
|
4063
|
+
* 只是它不完整。宿主该说的话是「回复被截断了」而不是「失败了」。
|
|
3948
4064
|
*/
|
|
3949
|
-
| '
|
|
3950
|
-
type AgentEventType = AgentEvent['type'];
|
|
3951
|
-
/** 取出某个具体事件的类型,例如 AgentEventOf<'tool-call'> */
|
|
3952
|
-
type AgentEventOf<T extends AgentEventType> = Extract<AgentEvent, {
|
|
3953
|
-
type: T;
|
|
3954
|
-
}>;
|
|
3955
|
-
|
|
4065
|
+
| 'length'
|
|
3956
4066
|
/**
|
|
3957
|
-
*
|
|
4067
|
+
* 这一轮**干了活却什么都没说**:零工具调用收尾,而正文是空的(或只有几个字),
|
|
4068
|
+
* 而且踢回去重答一次之后还是这样(2026-09-16)。
|
|
3958
4069
|
*
|
|
3959
|
-
*
|
|
3960
|
-
* 工具、技能和生命周期钩子。插件只需依赖本包(@epoch-agent/protocol),
|
|
3961
|
-
* 不依赖引擎实现。
|
|
4070
|
+
* ## 为什么必须和 `'stop'` 分开
|
|
3962
4071
|
*
|
|
3963
|
-
*
|
|
3964
|
-
*
|
|
3965
|
-
* -
|
|
4072
|
+
* 分不开的时候,它和「模型正常收工」在事后**和当场都**无法区分 —— 两边都是
|
|
4073
|
+
* 「这一轮没有工具调用」+ 一个 `finish_reason` 说 `stop`。现场是
|
|
4074
|
+
* `deepseek-flash` 调了 8 次工具、烧掉 289.2k token,最后正文只有「阿飞」
|
|
4075
|
+
* 两个字;同一个库里第二例正文是**空串**,reasoning 只有一个 `The`。
|
|
4076
|
+
* 两条都被记成了成功收工:`hooks.fire('stop')` 照发、`maybeLearn()` 照跑、
|
|
4077
|
+
* headless 的 `ok` 照给。
|
|
4078
|
+
*
|
|
4079
|
+
* ⚠️ **这一档和 `'length'` 不是一回事。** 那一档 provider 亲口说了它切了
|
|
4080
|
+
* (`finishReason === 'length'`),处置是调高 max output tokens;这一档
|
|
4081
|
+
* provider 说的是 `stop` —— 它认为自己正常说完了,而说出来的是空气。
|
|
4082
|
+
* 实测那两例的账:`outputTokens − reasoningTokens` 减掉工具调用 JSON 之后
|
|
4083
|
+
* 残差是 −61 / −58 token(估计噪声 ±484),也就是**正文真的没生成**,
|
|
4084
|
+
* 不是生成了没落库。
|
|
4085
|
+
*
|
|
4086
|
+
* ⚠️ **这一档不是「出错了」。** 请求成功、已经计费。宿主该说的是
|
|
4087
|
+
* 「这一轮没有产出回复」,而不是「失败了」—— 但它**也不是成功**,
|
|
4088
|
+
* 所以 headless 的 `ok` 和调度的 `ok` 都不该给(判据在
|
|
4089
|
+
* `runtime/src/schedule/executor.ts` 的 `FINISH_STATUS` 上)。
|
|
4090
|
+
*/
|
|
4091
|
+
| 'blank'
|
|
4092
|
+
/** 触达成本或 token 预算上限 */
|
|
4093
|
+
| 'budget-exceeded'
|
|
4094
|
+
/** 异常终止(详情见前一个 error 事件) */
|
|
4095
|
+
| 'error';
|
|
4096
|
+
type AgentEvent =
|
|
4097
|
+
/**
|
|
4098
|
+
* 这一轮真的要发出去了 —— **决定 8 的第一个时刻**(方案 42 §七,2026-08-15)。
|
|
4099
|
+
*
|
|
4100
|
+
* ## 发它的是谁
|
|
4101
|
+
*
|
|
4102
|
+
* core 的 `agent/loop.ts`,在**第一次模型请求之前**、且**在预算闸门之后**。
|
|
4103
|
+
* 位置是有判据的:闸门拦下的那一次请求根本没发出去,给它发一条「这一轮开跑了」
|
|
4104
|
+
* 是在宣布一件没发生的事(events.ts 上一条 ⚠️ 骂的正是「声明了却没有发射方」,
|
|
4105
|
+
* 这一条反过来 —— 别发一件没发生的)。
|
|
4106
|
+
*
|
|
4107
|
+
* 一次 `run()` 最多一条:它是「这一轮」的头,不是「这一步」的。
|
|
4108
|
+
* 收尾那条 `finish` 是它的另一半,两者一头一尾夹住同一段。
|
|
4109
|
+
*
|
|
4110
|
+
* ## `estimatedTokens` 是**估算**,而且永远是
|
|
4111
|
+
*
|
|
4112
|
+
* 它是那一刻手里那份完整待发送载荷(system prompt + 工具定义 + 全部消息)
|
|
4113
|
+
* 过一遍 `estimateTokens()` 的结果。2026-08-15 换了真分词器(o200k_base)之后
|
|
4114
|
+
* 这个数**说得出真话**了 —— 在那之前 `chars/4` 在中文场景系统性低估 2–2.7 倍。
|
|
4115
|
+
*
|
|
4116
|
+
* ⚠️ **换了真分词器不等于它变成了事实**,别把这两句话弄反:
|
|
4117
|
+
*
|
|
4118
|
+
* 1. 分词器是 o200k_base,而模型可能是 Claude / DeepSeek,各家 BPE 不同 ——
|
|
4119
|
+
* 这是 `provider/tokenizer.ts` 文件头明写的「有意的近似」;
|
|
4120
|
+
* 2. 更硬的一条:这个数是**发之前**算的,而真值只有 provider 回的 usage 说了算,
|
|
4121
|
+
* 中途还会插进压缩、任务通知、工具结果。
|
|
4122
|
+
*
|
|
4123
|
+
* 所以界面上那枚「估算」徽标**不许摘**,判据同 `design/web-ui/thread.css`
|
|
4124
|
+
* 里 `.est` 那条:token 的三个时刻里只有运行前这一个是猜的。
|
|
4125
|
+
*
|
|
4126
|
+
* ## 为什么不复用 `usage` 事件
|
|
4127
|
+
*
|
|
4128
|
+
* `usage` 上两个数都来自 provider 的实测回报(`cumulative` / `run`),
|
|
4129
|
+
* 往里塞一个猜出来的数,就得让每个消费方去判「这个字段是真的还是估的」——
|
|
4130
|
+
* 而那正是徽标要替用户回答的问题。两笔账两条事件,别合。
|
|
3966
4131
|
*/
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
4132
|
+
{
|
|
4133
|
+
type: 'run-start';
|
|
4134
|
+
/**
|
|
4135
|
+
* 这一轮由哪个角色在跑。**顶层会话没指定角色时整个键缺席**,
|
|
4136
|
+
* 不是给一个 `general` —— 「没选专家」在决定 20 ② 里是一个有形状的值
|
|
4137
|
+
* (底栏没有 chip),编一个默认名等于把那个值抹掉。
|
|
4138
|
+
*/
|
|
4139
|
+
role?: ActiveAgentRole;
|
|
4140
|
+
/** 运行前估算。见上面那两条为什么它永远带「估算」二字 */
|
|
4141
|
+
estimatedTokens: number;
|
|
3976
4142
|
}
|
|
3977
|
-
/**
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
4143
|
+
/** 回复正文增量 */
|
|
4144
|
+
| {
|
|
4145
|
+
type: 'text-delta';
|
|
4146
|
+
text: string;
|
|
4147
|
+
}
|
|
4148
|
+
/** 推理模型的思考过程增量 */
|
|
4149
|
+
| {
|
|
4150
|
+
type: 'reasoning-delta';
|
|
4151
|
+
text: string;
|
|
4152
|
+
}
|
|
4153
|
+
/** 模型请求调用工具(执行前) */
|
|
4154
|
+
| {
|
|
4155
|
+
type: 'tool-call';
|
|
4156
|
+
callId: string;
|
|
4157
|
+
toolName: string;
|
|
4158
|
+
input: Record<string, unknown>;
|
|
3983
4159
|
}
|
|
3984
4160
|
/**
|
|
3985
|
-
*
|
|
4161
|
+
* 工具**执行过程中**的输出增量。callId 与对应的 tool-call 一致。
|
|
3986
4162
|
*
|
|
3987
|
-
*
|
|
3988
|
-
*
|
|
3989
|
-
*
|
|
4163
|
+
* 这是一条**只给人看**的旁路通道:模型看到的永远是最终 `tool-result.output`
|
|
4164
|
+
* (那份已经截断、已经包过隔离标签)。加它是因为 `tool-call` 和 `tool-result`
|
|
4165
|
+
* 之间可以隔着一次三分钟的 `pnpm build` —— 在那三分钟里宿主一个事件都收不到,
|
|
4166
|
+
* 屏幕上是一行不动的 `⏳ terminal`,用户分不清是在跑还是挂了。
|
|
3990
4167
|
*
|
|
3991
|
-
*
|
|
4168
|
+
* 发这个事件的工具不必逐 chunk 发:引擎侧统一做合并窗口、剥 ANSI、`\r` 折叠
|
|
4169
|
+
* 和总量封顶(见 core 的 tool-output-pump.ts),工具只管把原始 chunk 交出来。
|
|
3992
4170
|
*
|
|
3993
|
-
*
|
|
3994
|
-
|
|
4171
|
+
* 宿主可以完全忽略它 —— 忽略的结果就是改动前的行为。
|
|
4172
|
+
*/
|
|
4173
|
+
| {
|
|
4174
|
+
type: 'tool-output-delta';
|
|
4175
|
+
callId: string;
|
|
4176
|
+
toolName: string;
|
|
4177
|
+
text: string;
|
|
4178
|
+
}
|
|
4179
|
+
/**
|
|
4180
|
+
* 工具执行完毕。callId 与对应的 tool-call 一致。
|
|
3995
4181
|
*
|
|
3996
|
-
*
|
|
3997
|
-
*
|
|
3998
|
-
*
|
|
3999
|
-
* 要单独引用级别时写 `Parameters<LogFn>[0]`。
|
|
4182
|
+
* `artifacts` 给宿主渲染缩略图 / 打开文件用。它和 `output` 是**两个通道**:
|
|
4183
|
+
* `output` 是喂给模型的纯文本(已含每个 artifact 的 summary),
|
|
4184
|
+
* `artifacts` 是给人看的那一份 —— 宿主不必解析 output 去猜有没有图。
|
|
4000
4185
|
*/
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
/** 日志函数 */
|
|
4009
|
-
log: LogFn;
|
|
4186
|
+
| {
|
|
4187
|
+
type: 'tool-result';
|
|
4188
|
+
callId: string;
|
|
4189
|
+
toolName: string;
|
|
4190
|
+
output: string;
|
|
4191
|
+
ok: boolean;
|
|
4192
|
+
artifacts?: ToolArtifact[];
|
|
4010
4193
|
}
|
|
4011
|
-
/**
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
/** 语义化版本 */
|
|
4016
|
-
version: string;
|
|
4017
|
-
/** 插件描述 */
|
|
4018
|
-
description: string;
|
|
4019
|
-
/** 提供的工具列表 */
|
|
4020
|
-
tools?: EpochTool[];
|
|
4021
|
-
/** 提供的技能(可复用的 prompt 模板) */
|
|
4022
|
-
skills?: EpochSkill[];
|
|
4023
|
-
/** 注册的生命周期钩子 */
|
|
4024
|
-
hooks?: EpochHook[];
|
|
4025
|
-
/** 初始化回调 */
|
|
4026
|
-
onInit?: (ctx: PluginContext) => Promise<void>;
|
|
4027
|
-
/** 清理回调 */
|
|
4028
|
-
onDestroy?: () => Promise<void>;
|
|
4194
|
+
/** 一轮 ReAct 结束(轮次从 1 开始) */
|
|
4195
|
+
| {
|
|
4196
|
+
type: 'turn-complete';
|
|
4197
|
+
turn: number;
|
|
4029
4198
|
}
|
|
4030
|
-
|
|
4031
4199
|
/**
|
|
4032
|
-
*
|
|
4200
|
+
* 需要用户确认。宿主**必须**调用 respond(),否则引擎会一直阻塞在这里。
|
|
4201
|
+
* 引擎侧对未答复的请求有兜底(流结束时按 deny 处理),但那意味着操作被拒。
|
|
4033
4202
|
*
|
|
4034
|
-
*
|
|
4203
|
+
* `note` 只有计划审批的 `plan-revise` 用得上 —— 它装的是用户的修改意见,
|
|
4204
|
+
* 其余出口一律忽略。**没有它这个出口就是个死循环**:模型不知道要改什么,
|
|
4205
|
+
* 只会把同一份计划原样再交一次。
|
|
4206
|
+
*/
|
|
4207
|
+
| {
|
|
4208
|
+
type: 'approval-request';
|
|
4209
|
+
request: ApprovalRequest;
|
|
4210
|
+
respond: (outcome: ApprovalOutcome, note?: string) => void;
|
|
4211
|
+
}
|
|
4212
|
+
/**
|
|
4213
|
+
* 模型问了一个结构化问题,等用户选(方案 34)。宿主**必须**调用 `respond()`,
|
|
4214
|
+
* 否则引擎会一直阻塞在这里。
|
|
4035
4215
|
*
|
|
4036
|
-
*
|
|
4037
|
-
*
|
|
4038
|
-
*
|
|
4039
|
-
*
|
|
4216
|
+
* 结构与 `approval-request` 同构(请求 + 一个答复闭包),于是中继、wire 编码、
|
|
4217
|
+
* 宿主回调那几层一个都不用新造。但**语义不同,别把两者的兜底混用**:
|
|
4218
|
+
* 审批答不上来时有一个安全侧(`deny`),提问没有 —— 替用户挑一个选项和替他
|
|
4219
|
+
* 点「允许」一样糟。答不上来只有一种表达:`respond({ answers: {}, skipped: true })`,
|
|
4220
|
+
* 模型收到之后**自己决定**(通常是选默认继续),这一轮不算失败。
|
|
4221
|
+
*
|
|
4222
|
+
* 引擎侧对未答复的请求有兜底(流结束时按「跳过」处理)—— 那不是伪造答案,
|
|
4223
|
+
* 是如实说「没人答」。
|
|
4040
4224
|
*/
|
|
4041
|
-
|
|
4042
|
-
type
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
sessionId: string;
|
|
4046
|
-
timestamp: number;
|
|
4047
|
-
turnIndex?: number;
|
|
4048
|
-
userMessage?: string;
|
|
4049
|
-
response?: string;
|
|
4050
|
-
/**
|
|
4051
|
-
* 这一轮的用量。**provider 没上报时整个字段缺席**,不是一个填了 0 的对象 ——
|
|
4052
|
-
* 插件据此能区分「这轮没花钱」和「这轮花了多少不知道」(方案 42 PR-0)。
|
|
4053
|
-
*/
|
|
4054
|
-
usage?: TokenUsage;
|
|
4055
|
-
toolName?: string;
|
|
4056
|
-
toolArgs?: Record<string, unknown>;
|
|
4057
|
-
toolResult?: {
|
|
4058
|
-
success: boolean;
|
|
4059
|
-
output: string;
|
|
4060
|
-
error?: {
|
|
4061
|
-
code: number;
|
|
4062
|
-
message: string;
|
|
4063
|
-
};
|
|
4064
|
-
};
|
|
4065
|
-
rawOutput?: string;
|
|
4066
|
-
rawInput?: string;
|
|
4067
|
-
reason?: string;
|
|
4068
|
-
messageCount?: number;
|
|
4069
|
-
totalTokens?: number;
|
|
4070
|
-
prunedCount?: number;
|
|
4071
|
-
summaryTokens?: number;
|
|
4072
|
-
turns?: Array<{
|
|
4073
|
-
index: number;
|
|
4074
|
-
llmResponse: string | null;
|
|
4075
|
-
}>;
|
|
4076
|
-
data?: Record<string, unknown>;
|
|
4077
|
-
}
|
|
4078
|
-
interface HookContext {
|
|
4079
|
-
workDir: string;
|
|
4080
|
-
log: (level: 'debug' | 'info' | 'warn' | 'error', message: string) => void;
|
|
4081
|
-
}
|
|
4082
|
-
type HookHandler<T = void> = (event: HookEvent, ctx: HookContext) => Promise<T>;
|
|
4083
|
-
interface HookRegistration {
|
|
4084
|
-
type: HookType;
|
|
4085
|
-
handler: HookHandler<unknown>;
|
|
4086
|
-
priority: number;
|
|
4087
|
-
plugin: string;
|
|
4225
|
+
| {
|
|
4226
|
+
type: 'question';
|
|
4227
|
+
request: QuestionRequest;
|
|
4228
|
+
respond: (answer: QuestionAnswer) => void;
|
|
4088
4229
|
}
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4230
|
+
/**
|
|
4231
|
+
* 补丁预览:这次工具调用真的改了哪个文件、改成什么样。
|
|
4232
|
+
*
|
|
4233
|
+
* 发它的是 core 的 `agent/file-diff.ts`(`ToolExecutor` 在写类工具动手前后
|
|
4234
|
+
* 各读一遍盘),排在同一次调用的 `tool-result` **之前**。
|
|
4235
|
+
* `oldContent` 缺 = 这个文件是新建的;`newContent` 缺 = 被删了;两者都在 = 改了。
|
|
4236
|
+
*
|
|
4237
|
+
* ⚠️ 这个类型 2026-08-13 之前**声明在这里、消费在 view、渲染在两个宿主,
|
|
4238
|
+
* 中间一个发射方都没有** —— 三边契约测试全绿,因为站在引擎位置上的是
|
|
4239
|
+
* fixture 喂出来的假 agent。判据见 `.agents/plans/43-web-ui-shell-plan.md` §12.5。
|
|
4240
|
+
* **往这个 union 里加新类型时,先想清楚谁发。**
|
|
4241
|
+
*/
|
|
4242
|
+
| {
|
|
4243
|
+
type: 'diff';
|
|
4244
|
+
filePath: string;
|
|
4245
|
+
oldContent?: string;
|
|
4246
|
+
newContent?: string;
|
|
4092
4247
|
}
|
|
4093
|
-
/**
|
|
4094
|
-
|
|
4248
|
+
/** 累计用量变化(每轮结束时发一次,宿主用来更新状态栏) */
|
|
4249
|
+
| {
|
|
4250
|
+
type: 'usage';
|
|
4251
|
+
cumulative: TokenUsage;
|
|
4095
4252
|
/**
|
|
4096
|
-
*
|
|
4253
|
+
* **到此刻为止、这一次 run** 的用量(方案 42 PR-2)。
|
|
4097
4254
|
*
|
|
4098
|
-
*
|
|
4099
|
-
*
|
|
4100
|
-
*
|
|
4255
|
+
* 和 `finish.runUsage` 同一笔账,只是这一份是**过程中**的快照 ——
|
|
4256
|
+
* 界面上「运行中:已消耗 N token」要的就是它。
|
|
4257
|
+
*
|
|
4258
|
+
* ⚠️ 为什么不让宿主拿 `cumulative` 相减:相减要有一个起算基线,而
|
|
4259
|
+
* 「run 开始那一刻的累计」在事件流上根本没有 —— 第一帧 `usage` 到达时,
|
|
4260
|
+
* 它的 `cumulative` 既可能是本轮用量、也可能是 `BudgetStore` 恢复的历史
|
|
4261
|
+
* 加上本轮,从流本身分辨不了。基线猜错的代价是屏幕上那个数直接差一个量级。
|
|
4101
4262
|
*/
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
/** 从 EpochConfig.hooks 加载 command hooks */
|
|
4108
|
-
loadFromConfig(rawHooks: Record<string, unknown> | undefined): void;
|
|
4109
|
-
/** 触发 command hooks(用于 tool:before 等可阻止的事件) */
|
|
4110
|
-
triggerCommandHooks(type: HookType, event: HookEvent, workDir: string): Promise<{
|
|
4111
|
-
blocked: boolean;
|
|
4112
|
-
reason?: string;
|
|
4113
|
-
} | null>;
|
|
4114
|
-
hasHook(type: HookType): boolean;
|
|
4115
|
-
unregister(type: HookType, handler: HookHandler<unknown>): void;
|
|
4116
|
-
count(type: HookType): number;
|
|
4263
|
+
run?: TokenUsage;
|
|
4264
|
+
budgetRemaining?: {
|
|
4265
|
+
costUsd?: number;
|
|
4266
|
+
tokens?: number;
|
|
4267
|
+
};
|
|
4117
4268
|
}
|
|
4118
|
-
|
|
4119
4269
|
/**
|
|
4120
|
-
*
|
|
4270
|
+
* 运行结束。
|
|
4121
4271
|
*
|
|
4122
|
-
*
|
|
4123
|
-
*
|
|
4272
|
+
* `breach` 只在 `reason === 'budget-exceeded'` 时有值,说明是哪条上限触顶的。
|
|
4273
|
+
* 没有它的话「触顶」在事件流上只是一句中文提示(`describeBreach()` 的产物),
|
|
4274
|
+
* 而机器宿主要分支就只能去正则匹配那句中文 —— 我们改一个字它就碎。
|
|
4124
4275
|
*
|
|
4125
|
-
* ##
|
|
4276
|
+
* ## `usage` 和 `runUsage` 是两笔账,别当同义词(方案 42 PR-2)
|
|
4126
4277
|
*
|
|
4127
|
-
*
|
|
4128
|
-
*
|
|
4129
|
-
*
|
|
4130
|
-
* 类型)的代价是「每周三个字段里只填了两个」这种状态在类型上说得出口。
|
|
4278
|
+
* `usage` 是 `BudgetGuard.snapshot()` —— **累计**,口径由 `EpochRuntime.usageScope`
|
|
4279
|
+
* 决定(配了 `BudgetStore` 就是跨会话累计,否则是本进程从零算)。宿主拿它画
|
|
4280
|
+
* 「这个会话一共花了多少」。
|
|
4131
4281
|
*
|
|
4132
|
-
*
|
|
4282
|
+
* `runUsage` 是**这一次 run** 的用量,由本次循环里每一轮的 `result.usage`
|
|
4283
|
+
* 累加而成。加它的理由很具体:界面上每一轮结束时要写「共 14,270 token」,
|
|
4284
|
+
* 而那个数**减不出来** —— 累计值可能被 `BudgetStore` 从上次会话恢复,
|
|
4285
|
+
* 「第一个累计值」既可能是本轮用量、也可能是历史 + 本轮,
|
|
4286
|
+
* 从事件流本身分辨不了(`AgentLoopConfig.onTurnUsage` 那段注释骂的是同一件事)。
|
|
4133
4287
|
*
|
|
4134
|
-
*
|
|
4135
|
-
*
|
|
4136
|
-
*
|
|
4137
|
-
*
|
|
4288
|
+
* **两个都是可选的,那不是偷懒**:一次 run 可以一次模型调用都没发生就结束
|
|
4289
|
+
* (装配抛异常、预算在发送前拦下)。那时候「这一轮用了多少 / 用的哪个模型」
|
|
4290
|
+
* 没有答案,而给一个 0 和给配置里那个模型名都是撒谎 —— 与
|
|
4291
|
+
* `TokenUsage.costUsd` 的「0 是免费,undefined 是不知道」同一条约定。
|
|
4138
4292
|
*/
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4293
|
+
| {
|
|
4294
|
+
type: 'finish';
|
|
4295
|
+
reason: FinishReason;
|
|
4296
|
+
usage: TokenUsage;
|
|
4297
|
+
/** **这一次 run** 的用量。provider 一次都没上报就没有这个键 */
|
|
4298
|
+
runUsage?: TokenUsage;
|
|
4299
|
+
/**
|
|
4300
|
+
* 最后一次模型调用**实际回来**的模型(`GenerateOutput.model`)。
|
|
4301
|
+
*
|
|
4302
|
+
* ⚠️ 不是配置里选的那个。降级和 `switch_model` 之后两者不一样,而
|
|
4303
|
+
* 「这一轮到底是谁答的」正是那时候用户最需要知道的一件事。
|
|
4304
|
+
*/
|
|
4305
|
+
model?: string;
|
|
4306
|
+
/**
|
|
4307
|
+
* 最后一次模型调用**要求**的那个模型(`GenerateOutput.requestedModel`)。
|
|
4308
|
+
*
|
|
4309
|
+
* 和上面的 {@link model} 是一对,宿主要的那个事实是**两者是否相等** ——
|
|
4310
|
+
* 不等 = 引擎降级了,相等 = 用户/命令自己挑的。
|
|
4311
|
+
*
|
|
4312
|
+
* ⚠️ **读侧不许拿 `model` 去和「当前配置/当前选择」比来代替这个判断。**
|
|
4313
|
+
* 那是一个按**此刻**的值,而这条断言是按**轮**的 —— 它会被拿去比对所有
|
|
4314
|
+
* 历史轮,于是用户在顶栏换一次模型,之前那些跑得好好的轮子集体变成
|
|
4315
|
+
* 「故障转移」。web 脚注那枚徽标栽过这一次,判据全文在
|
|
4316
|
+
* `core/src/provider/types.ts` 的 `GenerateOutput.requestedModel` 上。
|
|
4317
|
+
*/
|
|
4318
|
+
requestedModel?: string;
|
|
4319
|
+
breach?: BudgetBreach;
|
|
4320
|
+
}
|
|
4144
4321
|
/**
|
|
4145
|
-
*
|
|
4322
|
+
* 引擎有一句话要对用户说,而**它不是回复正文**(2026-08-26)。
|
|
4146
4323
|
*
|
|
4147
|
-
*
|
|
4324
|
+
* ## 发它的是谁,以及为什么非要一条新帧
|
|
4325
|
+
*
|
|
4326
|
+
* 今天唯一的发射方是 core 的 `AgentLoop`:技能学习器在上一轮收尾之后
|
|
4327
|
+
* fire-and-forget 学到了一条技能,而那件事没有任何出口(判据全文在
|
|
4328
|
+
* `agent/loop.ts` 的 `maybeLearn()`)。三条现成的路各自被什么挡掉:
|
|
4329
|
+
*
|
|
4330
|
+
* 1. **`text-delta`** —— 它会被折进 assistant 那一段正文,于是这句话进历史、
|
|
4331
|
+
* 进落盘、下一轮再进模型。一句「你学会了 X」被当成模型自己说过的话,
|
|
4332
|
+
* 等于往上下文里塞一条它没说过的发言;
|
|
4333
|
+
* 2. **往 system prompt 里塞** —— 那是 prompt cache 的前缀,每轮加一句会让整段
|
|
4334
|
+
* 对话的缓存逐轮失效。同一条判据 `loop.ts` 里为后台任务通知讲过一遍;
|
|
4335
|
+
* 3. **一条 `[系统]` 进历史**(`systemNote()` + `recordNote()`,样板是
|
|
4336
|
+
* `runtime` 的 `markRoleHandoff()`)—— 它是真出口,但那句话**只在刷新 /
|
|
4337
|
+
* 回放时看得见**,而且今天只有 web 认那个前缀,TUI 会把 `[系统] ` 原样露出来。
|
|
4338
|
+
*
|
|
4339
|
+
* ## 为什么是「码 + 已经翻好的话」两个字段
|
|
4340
|
+
*
|
|
4341
|
+
* `text` 由**发射方**(core,它有 `t()`)翻好:`@epoch-agent/view` 只依赖
|
|
4342
|
+
* protocol、够不着 catalog,在那儿兜底就只能是中文 —— 同 `error` 那条空 message
|
|
4343
|
+
* 的兜底为什么落在两个宿主身上,是同一条判据。
|
|
4344
|
+
*
|
|
4345
|
+
* `code` 是给**要分支的宿主**的:判「这是哪一类通知」不许去 `includes()` 那句
|
|
4346
|
+
* 随时会改的话(RECORD-40-i18n §32.1 那条 `reason === '已取消'` 的教训)。
|
|
4347
|
+
*
|
|
4348
|
+
* ⚠️ **要拿技能名去做别的事**(跳到能力页、打开那份 SKILL.md),就再加一个
|
|
4349
|
+
* 字段,别去正则 `text` —— 那句话是会随文案改的。
|
|
4148
4350
|
*/
|
|
4149
|
-
|
|
4351
|
+
| {
|
|
4352
|
+
type: 'notice';
|
|
4353
|
+
code: NoticeCode;
|
|
4354
|
+
text: string;
|
|
4355
|
+
}
|
|
4150
4356
|
/**
|
|
4151
|
-
*
|
|
4357
|
+
* 用户在这一轮**跑到一半时**插进来的一句话,已经被引擎取走并拌进上下文
|
|
4358
|
+
* (2026-09-16)。
|
|
4152
4359
|
*
|
|
4153
|
-
*
|
|
4154
|
-
*
|
|
4360
|
+
* ## 它是「已经发生」的回执,不是请求
|
|
4361
|
+
*
|
|
4362
|
+
* 发它的是 core 的 `agent/loop.ts`,**在那句话真的被 push 进 `messages`
|
|
4363
|
+
* 的同一处**。宿主收到它就意味着「模型下一次请求会看见这句话」——
|
|
4364
|
+
* 在这之前那条消息只是躺在宿主自己的队列里,随时可能因为
|
|
4365
|
+
* 到了 `maxTurns`、或者这一轮被中止而**根本没进去**。
|
|
4366
|
+
*
|
|
4367
|
+
* 两件事因此只能由这一帧回答,别在宿主侧猜:
|
|
4368
|
+
*
|
|
4369
|
+
* 1. **落盘位置**。`runtime` 收到它才写那条 user 行,写下去的位置正是它在
|
|
4370
|
+
* 事件流里的位置 —— 也就是模型看到的位置。宿主自己按「发送时刻」插的话,
|
|
4371
|
+
* 会插到那一刻还在流式输出的 assistant 行**前面**去
|
|
4372
|
+
* (hermes 的 #73793 就是这个形状:mid-turn user bubble 被拼在
|
|
4373
|
+
* active assistant stream row 之前)。
|
|
4374
|
+
* 2. **界面上说哪句话**。「插进这一轮了」和「排在这一轮后面」是两件事,
|
|
4375
|
+
* 而 `POST /messages` 那一刻答不出是哪一件(见 `WireSendMessageResponse`)。
|
|
4376
|
+
*
|
|
4377
|
+
* ## 为什么不复用 `text-delta` 或者 `notice`
|
|
4378
|
+
*
|
|
4379
|
+
* 前者是**模型**说的话,宿主拿它拼 assistant 正文;这一条是**用户**说的,
|
|
4380
|
+
* 拼进去等于把用户的话记成模型的回答。后者是「给人看一眼就过去」的旁路,
|
|
4381
|
+
* 不落盘(判据在 `TurnStepCollector` 那个 `default` 上),而这一条必须落盘。
|
|
4382
|
+
*
|
|
4383
|
+
* ## `text` 与 `parts`
|
|
4384
|
+
*
|
|
4385
|
+
* 同 `EpochMessage`:`text` 是文本投影(落 `content` 列、进 FTS),
|
|
4386
|
+
* `parts` 只在有图片 / 文件时出现。**`parts` 里的 base64 不落 `text`** ——
|
|
4387
|
+
* 判据同 `AgentSession.run()` 里那句「绝不能把 base64 塞进这里」。
|
|
4155
4388
|
*/
|
|
4156
|
-
type ScheduleTrigger = {
|
|
4157
|
-
kind: 'cron';
|
|
4158
|
-
cycle: 'daily';
|
|
4159
|
-
at: string;
|
|
4160
|
-
} | {
|
|
4161
|
-
kind: 'cron';
|
|
4162
|
-
cycle: 'weekly';
|
|
4163
|
-
at: string;
|
|
4164
|
-
weekdays: readonly ScheduleWeekday[];
|
|
4165
|
-
}
|
|
4166
|
-
/** `days` 是几号(1–31)。31 号在小月不触发,两个平台的原生行为一致 */
|
|
4167
4389
|
| {
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
days: readonly number[];
|
|
4172
|
-
} | {
|
|
4173
|
-
kind: 'interval';
|
|
4174
|
-
everyMinutes: number;
|
|
4390
|
+
type: 'steer';
|
|
4391
|
+
text: string;
|
|
4392
|
+
parts?: EpochContentPart[];
|
|
4175
4393
|
}
|
|
4176
|
-
/**
|
|
4394
|
+
/** 运行中出错(随后会有一个 finish: 'error') */
|
|
4177
4395
|
| {
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
at: string;
|
|
4396
|
+
type: 'error';
|
|
4397
|
+
message: string;
|
|
4181
4398
|
};
|
|
4182
4399
|
/**
|
|
4183
|
-
*
|
|
4400
|
+
* 一条 `notice` 是哪一类。
|
|
4184
4401
|
*
|
|
4185
|
-
*
|
|
4186
|
-
*
|
|
4187
|
-
* 丢掉,10.11 起文档明写)。60 除不尽 7 时,「每 7 分钟」列举出来的那 9 个时刻
|
|
4188
|
-
* 在每小时边界上会漂 —— 于是 Windows 上准、macOS 上漂,而那种平台差异是 bug 的温床。
|
|
4402
|
+
* 写成联合而不是 `string`,是为了让**加一档时消费方的 switch 会红** ——
|
|
4403
|
+
* 判据同 wire.ts 的 `WireResetReason`。
|
|
4189
4404
|
*
|
|
4190
|
-
*
|
|
4191
|
-
*
|
|
4405
|
+
* ⚠️ 这里原来写着「今天只有一种」—— 那句话在写下之后就一直在过期
|
|
4406
|
+
* (`images-dropped` / `compliance-masked` / `response-truncated` 陆续进来)。
|
|
4407
|
+
* 有几种去数下面这个联合,别在散文里再抄一个会骗人的数字。
|
|
4192
4408
|
*/
|
|
4193
|
-
|
|
4409
|
+
type NoticeCode =
|
|
4194
4410
|
/**
|
|
4195
|
-
*
|
|
4411
|
+
* 技能学习器在上一轮收尾之后学到了一条技能。
|
|
4196
4412
|
*
|
|
4197
|
-
*
|
|
4198
|
-
*
|
|
4199
|
-
|
|
4200
|
-
type ScheduleRunStatus = 'ok' | 'failed' | 'denied' | 'timeout' | 'budget' | 'skipped-overlap' | 'skipped-window' | 'missed';
|
|
4201
|
-
/** 合法结局全集。存进库之前用它收窄,别让一个拼错的状态混进 `last_status` */
|
|
4202
|
-
declare const SCHEDULE_RUN_STATUSES: readonly ScheduleRunStatus[];
|
|
4203
|
-
/**
|
|
4204
|
-
* 任务注册在哪儿。
|
|
4205
|
-
*
|
|
4206
|
-
* - `schtasks` / `launchd` —— 真的注册进了操作系统的调度器,**epoch 不用开着**
|
|
4207
|
-
* - `host` —— **一行都没注册进 OS**(方案 45 §2.6 的路 B)。到点了唤起它的只能是
|
|
4208
|
-
* 宿主自己(`setInterval` 调 `fire()`)—— 而宿主有没有真的这么做,**这一格答不出来**,
|
|
4209
|
-
* 它唯一承诺的事实就是「没进 OS」。所以界面上不许把它写成「已启用」,
|
|
4210
|
-
* 也**不许许诺「开着就会跑」**(判据全文在 locales 的 `notice_host` 上)
|
|
4211
|
-
*/
|
|
4212
|
-
type ScheduleBackendKind = 'schtasks' | 'launchd' | 'host';
|
|
4213
|
-
/**
|
|
4214
|
-
* 无人值守清单 —— 「要确认,但没人在,怎么办」。
|
|
4215
|
-
*
|
|
4216
|
-
* 和权限档(`ScheduleDefinition.permission`)**各管一条轴**:档位回答「这个操作
|
|
4217
|
-
* 要不要人确认」,这份清单回答「要确认但没人在时,哪几条可以不问」。
|
|
4218
|
-
* 它不是「更宽的一档」,是把「要问人」逐条变成「这一条可以不问」。
|
|
4219
|
-
*
|
|
4220
|
-
* 三个字段由粗到细,同时命中任一条即放行:
|
|
4221
|
-
* `allowTools` 整个工具 / `allowOperations` 五类操作 / `allowRules` 规则字符串
|
|
4222
|
-
* (`terminal(git status)`、`mcp__github__*`、`file_write(./reports/**)`)。
|
|
4413
|
+
* ⚠️ 这一帧发在**下一轮的头上**,不是学成的那一刻 —— 学成时那条流早关了
|
|
4414
|
+
* (`maybeLearn()` 紧接着就是 `yield finish('stop'); return;`)。发在这里还有
|
|
4415
|
+
* 第二层意思:那一轮正是这条技能**真的进了 system prompt** 的那一轮。
|
|
4223
4416
|
*/
|
|
4224
|
-
|
|
4225
|
-
allowTools: readonly string[];
|
|
4226
|
-
allowOperations: readonly OperationType[];
|
|
4227
|
-
allowRules: readonly string[];
|
|
4228
|
-
}
|
|
4417
|
+
'skill-learned'
|
|
4229
4418
|
/**
|
|
4230
|
-
*
|
|
4419
|
+
* provider **把用户发的图片丢了**,而请求已经发出去、已经计费(2026-09-01)。
|
|
4231
4420
|
*
|
|
4232
|
-
*
|
|
4233
|
-
* **不进欠条** —— 那是档位的判定结果,不是缺授权;把它记成欠条等于建议用户
|
|
4234
|
-
* 去加一条永远不会被查到的规则(第 2 层判 deny 时压根走不到第 3 层)。
|
|
4235
|
-
*/
|
|
4236
|
-
interface SchedulePendingApproval {
|
|
4237
|
-
toolName: string;
|
|
4238
|
-
type: OperationType;
|
|
4239
|
-
target: string;
|
|
4240
|
-
/** 权限层给的原因,原样带着 —— 人回来时最想看的就是这句 */
|
|
4241
|
-
reason: string;
|
|
4242
|
-
/**
|
|
4243
|
-
* 该往清单里加哪条规则(`rule-suggest` 算的精确规则)。
|
|
4244
|
-
*
|
|
4245
|
-
* 算不出来时**整个不带这个键**:硬编一条 `terminal()` 出来只会让用户
|
|
4246
|
-
* 加进去一个死规则。
|
|
4247
|
-
*/
|
|
4248
|
-
suggestedRule?: string;
|
|
4249
|
-
at: number;
|
|
4250
|
-
}
|
|
4251
|
-
/**
|
|
4252
|
-
* 一条定时任务。
|
|
4421
|
+
* 和 `agent_run.images_unsupported` 那条错误分得很清,两者别混:
|
|
4253
4422
|
*
|
|
4254
|
-
*
|
|
4255
|
-
*
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
id: string;
|
|
4259
|
-
name: string;
|
|
4260
|
-
/**
|
|
4261
|
-
* 停用不等于删除。停用的任务在系统的任务计划里**仍然看得见,只是停着** ——
|
|
4262
|
-
* 删掉再建的话,用户在任务计划程序里看到条目消失会以为我们把他的任务弄丢了。
|
|
4263
|
-
*/
|
|
4264
|
-
enabled: boolean;
|
|
4265
|
-
createdAt: number;
|
|
4266
|
-
updatedAt: number;
|
|
4267
|
-
prompt: string;
|
|
4268
|
-
/**
|
|
4269
|
-
* 工作目录。**执行器不许继承 cwd** —— OS 调度器起进程时的 cwd 不由我们决定
|
|
4270
|
-
* (Windows 上是 `%SystemRoot%\System32`)。用户没填时落在
|
|
4271
|
-
* `~/.epoch/automation/<id>/`,那是一个确定的目录,不是「随便哪儿」。
|
|
4272
|
-
*/
|
|
4273
|
-
workDir: string;
|
|
4274
|
-
model?: string;
|
|
4275
|
-
/**
|
|
4276
|
-
* 「召唤专家」。**今天永远不带 —— 但已经不是被别人挡着了。**
|
|
4277
|
-
*
|
|
4278
|
-
* 立案时写的是「等方案 42 PR-1 把顶层会话的角色接线」,而那一笔
|
|
4279
|
-
* **2026-08-15 就结清了**(`SessionFactory.create({ role })` +
|
|
4280
|
-
* `WireCreateSessionRequest.role`,见 [agent-role.ts](./agent-role.ts) 文件头)。
|
|
4281
|
-
* 挡着这一格的只剩「没人做」:`add` / 表单 / 端点三条写入路都不写它。
|
|
4282
|
-
* ⚠️ **别把「被别人挡着」和「没人做」读成一件事** —— 逐条状态在
|
|
4283
|
-
* [验收记录 9.2](../../../docs/verify/VERIFY_RECORD-45-automation.md)。
|
|
4284
|
-
*/
|
|
4285
|
-
role?: string;
|
|
4286
|
-
/** 同 {@link role}:方案 44 的清单契约早就到齐了,缺的是写入点。今天永远不带 */
|
|
4287
|
-
skills?: readonly string[];
|
|
4288
|
-
/**
|
|
4289
|
-
* 同 {@link role},缺的是写入点。
|
|
4290
|
-
* 连接器的免确认另有一条现成的路:`allowRules` 里的 `mcp__<server>__*`。
|
|
4291
|
-
*/
|
|
4292
|
-
mcpServers?: readonly string[];
|
|
4293
|
-
/**
|
|
4294
|
-
* 权限档 —— **和对话里那枚胶囊同一套取值、同一套词**。
|
|
4295
|
-
*
|
|
4296
|
-
* 界面只暴露四档(`plan` / `default` / `acceptEdits` / `bypass`)。
|
|
4297
|
-
* 不为定时任务发明新词:用户在对话里已经学过这四个词一次,
|
|
4298
|
-
* 再教一套等于让他学两遍同一件事的两种说法。
|
|
4299
|
-
*/
|
|
4300
|
-
permission: PermissionLevel;
|
|
4301
|
-
allowTools: readonly string[];
|
|
4302
|
-
allowOperations: readonly OperationType[];
|
|
4303
|
-
allowRules: readonly string[];
|
|
4304
|
-
/** 无人看着的一轮不该比有人看着的更长(交互缺省是 50) */
|
|
4305
|
-
maxTurns: number;
|
|
4306
|
-
/**
|
|
4307
|
-
* **必填,没有默认值。** 这是整个方案唯一一处刻意增加摩擦的地方:
|
|
4308
|
-
* 一个假默认会让「一觉醒来 40 美元」变成我们的错。
|
|
4309
|
-
*/
|
|
4310
|
-
maxBudgetUsd: number;
|
|
4311
|
-
/**
|
|
4312
|
-
* 墙钟超时。前两把尺子拦不住「一条 `terminal` 命令挂住了」——
|
|
4313
|
-
* 那种情况轮次和花费都不增长,这把尺子是唯一能拦它的。
|
|
4314
|
-
*/
|
|
4315
|
-
timeoutMs: number;
|
|
4316
|
-
trigger: ScheduleTrigger;
|
|
4317
|
-
/**
|
|
4318
|
-
* 生效区间(`YYYY-MM-DD`,含首尾),留空表示始终生效。
|
|
4319
|
-
*
|
|
4320
|
-
* ⚠️ **两个平台在这个字段上语义不同**:Windows 有原生的 `/SD` `/ED`,过了截止日
|
|
4321
|
-
* 系统压根不会调我们;launchd 没有这个概念,所以 macOS 上我们会被调起来、
|
|
4322
|
-
* 然后由执行器自己判出区间外并退 0。
|
|
4323
|
-
*/
|
|
4324
|
-
startDate?: string;
|
|
4325
|
-
endDate?: string;
|
|
4326
|
-
osBackend: ScheduleBackendKind;
|
|
4327
|
-
/** 注册进 OS 时用的标识(Windows 的 `epoch\<id>` / launchd 的 `Label`) */
|
|
4328
|
-
osTaskId: string;
|
|
4329
|
-
/** 没注册进 OS 时不带(`osBackend: 'host'`,或者注册失败) */
|
|
4330
|
-
osRegisteredAt?: number;
|
|
4331
|
-
/**
|
|
4332
|
-
* **真正注册进去的那条 argv**,逐字。`doctor` 靠它比对 OS 侧有没有漂。
|
|
4333
|
-
*
|
|
4334
|
-
* 存下来而不是每次现算:现算的结果依赖**此刻**的 `process.execPath` 和入口路径,
|
|
4335
|
-
* 而漂移检查要问的恰恰是「当初写进去的是什么」。
|
|
4336
|
-
*/
|
|
4337
|
-
osCommand: readonly string[];
|
|
4338
|
-
lastRunAt?: number;
|
|
4339
|
-
lastStatus?: ScheduleRunStatus;
|
|
4340
|
-
}
|
|
4341
|
-
/** 一次运行的记录 */
|
|
4342
|
-
interface ScheduleRun {
|
|
4343
|
-
id: string;
|
|
4344
|
-
scheduleId: string;
|
|
4345
|
-
/** 真的起了会话才有;三种 `skipped-*` / `missed` 都没有 */
|
|
4346
|
-
sessionId?: string;
|
|
4347
|
-
startedAt: number;
|
|
4348
|
-
endedAt?: number;
|
|
4349
|
-
status: ScheduleRunStatus;
|
|
4350
|
-
exitCode?: number;
|
|
4351
|
-
reason?: string;
|
|
4352
|
-
costUsd: number;
|
|
4353
|
-
tokens?: TokenUsage;
|
|
4354
|
-
turns: number;
|
|
4355
|
-
/** 欠条。空数组 = 这一轮没撞上需要确认的操作 */
|
|
4356
|
-
pendingApprovals: readonly SchedulePendingApproval[];
|
|
4357
|
-
/** 录像(`<runId>.ndjson`)的绝对路径;没起会话时不带 */
|
|
4358
|
-
logPath?: string;
|
|
4359
|
-
/**
|
|
4360
|
-
* `epoch schedule run` 手动跑的那一次。
|
|
4361
|
-
*
|
|
4362
|
-
* 标出来是因为它**不该影响「上一次到点跑成没有」的判断** ——
|
|
4363
|
-
* 用户手点一次成功,不等于明天 9 点那次会成功。
|
|
4364
|
-
*/
|
|
4365
|
-
manual: boolean;
|
|
4366
|
-
}
|
|
4367
|
-
/**
|
|
4368
|
-
* 宿主注入的「怎么以无界面方式唤起我自己」(方案 45 §2.6)。
|
|
4423
|
+
* | | 谁判的 | 什么时候 | 钱 |
|
|
4424
|
+
* | --- | --- | --- | --- |
|
|
4425
|
+
* | `images_unsupported`(`error` 帧) | **我们**(闸门 1 的输入侧) | 发请求**之前** | 没花 |
|
|
4426
|
+
* | 这一条(`notice` 帧) | **provider 自己报的 warning** | 请求回来之后 | **已经花了** |
|
|
4369
4427
|
*
|
|
4370
|
-
*
|
|
4371
|
-
*
|
|
4428
|
+
* 也就是说走到这一帧意味着闸门 1 判「这个模型认图」,而 provider 那侧不认。
|
|
4429
|
+
* 2026-09-01 那次事故就是这一格:`@ai-sdk/deepseek@3.0.20` 静默丢弃图片,
|
|
4430
|
+
* 模型对着一张不存在的图回答「我只看到一个占位符」,而屏幕上一个字都没有。
|
|
4372
4431
|
*
|
|
4373
|
-
* ⚠️
|
|
4374
|
-
*
|
|
4375
|
-
* 整个应用的主窗口**(甚至一天弹 24 次)。那不是「不够好」,是把定时任务变成骚扰。
|
|
4432
|
+
* ⚠️ **一次 run 只发一帧**,不是每轮一帧:带图的那条用户消息在这一次 run 的
|
|
4433
|
+
* 每一轮都会重发,逐轮报的话一次多工具调用的对话会刷出十几行同样的话。
|
|
4376
4434
|
*/
|
|
4377
|
-
|
|
4378
|
-
/**
|
|
4379
|
-
* 无界面入口的 argv 前缀,绝对路径。我们在后面追加
|
|
4380
|
-
* `schedule fire <id> --home <dir>`。
|
|
4381
|
-
*
|
|
4382
|
-
* 那个入口必须**不建窗口、跑完就退**(Electron 里就是在 `whenReady` 之前
|
|
4383
|
-
* 分叉出去)。
|
|
4384
|
-
*/
|
|
4385
|
-
argv: readonly string[];
|
|
4386
|
-
/** 注册进 OS 时给人看的名字前缀。不给就用 `epoch` */
|
|
4387
|
-
label?: string;
|
|
4388
|
-
}
|
|
4389
|
-
|
|
4435
|
+
| 'images-dropped'
|
|
4390
4436
|
/**
|
|
4391
|
-
*
|
|
4392
|
-
*
|
|
4393
|
-
* 判据是「**这件事必须每天/每周做一次,而且做完是一份能读的东西**」——
|
|
4394
|
-
* 所以 WorkBuddy 那 12 张消费级的(儿童睡前故事 / 父母联系提醒 / 萌宠壁纸)
|
|
4395
|
-
* 一张都没抄,换成开发者场景。
|
|
4396
|
-
*
|
|
4397
|
-
* ## ⚠️ 三条硬约束都摊在类型上,不靠注释
|
|
4398
|
-
*
|
|
4399
|
-
* | §7.3 那句话 | 落在类型的哪一格 |
|
|
4400
|
-
* | ------------------------------- | -------------------------------------------------------------------- |
|
|
4401
|
-
* | 模板**不联网** | 这是一份 `const`,没有取数入口 —— 在线模板市场 42 已定不做 |
|
|
4402
|
-
* | 一律不预设「跳过全部检查」 | {@link ScheduleTemplate.permission} 的取值里**没有** `bypass` |
|
|
4403
|
-
* | 填进表单后**必须让用户过一遍** | 整份形状里**没有** `maxBudgetUsd`,而它是必填 —— 表单因此保存不了 |
|
|
4404
|
-
*
|
|
4405
|
-
* 最后一条是三条里最要紧的,而它是**结构性**的而不是靠一句提示:预算必填、
|
|
4406
|
-
* 没有默认值(§3.5),于是一张模板卡填进表单之后那个保存按钮**天然是灰的**,
|
|
4407
|
-
* 用户不看完那一屏就出不去。写成「模板里预填 0.50 + 一句『请检查』」的话,
|
|
4408
|
-
* 「必须过一遍」就退化成一句没人读的话。
|
|
4409
|
-
*
|
|
4410
|
-
* ## 为什么住在 protocol,而不是「core 一份 + 这里抄一份」
|
|
4411
|
-
*
|
|
4412
|
-
* 任务书给的两条路是「从 server 端点下发」或者「抄一份到 protocol + 一条逐项相等
|
|
4413
|
-
* 的用例」(照 `WireScheduleIssueCode` 的先例)。**两条都不用,因为这一份和那个
|
|
4414
|
-
* 先例不是同一种东西**:
|
|
4437
|
+
* 内容合规的输入闸门**改写了用户刚发的那句话**(`mask` 档,2026-09-01)。
|
|
4415
4438
|
*
|
|
4416
|
-
*
|
|
4417
|
-
* `validateSchedule()` 的出口集合,它和判定逻辑长在一起,搬不动;
|
|
4418
|
-
* - 而这一份是**纯数据、零判定**:六个触发器、六档权限、六份最小清单。
|
|
4419
|
-
* 它谁都不依赖。而 [AGENTS.md](../../../AGENTS.md) 的分层表里 protocol 那一行
|
|
4420
|
-
* 写着「跨包共享的东西**只能**放这里」。
|
|
4439
|
+
* ## 没有这一帧的话,这件事是**静默**的
|
|
4421
4440
|
*
|
|
4422
|
-
*
|
|
4423
|
-
*
|
|
4424
|
-
*
|
|
4425
|
-
* 走一条 server 端点则要为一份**从不改变的常量**加一条 HTTP 往返和一个失败态
|
|
4426
|
-
* (「模板拉不下来」——而它明明就在包里)。
|
|
4441
|
+
* `mask` 档不拒绝、也不报错:它把命中处换成 `*` 之后照常发出去。而屏幕上那条
|
|
4442
|
+
* 用户气泡显示的是**用户自己敲的原文**(宿主回显的是输入框里那份),
|
|
4443
|
+
* 模型收到的是打过码的那份 —— 两边不一样,且没有任何东西说过这件事。
|
|
4427
4444
|
*
|
|
4428
|
-
*
|
|
4445
|
+
* 实测过:`帮我翻译一下黑鬼这个词` 发出去变成 `帮我翻译一下**这个词`。
|
|
4446
|
+
* 用户看着自己那句完整的话,收到一个答非所问的回复,**没有任何线索**。
|
|
4447
|
+
* 这一帧就是那个线索。
|
|
4429
4448
|
*
|
|
4430
|
-
*
|
|
4431
|
-
*
|
|
4432
|
-
*
|
|
4433
|
-
* `web/src/automation/labels.ts` 的 `TEMPLATE_TEXT` 上 —— 那张表
|
|
4434
|
-
* `satisfies Record<ScheduleTemplateId, …>`,**加一张模板而不给它文案会当场编译不过**。
|
|
4449
|
+
* ⚠️ 和 `block` 那一档分得很清:那一档请求根本没发,走的是
|
|
4450
|
+
* `finish: 'compliance'` + 一句正文,用户当场知道发生了什么。
|
|
4451
|
+
* 这一档请求照发,所以只欠一句旁白。
|
|
4435
4452
|
*/
|
|
4436
|
-
|
|
4453
|
+
| 'compliance-masked'
|
|
4437
4454
|
/**
|
|
4438
|
-
*
|
|
4455
|
+
* provider 按 max output tokens **把这一轮的回复切了**(2026-09-04)。
|
|
4439
4456
|
*
|
|
4440
|
-
*
|
|
4441
|
-
*
|
|
4457
|
+
* 和 `FinishReason` 的 `'length'` 是同一件事的两个出口,两个都要:
|
|
4458
|
+
* 收尾原因是给**机器**分支的(headless 的 `result.reason`、落盘的
|
|
4459
|
+
* `finish_reason` 列、web 那枚记号),这一帧是给**人**看的那句话 ——
|
|
4460
|
+
* 屏幕上那半截正文自己不会说「我是半截的」。
|
|
4461
|
+
*
|
|
4462
|
+
* ⚠️ 走 `notice` 而不是 `text-delta`,判据同上面 `skill-learned` 那三条:
|
|
4463
|
+
* 它不是模型说的话,塞进正文就会进历史、进落盘、下一轮再进模型。
|
|
4442
4464
|
*/
|
|
4443
|
-
|
|
4465
|
+
| 'response-truncated'
|
|
4444
4466
|
/**
|
|
4445
|
-
*
|
|
4467
|
+
* 这一轮零工具调用收尾,而正文是空的(或只有几个字)—— **已经踢回去让它重答**
|
|
4468
|
+
* (2026-09-16)。
|
|
4446
4469
|
*
|
|
4447
|
-
*
|
|
4470
|
+
* 和 `FinishReason` 的 `'blank'` 是同一件事的两个出口,但**两者不同时出现**,
|
|
4471
|
+
* 这一点和 `response-truncated` / `'length'` 那一对相反:
|
|
4448
4472
|
*
|
|
4449
|
-
*
|
|
4450
|
-
*
|
|
4451
|
-
*
|
|
4452
|
-
*
|
|
4453
|
-
* 而**清单里那条 `file_write(./reports/**)` 是相对的**,所以它跟着工作区走 ——
|
|
4454
|
-
* 不管用户填不填,报告都落在那条任务自己的目录下面。
|
|
4455
|
-
*/
|
|
4456
|
-
interface ScheduleTemplate {
|
|
4457
|
-
id: ScheduleTemplateId;
|
|
4458
|
-
trigger: ScheduleTrigger;
|
|
4459
|
-
/**
|
|
4460
|
-
* 权限档。**取值里没有 `bypass`**(§7.3:模板一律不预设「跳过全部检查」),
|
|
4461
|
-
* 也没有 `auto`(界面压根不暴露它,§3.2)。
|
|
4462
|
-
*
|
|
4463
|
-
* 两档各有各的用法,而分界线是 by-level 那张表:
|
|
4464
|
-
*
|
|
4465
|
-
* - `plan`(只读)—— 「读一遍然后写一份报告」的那几张。它天然零授权面,
|
|
4466
|
-
* 清单里只要一条 `file_write`(那条走**第 2 层**规则匹配,所以在只读档下
|
|
4467
|
-
* 照样放行 —— §3.2 那句「只读 + `file_write(<任务目录>/**)`」说的就是它);
|
|
4468
|
-
* - `default`(按需确认)—— 要跑命令的那几张。**除了 `bypass`,写命令在每一档
|
|
4469
|
-
* 下都要确认**,所以它们只能靠清单逐条放行,而这正是清单存在的理由。
|
|
4470
|
-
*/
|
|
4471
|
-
permission: 'plan' | 'default' | 'acceptEdits';
|
|
4472
|
-
/** 整个工具免确认(粗)。多数模板不用它 —— 逐条列出来的清单才审得动 */
|
|
4473
|
-
allowTools?: readonly string[];
|
|
4474
|
-
allowOperations?: readonly OperationType[];
|
|
4475
|
-
/** 规则(细)。⚠️ **路径一律写成相对的**,理由见 {@link ScheduleTemplate} */
|
|
4476
|
-
allowRules?: readonly string[];
|
|
4477
|
-
/**
|
|
4478
|
-
* 墙钟超时(毫秒)。不给就用引擎缺省的 15 分钟。
|
|
4479
|
-
*
|
|
4480
|
-
* 只有「测试 flake 复盘」给了 —— 它真的要跑一遍测试,而缺省那把尺子会在
|
|
4481
|
-
* 半路把它砍掉。**这一格存在的意义就是让那一张不至于生下来就超时**。
|
|
4482
|
-
*/
|
|
4483
|
-
timeoutMs?: number;
|
|
4484
|
-
}
|
|
4485
|
-
/**
|
|
4486
|
-
* 六张。顺序就是界面上卡片的顺序:**两张每天的排在前面** ——
|
|
4487
|
-
* 「每天一次」比「每周一次」更容易让人第一次就试出效果,而这一屏的空状态
|
|
4488
|
-
* 要答的问题是「这东西能替我做什么」。
|
|
4489
|
-
*/
|
|
4490
|
-
declare const SCHEDULE_TEMPLATES: readonly ScheduleTemplate[];
|
|
4491
|
-
|
|
4492
|
-
/**
|
|
4493
|
-
* 「还欠着的那几张欠条」——
|
|
4494
|
-
* [方案 45](../../../docs/verify/VERIFY_RECORD-45-automation.md) §八 ① / ③ 的素材。
|
|
4473
|
+
* - 这一帧发在**第一次**判空的那一刻,后面紧跟一次重答。重答成功了的话
|
|
4474
|
+
* 那一轮的收尾原因是 `'stop'`(它确实正常收工了),而屏幕上留着这一句话,
|
|
4475
|
+
* 解释「刚才为什么多花了一轮」;
|
|
4476
|
+
* - `'blank'` 只在重答**之后还是空**时才落下去。
|
|
4495
4477
|
*
|
|
4496
|
-
*
|
|
4497
|
-
*
|
|
4498
|
-
* 侧栏有点、点进去 dock 上什么都没有。
|
|
4478
|
+
* 所以少了这一帧,用户看到的是一次莫名其妙多出来的等待;少了 `'blank'`,
|
|
4479
|
+
* 两次都空的那一轮会被记成成功。两个都要。
|
|
4499
4480
|
*
|
|
4500
|
-
*
|
|
4481
|
+
* ⚠️ 走 `notice` 而不是 `text-delta`,判据同上面 `skill-learned` 那三条:
|
|
4482
|
+
* 它不是模型说的话,塞进正文就会进历史、进落盘、下一轮再进模型。
|
|
4483
|
+
*/
|
|
4484
|
+
| 'blank-response'
|
|
4485
|
+
/**
|
|
4486
|
+
* 这一条消息发出去之前,**上下文被自动压了一次**(2026-09-10)。
|
|
4501
4487
|
*
|
|
4502
|
-
*
|
|
4503
|
-
* 没有日期算术、不读盘、不查权限,一个字的 core 知识都不需要。
|
|
4504
|
-
* 而它的消费者有四个:`server`(那条端点)、`cli`(`--fix` 和 TUI 那行开机提示)、
|
|
4505
|
-
* `packages/web/dev` 那台假服务端、以及将来任何一个嵌入宿主。
|
|
4506
|
-
* [AGENTS.md](../../../AGENTS.md) 分层表里 protocol 那一行写着
|
|
4507
|
-
* 「跨包共享的东西**只能**放这里」,这就是那一行说的情况。
|
|
4488
|
+
* ## 没有这一帧的话,用户看到的是「它忘了我们刚才聊的」
|
|
4508
4489
|
*
|
|
4509
|
-
*
|
|
4510
|
-
*
|
|
4511
|
-
*
|
|
4512
|
-
*
|
|
4490
|
+
* 自动压缩是有损的:摘要替掉了早期几十轮的原文。屏幕上那段对话还在
|
|
4491
|
+
* (宿主回显的是自己那份记录),而模型手上只剩摘要 —— 于是它接下来那句
|
|
4492
|
+
* 「你之前说的那个方案」答得对不上,而**没有任何线索**说过发生了什么。
|
|
4493
|
+
* 这一帧就是那个线索,也是「为什么这一轮多花了一笔」的唯一出处。
|
|
4513
4494
|
*
|
|
4514
|
-
*
|
|
4495
|
+
* ⚠️ 走 `notice` 而不是 `text-delta`,判据同 `skill-learned`:它不是模型说的话,
|
|
4496
|
+
* 塞进正文就会进历史、进落盘、下一轮再进模型。
|
|
4515
4497
|
*
|
|
4516
|
-
*
|
|
4498
|
+
* ⚠️ **两个位置都发这一帧,语义相同**(都是「这一发之前压了」):
|
|
4499
|
+
* `AgentSession` 跑前那道(软线,压完写回会话历史)和 `AgentLoop` 送出前那道
|
|
4500
|
+
* (硬线,见 protocol 的 `isPromptOversize`)。不给它们分两档码是因为
|
|
4501
|
+
* 用户要知道的是同一件事,而「哪道闸判的」是我们的实现细节。
|
|
4502
|
+
*/
|
|
4503
|
+
| 'context-compacted'
|
|
4504
|
+
/**
|
|
4505
|
+
* 这个模型认不认图,**我们查不到,正在现问它本人**(2026-09-11)。
|
|
4517
4506
|
*
|
|
4518
|
-
*
|
|
4519
|
-
* 进不了这份清单 —— dock 上唯一的动作是「加进授权清单」,而对它们那一下
|
|
4520
|
-
* 什么都加不了。一条点了没用的通知比没有通知坏(设计稿决定 20 ①),
|
|
4521
|
-
* 而且它**永远清不掉**:没有任何动作能让它消失。
|
|
4522
|
-
* ⚠️ 它们并没有被藏起来:「运行记录」那一屏上那一轮照旧列着它们、
|
|
4523
|
-
* 照旧写着「这一条反推不出精确规则」。这里筛掉的是**通知**,不是事实。
|
|
4524
|
-
* 2. **规则还不在 `allowRules` 里。** 用户按过那一下之后规则就进清单了,
|
|
4525
|
-
* 于是这个数**自己减一**。这条让整件事不需要「读标记」那套东西 ——
|
|
4526
|
-
* 没有迁移、没有「哪个界面算看过」这个产品判断、也没有一个只能靠
|
|
4527
|
-
* 「我看过了」来清的角标。
|
|
4507
|
+
* ## 为什么这一次探测必须说出来
|
|
4528
4508
|
*
|
|
4529
|
-
*
|
|
4509
|
+
* 它是**用户没要求过的一次额外请求**:多一趟往返(约两秒)、多一笔约 225 token
|
|
4510
|
+
* 的账。屏幕上那段静默否则解释不了 —— 用户发了张图,等了两秒,什么都没有。
|
|
4530
4511
|
*
|
|
4531
|
-
*
|
|
4532
|
-
* 动作**:屏幕上给不出一个按钮,用户只能去看运行记录。把它们算进来的话,这个点
|
|
4533
|
-
* 会一直亮着,直到那条任务下一次跑成 —— 而在那之前它已经不再指向任何可做的事,
|
|
4534
|
-
* 也就退化成了一个「有东西发生过」的常亮灯。
|
|
4512
|
+
* 判据同上面几条:多花的钱和多等的时间,只要不是用户自己点的,就欠一句旁白。
|
|
4535
4513
|
*
|
|
4536
|
-
*
|
|
4537
|
-
*
|
|
4538
|
-
*
|
|
4539
|
-
* 判断)。那是独立的一次改动,不该顺手塞进这一轮。
|
|
4540
|
-
*/
|
|
4541
|
-
|
|
4542
|
-
/**
|
|
4543
|
-
* 一条**算得出规则**的欠条。
|
|
4514
|
+
* ⚠️ **一次探测最多两帧**(「正在问」+ 只在**认图**时补一句「确认认图」):
|
|
4515
|
+
* 不认图那一路后面紧跟着 `error` 帧,那句话已经把结论说全了,再发一帧确认
|
|
4516
|
+
* 就是同一件事说两遍。
|
|
4544
4517
|
*
|
|
4545
|
-
*
|
|
4546
|
-
*
|
|
4518
|
+
* ⚠️ 每个型号**一辈子只探一次**(结论进 `~/.epoch/cache/model_probe.json`,
|
|
4519
|
+
* 认图存 30 天、不认图存 24 小时),所以这一帧天然罕见。要是它开始每轮都出现,
|
|
4520
|
+
* 说明缓存写不进去(只读的 home),那是个 bug,不是这一帧的问题。
|
|
4547
4521
|
*/
|
|
4548
|
-
|
|
4549
|
-
suggestedRule: string;
|
|
4550
|
-
};
|
|
4522
|
+
| 'image-probe'
|
|
4551
4523
|
/**
|
|
4552
|
-
*
|
|
4524
|
+
* **历史里**那几张图被摘掉了,因为此刻这个模型不认图(2026-09-12)。
|
|
4553
4525
|
*
|
|
4554
|
-
*
|
|
4555
|
-
* (「《每日仓库巡检》差一条授权」,不是「有 1 条待确认」)——
|
|
4556
|
-
* 判据和跨会话待办条那一条同源(`web/src/dock/crossbar.tsx` 文件头
|
|
4557
|
-
* 「措辞必须带宾语」那一节):一个不带宾语的计数器逼用户挨个点开去看。
|
|
4558
|
-
*/
|
|
4559
|
-
interface SchedulePendingItem {
|
|
4560
|
-
scheduleId: string;
|
|
4561
|
-
scheduleName: string;
|
|
4562
|
-
/** 欠条挂在哪一次运行上。`POST /api/schedules/:id/fix` 收的就是它 */
|
|
4563
|
-
runId: string;
|
|
4564
|
-
/** 那一次运行的开始时刻(epoch 毫秒)。界面上那句「已欠了多久」 */
|
|
4565
|
-
at: number;
|
|
4566
|
-
/** 还没进清单的那几条,**至少一条**(空的那几轮压根不进这份清单) */
|
|
4567
|
-
approvals: readonly FixableApproval[];
|
|
4568
|
-
}
|
|
4569
|
-
/**
|
|
4570
|
-
* 这一轮的欠条里,有哪几条规则还不在清单里。**去重,顺序照欠条的顺序。**
|
|
4526
|
+
* ## 和上面 `images-dropped`、和 `agent_run.images_unsupported` 三者的分工
|
|
4571
4527
|
*
|
|
4572
|
-
*
|
|
4573
|
-
*
|
|
4574
|
-
*
|
|
4575
|
-
*
|
|
4576
|
-
|
|
4577
|
-
declare function unfixedRules(allowRules: readonly string[], approvals: readonly SchedulePendingApproval[]): string[];
|
|
4578
|
-
/**
|
|
4579
|
-
* 手上那几条任务 + 最近那些运行 → 该通知的清单。
|
|
4528
|
+
* | | 图从哪来 | 谁判的 | 请求 | 钱 |
|
|
4529
|
+
* | --- | --- | --- | --- | --- |
|
|
4530
|
+
* | `images_unsupported`(`error` 帧) | **这一轮新发的** | 我们(闸门 1) | 不发 | 没花 |
|
|
4531
|
+
* | 这一条(`notice` 帧) | **历史里的** | 我们(装 history 时) | 照发 | 花了,但图不占 token |
|
|
4532
|
+
* | `images-dropped`(`notice` 帧) | 这一轮新发的 | **provider 的 warning** | 已发 | 花了 |
|
|
4580
4533
|
*
|
|
4581
|
-
*
|
|
4534
|
+
* 第一行是「你现在做的这件事做不了」,用户当场能改(换模型 / 去掉图);
|
|
4535
|
+
* 这一条是「你**以前**做的那件事,从现在起看不见了」—— 他此刻什么都没做,
|
|
4536
|
+
* 所以更需要一句旁白。
|
|
4582
4537
|
*
|
|
4583
|
-
*
|
|
4584
|
-
* 所以按任务收拢、取最近那一次。这不会漏掉东西:修最近那一次加进去的规则
|
|
4585
|
-
* 通常把老的那几次也一起覆盖了;万一老的那次欠的是**另一条**规则,
|
|
4586
|
-
* 它在下一次取数时自然浮上来成为「最近的那条未解决的」。
|
|
4538
|
+
* ## 没有这一帧的话,这件事是彻底静默的
|
|
4587
4539
|
*
|
|
4588
|
-
*
|
|
4589
|
-
*
|
|
4590
|
-
*
|
|
4540
|
+
* 触发面是两个:`epoch --resume` 装一段贴过图的会话(启动路径上**没有任何
|
|
4541
|
+
* 能力检查**),以及 `/model` 切到一个能力**查不到**的型号
|
|
4542
|
+
* (`provider/selection.ts` 那条硬拒绝只挡查得到不认图的)。两条路上用户都
|
|
4543
|
+
* 没有再发图,于是屏幕上不会有任何东西提到图 —— 而模型接下来那句
|
|
4544
|
+
* 「你发的那张截图我看不到」会像是它在犯病。
|
|
4591
4545
|
*
|
|
4592
|
-
*
|
|
4593
|
-
*
|
|
4546
|
+
* ⚠️ **一次 run 只发一帧**,判据逐字同 `images-dropped`:那几张历史图在这次
|
|
4547
|
+
* run 的每一轮都要重新摘一次,逐轮报的话一次多工具调用的对话会刷出十几行。
|
|
4594
4548
|
*/
|
|
4595
|
-
|
|
4549
|
+
| 'history-images-stripped';
|
|
4550
|
+
type AgentEventType = AgentEvent['type'];
|
|
4551
|
+
/** 取出某个具体事件的类型,例如 AgentEventOf<'tool-call'> */
|
|
4552
|
+
type AgentEventOf<T extends AgentEventType> = Extract<AgentEvent, {
|
|
4553
|
+
type: T;
|
|
4554
|
+
}>;
|
|
4596
4555
|
|
|
4597
4556
|
/**
|
|
4598
|
-
*
|
|
4599
|
-
*
|
|
4600
|
-
* `epoch schedule add` 和自动化面板那张表单是两条**人操作**的写入路。这一份是
|
|
4601
|
-
* 第三条来路,而它**不是写入路** —— 草稿谁都落不了库,它只是一份摆到人眼前的
|
|
4602
|
-
* 提案。真正落库的仍然是那两条:人点「审阅并保存」之后走的是同一个
|
|
4603
|
-
* `POST /api/schedules`,人粘贴命令行走的是同一个 `epoch schedule add`。
|
|
4604
|
-
*
|
|
4605
|
-
* ## ⚠️ 三条硬约束都摊在类型上,不靠注释(照 [schedule-templates.ts](./schedule-templates.js) 那三条办)
|
|
4557
|
+
* 插件契约。
|
|
4606
4558
|
*
|
|
4607
|
-
*
|
|
4608
|
-
*
|
|
4609
|
-
*
|
|
4610
|
-
* | 填进表单后**必须让用户过一遍** | 整份形状里**没有** `maxBudgetUsd`,而它是必填 —— 表单因此保存不了 |
|
|
4611
|
-
* | 草稿不是写入路 | 这里只有数据,没有 id 生成、没有 `enabled`、没有 `createdAt` |
|
|
4559
|
+
* 插件是 epoch-agent 的扩展单元:一个独立 npm 包,实现 EpochPlugin 接口来提供
|
|
4560
|
+
* 工具、技能和生命周期钩子。插件只需依赖本包(@epoch-agent/protocol),
|
|
4561
|
+
* 不依赖引擎实现。
|
|
4612
4562
|
*
|
|
4613
|
-
*
|
|
4614
|
-
*
|
|
4615
|
-
*
|
|
4616
|
-
|
|
4617
|
-
|
|
4563
|
+
* 设计参考:
|
|
4564
|
+
* - MCP (Model Context Protocol) 的 tool 定义格式
|
|
4565
|
+
* - Vercel AI SDK 的 tool 接口
|
|
4566
|
+
*/
|
|
4567
|
+
|
|
4568
|
+
/** 技能定义 */
|
|
4569
|
+
interface EpochSkill {
|
|
4570
|
+
/** 技能名称 */
|
|
4571
|
+
name: string;
|
|
4572
|
+
/** 触发条件(自然语言描述 LLM 何时应加载此技能) */
|
|
4573
|
+
trigger: string;
|
|
4574
|
+
/** 技能内容(Markdown 格式的流程指南) */
|
|
4575
|
+
content: string;
|
|
4576
|
+
}
|
|
4577
|
+
/** 生命周期钩子 */
|
|
4578
|
+
interface EpochHook {
|
|
4579
|
+
/** 钩子类型 */
|
|
4580
|
+
type: HookType;
|
|
4581
|
+
/** 执行函数 */
|
|
4582
|
+
handler: (event: HookEvent) => Promise<void>;
|
|
4583
|
+
}
|
|
4584
|
+
/**
|
|
4585
|
+
* 日志出口 —— 库代码唯一允许的「说点什么」的通道。
|
|
4618
4586
|
*
|
|
4619
|
-
*
|
|
4587
|
+
* 库不许直接写 stderr:嵌进 Electron 主进程或服务端之后,`console.warn` 既进不了
|
|
4588
|
+
* 宿主的日志系统,也没法在 UI 上呈现,更关不掉。所以凡是原来想 console 的地方,
|
|
4589
|
+
* 一律收成这个签名,由装配层从宿主那里接一个进来;宿主不给就静默 —— 不是退回 stderr。
|
|
4620
4590
|
*
|
|
4621
|
-
*
|
|
4622
|
-
* core 那侧要解析模型发来的入参(`tools/builtin/schedule-draft.ts`),
|
|
4623
|
-
* `packages/web` 那侧要从那次工具调用的入参里把它读回来渲染成一张卡片
|
|
4624
|
-
* (`thread/rows.ts`,照 `todo` 的先例)。web 只依赖 protocol + view,够不着 core。
|
|
4591
|
+
* 这本来就是 `PluginContext.log` 的签名,只是以前只有插件能用。
|
|
4625
4592
|
*
|
|
4626
|
-
*
|
|
4593
|
+
* **`message` 必须自带完整信息**:`detail` 是给结构化日志系统的补充,
|
|
4594
|
+
* 一个只看 `message` 的朴素宿主实现不能因此丢掉错误原因。
|
|
4627
4595
|
*
|
|
4628
|
-
*
|
|
4629
|
-
* `
|
|
4630
|
-
*
|
|
4596
|
+
* 级别刻意写成内联联合、不另开一个 `LogLevel` 导出:infra 的 logger 里已经有一个
|
|
4597
|
+
* 结构完全相同的 `LogLevel`,而 protocol 和 infra 都是零内部依赖的包,谁也 import
|
|
4598
|
+
* 不了谁。与其在两个包里各挂一个同名类型让人猜该用哪个,不如这里不占这个名字 ——
|
|
4599
|
+
* 要单独引用级别时写 `Parameters<LogFn>[0]`。
|
|
4631
4600
|
*/
|
|
4601
|
+
type LogFn = (level: 'debug' | 'info' | 'warn' | 'error', message: string, detail?: object) => void;
|
|
4602
|
+
/** 插件上下文(初始化时注入) */
|
|
4603
|
+
interface PluginContext {
|
|
4604
|
+
/** 工作目录 */
|
|
4605
|
+
workDir: string;
|
|
4606
|
+
/** 配置数据 */
|
|
4607
|
+
config: Record<string, unknown>;
|
|
4608
|
+
/** 日志函数 */
|
|
4609
|
+
log: LogFn;
|
|
4610
|
+
}
|
|
4611
|
+
/** 插件定义 */
|
|
4612
|
+
interface EpochPlugin {
|
|
4613
|
+
/** 插件唯一标识(对应 npm 包名) */
|
|
4614
|
+
name: string;
|
|
4615
|
+
/** 语义化版本 */
|
|
4616
|
+
version: string;
|
|
4617
|
+
/** 插件描述 */
|
|
4618
|
+
description: string;
|
|
4619
|
+
/** 提供的工具列表 */
|
|
4620
|
+
tools?: EpochTool[];
|
|
4621
|
+
/** 提供的技能(可复用的 prompt 模板) */
|
|
4622
|
+
skills?: EpochSkill[];
|
|
4623
|
+
/** 注册的生命周期钩子 */
|
|
4624
|
+
hooks?: EpochHook[];
|
|
4625
|
+
/** 初始化回调 */
|
|
4626
|
+
onInit?: (ctx: PluginContext) => Promise<void>;
|
|
4627
|
+
/** 清理回调 */
|
|
4628
|
+
onDestroy?: () => Promise<void>;
|
|
4629
|
+
}
|
|
4632
4630
|
|
|
4633
4631
|
/**
|
|
4634
|
-
*
|
|
4635
|
-
* core 注册工具时用它,web 从工具流里认出那次调用时用它
|
|
4636
|
-
* (`thread/rows.ts`)。
|
|
4637
|
-
*
|
|
4638
|
-
* ⚠️ 这一点上它和隔壁 `TODO_TOOL` 的做法不一样:那个名字今天在
|
|
4639
|
-
* `web/src/thread/rows.ts` 里另写了一份字面量(那时 protocol 里没有落脚点)。
|
|
4640
|
-
* 别照那个抄 —— 两份字面量漂了的表现是「卡片再也不渲染了」,而 typecheck
|
|
4641
|
-
* 一声不响。
|
|
4642
|
-
*/
|
|
4643
|
-
declare const SCHEDULE_DRAFT_TOOL = "schedule_draft";
|
|
4644
|
-
/**
|
|
4645
|
-
* 草稿能落在哪几档权限上 —— **`bypass` 不在里面**。
|
|
4632
|
+
* Hook 契约。
|
|
4646
4633
|
*
|
|
4647
|
-
*
|
|
4648
|
-
* `ScheduleTemplate.permission`)。理由比模板那一侧更直接:`bypass` 要一次
|
|
4649
|
-
* 二次确认(`bypass-not-confirmed`),而那次确认的**对象是人**;让模型提议
|
|
4650
|
-
* 一个「等着人去勾确认框」的档位,等于把那道确认变成一道例行手续。
|
|
4634
|
+
* 参考 Hermes hermes_cli/plugins.py → VALID_HOOKS + invoke_hook
|
|
4651
4635
|
*
|
|
4652
|
-
*
|
|
4636
|
+
* 注意:HookType 之前在 core/src/plugin.ts 和 core/src/hook/types.ts 各有一份,
|
|
4637
|
+
* 且**内容不同**(plugin.ts 那份只有 7 个事件,缺 compress:after / transform:* /
|
|
4638
|
+
* verify:stop)。插件作者按 EpochHook 写代码时拿到的是窄的那份,注册进
|
|
4639
|
+
* HookManager 却是宽的那份。这里合并成唯一一份。
|
|
4653
4640
|
*/
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
interface
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4641
|
+
|
|
4642
|
+
type HookType = 'session:start' | 'session:end' | 'turn:before' | 'turn:after' | 'tool:before' | 'tool:after' | 'stop' | 'compress:after' | 'transform:tool:output' | 'transform:response' | 'transform:user:input' | 'verify:stop';
|
|
4643
|
+
interface HookEvent {
|
|
4644
|
+
type: HookType;
|
|
4645
|
+
sessionId: string;
|
|
4646
|
+
timestamp: number;
|
|
4647
|
+
turnIndex?: number;
|
|
4648
|
+
userMessage?: string;
|
|
4649
|
+
response?: string;
|
|
4650
|
+
/**
|
|
4651
|
+
* 这一轮的用量。**provider 没上报时整个字段缺席**,不是一个填了 0 的对象 ——
|
|
4652
|
+
* 插件据此能区分「这轮没花钱」和「这轮花了多少不知道」(方案 42 PR-0)。
|
|
4653
|
+
*/
|
|
4654
|
+
usage?: TokenUsage;
|
|
4655
|
+
toolName?: string;
|
|
4656
|
+
toolArgs?: Record<string, unknown>;
|
|
4657
|
+
toolResult?: {
|
|
4658
|
+
success: boolean;
|
|
4659
|
+
output: string;
|
|
4660
|
+
error?: {
|
|
4661
|
+
code: number;
|
|
4662
|
+
message: string;
|
|
4663
|
+
};
|
|
4664
|
+
};
|
|
4665
|
+
rawOutput?: string;
|
|
4666
|
+
rawInput?: string;
|
|
4667
|
+
reason?: string;
|
|
4668
|
+
messageCount?: number;
|
|
4669
|
+
totalTokens?: number;
|
|
4670
|
+
prunedCount?: number;
|
|
4671
|
+
summaryTokens?: number;
|
|
4672
|
+
turns?: Array<{
|
|
4673
|
+
index: number;
|
|
4674
|
+
llmResponse: string | null;
|
|
4675
|
+
}>;
|
|
4676
|
+
data?: Record<string, unknown>;
|
|
4660
4677
|
}
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4678
|
+
interface HookContext {
|
|
4679
|
+
workDir: string;
|
|
4680
|
+
log: (level: 'debug' | 'info' | 'warn' | 'error', message: string) => void;
|
|
4681
|
+
}
|
|
4682
|
+
type HookHandler<T = void> = (event: HookEvent, ctx: HookContext) => Promise<T>;
|
|
4683
|
+
interface HookRegistration {
|
|
4684
|
+
type: HookType;
|
|
4685
|
+
handler: HookHandler<unknown>;
|
|
4686
|
+
priority: number;
|
|
4687
|
+
plugin: string;
|
|
4688
|
+
}
|
|
4689
|
+
interface VerifyResult {
|
|
4690
|
+
action: 'stop' | 'continue';
|
|
4691
|
+
message?: string;
|
|
4692
|
+
}
|
|
4693
|
+
/** Hook 管理器接口(实现在 core) */
|
|
4694
|
+
interface HookManager {
|
|
4670
4695
|
/**
|
|
4671
|
-
*
|
|
4696
|
+
* 宿主注入的诊断出口,同时也是 `HookContext.log` 的来源。
|
|
4672
4697
|
*
|
|
4673
|
-
*
|
|
4674
|
-
*
|
|
4698
|
+
* 挂在管理器上而不是让 `AgentLoop` 另外收一个 logger:那样宿主要接两次,
|
|
4699
|
+
* 接漏一处就是「hook 系统自己的告警有人收、插件在 hook 里说的话没人收」。
|
|
4700
|
+
* 可选是给别的实现留的余地,缺省时 `HookContext.log` 退化成静默。
|
|
4675
4701
|
*/
|
|
4676
|
-
|
|
4702
|
+
readonly log?: LogFn;
|
|
4703
|
+
register(reg: HookRegistration): void;
|
|
4704
|
+
trigger(type: HookType, event: HookEvent, ctx: HookContext): Promise<void>;
|
|
4705
|
+
triggerTransform<T>(type: HookType, event: HookEvent, ctx: HookContext): Promise<T | null>;
|
|
4706
|
+
triggerVerify(type: HookType, event: HookEvent, ctx: HookContext): Promise<VerifyResult[]>;
|
|
4707
|
+
/** 从 EpochConfig.hooks 加载 command hooks */
|
|
4708
|
+
loadFromConfig(rawHooks: Record<string, unknown> | undefined): void;
|
|
4709
|
+
/** 触发 command hooks(用于 tool:before 等可阻止的事件) */
|
|
4710
|
+
triggerCommandHooks(type: HookType, event: HookEvent, workDir: string): Promise<{
|
|
4711
|
+
blocked: boolean;
|
|
4712
|
+
reason?: string;
|
|
4713
|
+
} | null>;
|
|
4714
|
+
hasHook(type: HookType): boolean;
|
|
4715
|
+
unregister(type: HookType, handler: HookHandler<unknown>): void;
|
|
4716
|
+
count(type: HookType): number;
|
|
4677
4717
|
}
|
|
4718
|
+
|
|
4678
4719
|
/**
|
|
4679
|
-
*
|
|
4720
|
+
* 定时任务契约(方案 45)。
|
|
4680
4721
|
*
|
|
4681
|
-
*
|
|
4682
|
-
*
|
|
4683
|
-
*
|
|
4722
|
+
* 这一份只装**形状**:触发器怎么表达、一条任务有哪些字段、一次运行留下什么。
|
|
4723
|
+
* 注册进操作系统、抢锁、跑一次,全在 `core/src/schedule/` 和 `runtime` 里。
|
|
4724
|
+
*
|
|
4725
|
+
* ## 为什么触发器是判别联合,而存储是一段 JSON
|
|
4726
|
+
*
|
|
4727
|
+
* 三档的字段完全不同(每周要 `weekdays`、按间隔要 `everyMinutes`、单次还要个
|
|
4728
|
+
* 日期),拆成列会得到一张一半是 NULL 的表。所以库里 `trigger_spec` 是一段
|
|
4729
|
+
* JSON —— 但**类型是这个联合**,JSON 只是它的存储形式。反过来(先有列再拼
|
|
4730
|
+
* 类型)的代价是「每周三个字段里只填了两个」这种状态在类型上说得出口。
|
|
4731
|
+
*
|
|
4732
|
+
* ## 为什么没有完整 cron 表达式
|
|
4733
|
+
*
|
|
4734
|
+
* 这三档正好是 Windows 任务计划(`schtasks`)与 macOS launchd 的**公共**能力,
|
|
4735
|
+
* 而 5 字段 cron 不是。写全 cron 等于把「时区 + 夏令时 + 闰日」搬回我们自己的
|
|
4736
|
+
* 代码里算下一次触发时刻 —— 而把触发交给操作系统的全部意义就是不碰它们。
|
|
4737
|
+
* 判据一句话:**一个语义在两个平台上不能逐字一致,就不做这个语义。**
|
|
4738
|
+
*/
|
|
4739
|
+
|
|
4740
|
+
/** 三档频率。和 WorkBuddy 的分档逐字一致,理由见文件头 */
|
|
4741
|
+
type ScheduleTriggerKind = 'cron' | 'interval' | 'once';
|
|
4742
|
+
/** 星期。`0` = 周日 … `6` = 周六,与 `Date.prototype.getDay()` 同口径 */
|
|
4743
|
+
type ScheduleWeekday = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
4744
|
+
/**
|
|
4745
|
+
* 「周期」档的三种周期。
|
|
4684
4746
|
*
|
|
4685
|
-
*
|
|
4747
|
+
* 和 `ScheduleTriggerKind` 不是一回事:那个分的是三档,这个只细分 `cron` 那一档。
|
|
4748
|
+
*/
|
|
4749
|
+
type ScheduleCycle = 'daily' | 'weekly' | 'monthly';
|
|
4750
|
+
/**
|
|
4751
|
+
* 触发器。
|
|
4686
4752
|
*
|
|
4687
|
-
*
|
|
4688
|
-
*
|
|
4689
|
-
* 「这次提案放宽了什么」单独标出来(web 的卡片、core 的工具输出),
|
|
4690
|
-
* 不许把它混在一列改动里 —— 用户扫一眼就点保存的那一下,正是这条要防的。
|
|
4753
|
+
* 所有带时刻的档共用 `at: 'HH:mm'`(24 小时制,**本机时区**)—— 统一成一个字段名
|
|
4754
|
+
* 是为了让「几点跑」在三档之间读起来是同一件事;单次那一档另加 `date`。
|
|
4691
4755
|
*/
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
enabled?: boolean;
|
|
4702
|
-
maxTurns?: number;
|
|
4703
|
-
timeoutMs?: number;
|
|
4704
|
-
/** 同 {@link ScheduleDraftCreate.note},必填 */
|
|
4705
|
-
note: string;
|
|
4756
|
+
type ScheduleTrigger = {
|
|
4757
|
+
kind: 'cron';
|
|
4758
|
+
cycle: 'daily';
|
|
4759
|
+
at: string;
|
|
4760
|
+
} | {
|
|
4761
|
+
kind: 'cron';
|
|
4762
|
+
cycle: 'weekly';
|
|
4763
|
+
at: string;
|
|
4764
|
+
weekdays: readonly ScheduleWeekday[];
|
|
4706
4765
|
}
|
|
4707
|
-
|
|
4766
|
+
/** `days` 是几号(1–31)。31 号在小月不触发,两个平台的原生行为一致 */
|
|
4767
|
+
| {
|
|
4768
|
+
kind: 'cron';
|
|
4769
|
+
cycle: 'monthly';
|
|
4770
|
+
at: string;
|
|
4771
|
+
days: readonly number[];
|
|
4772
|
+
} | {
|
|
4773
|
+
kind: 'interval';
|
|
4774
|
+
everyMinutes: number;
|
|
4775
|
+
}
|
|
4776
|
+
/** `date` 是 `YYYY-MM-DD` */
|
|
4777
|
+
| {
|
|
4778
|
+
kind: 'once';
|
|
4779
|
+
date: string;
|
|
4780
|
+
at: string;
|
|
4781
|
+
};
|
|
4708
4782
|
/**
|
|
4709
|
-
*
|
|
4783
|
+
* 「按间隔」档的**全部**合法取值(分钟)。
|
|
4710
4784
|
*
|
|
4711
|
-
*
|
|
4712
|
-
*
|
|
4713
|
-
*
|
|
4785
|
+
* 收紧成「60 的约数」和「整小时数」不是洁癖,是 launchd 的硬约束:那一侧只能用
|
|
4786
|
+
* `StartCalendarInterval`(列举时刻),不能用 `StartInterval`(睡眠时那一次会被
|
|
4787
|
+
* 丢掉,10.11 起文档明写)。60 除不尽 7 时,「每 7 分钟」列举出来的那 9 个时刻
|
|
4788
|
+
* 在每小时边界上会漂 —— 于是 Windows 上准、macOS 上漂,而那种平台差异是 bug 的温床。
|
|
4714
4789
|
*
|
|
4715
|
-
*
|
|
4716
|
-
*
|
|
4717
|
-
* `web/src/thread/rows.ts` 文件头那条:web 读的是**模型发出的原始入参**,
|
|
4718
|
-
* 而不是 core 归一化之后的结论。
|
|
4790
|
+
* 界面上因此是个下拉而不是数字输入框:下拉能表达「只有这些值」,
|
|
4791
|
+
* 输入框会让人填 7 然后得到一个漂的任务。
|
|
4719
4792
|
*/
|
|
4720
|
-
|
|
4721
|
-
|
|
4793
|
+
declare const SCHEDULE_INTERVAL_MINUTES: readonly number[];
|
|
4722
4794
|
/**
|
|
4723
|
-
*
|
|
4724
|
-
*
|
|
4725
|
-
* 为什么需要:clone 一个仓库就等于让它的 `AGENTS.md` 进 system prompt、
|
|
4726
|
-
* 它的 hook 配置被执行、它的 policy 覆盖本地权限判定。这是漏洞,不是缺功能。
|
|
4727
|
-
* 信任判定把「加载项目级配置」这件事收到一次显式用户决策后面。
|
|
4728
|
-
*
|
|
4729
|
-
* 竞品:Codex `codex-rs/tui/src/onboarding/trust_directory.rs`;
|
|
4730
|
-
* Gemini CLI `packages/cli/src/config/trustedFolders.ts` + `packages/core/src/utils/trust.ts`。
|
|
4731
|
-
* 两家都只是读过的参照物:契约形状跟它们都不一样(上游是 TRUST_FOLDER / TRUST_PARENT /
|
|
4732
|
-
* DO_NOT_TRUST 三枚举 + 靠 TRUST_PARENT 取 dirname 来编码作用域,这里是三态字面量 +
|
|
4733
|
-
* 独立的 scope 字段 + decidedAt)。本文件是独立实现,不带 SPDX 头。
|
|
4734
|
-
* 判定证据见 docs/verify/VERIFY_RECORD-D-license-attribution.md
|
|
4795
|
+
* 一次运行的结局。
|
|
4735
4796
|
*
|
|
4736
|
-
*
|
|
4737
|
-
*
|
|
4738
|
-
* 契约与实现的授权状态不同是对的,不是漏登记。
|
|
4797
|
+
* 后三个是「没跑」而不是「跑失败了」,它们必须和 `failed` 分开 ——
|
|
4798
|
+
* 界面上把「上一次因为上一次还在跑所以跳过了」显示成红叉,用户会去查一个不存在的故障。
|
|
4739
4799
|
*/
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
'trusted'
|
|
4744
|
-
/** 用户明确拒绝:只当普通工作目录,不加载任何项目级配置 */
|
|
4745
|
-
| 'untrusted'
|
|
4746
|
-
/** 还没问过 */
|
|
4747
|
-
| 'unknown';
|
|
4748
|
-
/** 用户已经做过决策的信任状态(排除「还没问过」) */
|
|
4749
|
-
type DecidedTrustLevel = Exclude<TrustLevel, 'unknown'>;
|
|
4750
|
-
/** 信任决策的作用域 */
|
|
4751
|
-
type TrustScope =
|
|
4752
|
-
/** 只这个目录 */
|
|
4753
|
-
'directory'
|
|
4754
|
-
/** 这个目录及其所有子目录 */
|
|
4755
|
-
| 'directory-tree';
|
|
4800
|
+
type ScheduleRunStatus = 'ok' | 'failed' | 'denied' | 'timeout' | 'budget' | 'skipped-overlap' | 'skipped-window' | 'missed';
|
|
4801
|
+
/** 合法结局全集。存进库之前用它收窄,别让一个拼错的状态混进 `last_status` */
|
|
4802
|
+
declare const SCHEDULE_RUN_STATUSES: readonly ScheduleRunStatus[];
|
|
4756
4803
|
/**
|
|
4757
|
-
*
|
|
4804
|
+
* 任务注册在哪儿。
|
|
4758
4805
|
*
|
|
4759
|
-
*
|
|
4806
|
+
* - `schtasks` / `launchd` —— 真的注册进了操作系统的调度器,**epoch 不用开着**
|
|
4807
|
+
* - `host` —— **一行都没注册进 OS**(方案 45 §2.6 的路 B)。到点了唤起它的只能是
|
|
4808
|
+
* 宿主自己(`setInterval` 调 `fire()`)—— 而宿主有没有真的这么做,**这一格答不出来**,
|
|
4809
|
+
* 它唯一承诺的事实就是「没进 OS」。所以界面上不许把它写成「已启用」,
|
|
4810
|
+
* 也**不许许诺「开着就会跑」**(判据全文在 locales 的 `notice_host` 上)
|
|
4811
|
+
*/
|
|
4812
|
+
type ScheduleBackendKind = 'schtasks' | 'launchd' | 'host';
|
|
4813
|
+
/**
|
|
4814
|
+
* 无人值守清单 —— 「要确认,但没人在,怎么办」。
|
|
4760
4815
|
*
|
|
4761
|
-
*
|
|
4762
|
-
*
|
|
4763
|
-
*
|
|
4764
|
-
* 而用户被问到的那句话逐字是「本目录的项目指令会加载」(`trust_prompt.granted`),
|
|
4765
|
-
* **从来没提过 hook**。也就是说「往上下文里注入一段文本」和「在这台机器上执行
|
|
4766
|
-
* 任意命令」共用了同一次点击,而问话只描述了前者。这不是 UX 问题,是知情同意错位。
|
|
4816
|
+
* 和权限档(`ScheduleDefinition.permission`)**各管一条轴**:档位回答「这个操作
|
|
4817
|
+
* 要不要人确认」,这份清单回答「要确认但没人在时,哪几条可以不问」。
|
|
4818
|
+
* 它不是「更宽的一档」,是把「要问人」逐条变成「这一条可以不问」。
|
|
4767
4819
|
*
|
|
4768
|
-
*
|
|
4769
|
-
*
|
|
4770
|
-
*
|
|
4771
|
-
|
|
4820
|
+
* 三个字段由粗到细,同时命中任一条即放行:
|
|
4821
|
+
* `allowTools` 整个工具 / `allowOperations` 五类操作 / `allowRules` 规则字符串
|
|
4822
|
+
* (`terminal(git status)`、`mcp__github__*`、`file_write(./reports/**)`)。
|
|
4823
|
+
*/
|
|
4824
|
+
interface ScheduleAllowlist {
|
|
4825
|
+
allowTools: readonly string[];
|
|
4826
|
+
allowOperations: readonly OperationType[];
|
|
4827
|
+
allowRules: readonly string[];
|
|
4828
|
+
}
|
|
4829
|
+
/**
|
|
4830
|
+
* 一条欠条 —— 「本该问人,但没人可问」。
|
|
4772
4831
|
*
|
|
4773
|
-
*
|
|
4832
|
+
* ⚠️ **只收这一类**。权限档自己判出的拒绝(选了「只读」档然后去写文件)
|
|
4833
|
+
* **不进欠条** —— 那是档位的判定结果,不是缺授权;把它记成欠条等于建议用户
|
|
4834
|
+
* 去加一条永远不会被查到的规则(第 2 层判 deny 时压根走不到第 3 层)。
|
|
4835
|
+
*/
|
|
4836
|
+
interface SchedulePendingApproval {
|
|
4837
|
+
toolName: string;
|
|
4838
|
+
type: OperationType;
|
|
4839
|
+
target: string;
|
|
4840
|
+
/** 权限层给的原因,原样带着 —— 人回来时最想看的就是这句 */
|
|
4841
|
+
reason: string;
|
|
4842
|
+
/**
|
|
4843
|
+
* 该往清单里加哪条规则(`rule-suggest` 算的精确规则)。
|
|
4844
|
+
*
|
|
4845
|
+
* 算不出来时**整个不带这个键**:硬编一条 `terminal()` 出来只会让用户
|
|
4846
|
+
* 加进去一个死规则。
|
|
4847
|
+
*/
|
|
4848
|
+
suggestedRule?: string;
|
|
4849
|
+
at: number;
|
|
4850
|
+
}
|
|
4851
|
+
/**
|
|
4852
|
+
* 一条定时任务。
|
|
4774
4853
|
*
|
|
4775
|
-
*
|
|
4776
|
-
*
|
|
4777
|
-
* 未授 `exec` 的目录里 agent 照样能跑 bash,只是仓库里那份 `hooks.json` 不生效;
|
|
4778
|
-
* 2. **`read` 不是「安全档」。** 一份 `AGENTS.md` 能指挥 agent 去做任何它有权限做的
|
|
4779
|
-
* 事,包括执行命令。拆开不是因为 `read` 无害,是因为**两者的问话不该合并成一次**。
|
|
4854
|
+
* 时间一律是 **epoch 毫秒**而不是 `Date`:这个形状要过 JSON(存库、将来上 SSE),
|
|
4855
|
+
* `Date` 一过 JSON 就变成字符串,于是同一个字段在两处是两种类型。
|
|
4780
4856
|
*/
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
path: string;
|
|
4785
|
-
level: DecidedTrustLevel;
|
|
4857
|
+
interface ScheduleDefinition {
|
|
4858
|
+
id: string;
|
|
4859
|
+
name: string;
|
|
4786
4860
|
/**
|
|
4787
|
-
*
|
|
4861
|
+
* 停用不等于删除。停用的任务在系统的任务计划里**仍然看得见,只是停着** ——
|
|
4862
|
+
* 删掉再建的话,用户在任务计划程序里看到条目消失会以为我们把他的任务弄丢了。
|
|
4863
|
+
*/
|
|
4864
|
+
enabled: boolean;
|
|
4865
|
+
createdAt: number;
|
|
4866
|
+
updatedAt: number;
|
|
4867
|
+
prompt: string;
|
|
4868
|
+
/**
|
|
4869
|
+
* 工作目录。**执行器不许继承 cwd** —— OS 调度器起进程时的 cwd 不由我们决定
|
|
4870
|
+
* (Windows 上是 `%SystemRoot%\System32`)。用户没填时落在
|
|
4871
|
+
* `~/.epoch/automation/<id>/`,那是一个确定的目录,不是「随便哪儿」。
|
|
4872
|
+
*/
|
|
4873
|
+
workDir: string;
|
|
4874
|
+
model?: string;
|
|
4875
|
+
/**
|
|
4876
|
+
* 「召唤专家」。**今天永远不带 —— 但已经不是被别人挡着了。**
|
|
4788
4877
|
*
|
|
4789
|
-
*
|
|
4790
|
-
*
|
|
4791
|
-
* `
|
|
4878
|
+
* 立案时写的是「等方案 42 PR-1 把顶层会话的角色接线」,而那一笔
|
|
4879
|
+
* **2026-08-15 就结清了**(`SessionFactory.create({ role })` +
|
|
4880
|
+
* `WireCreateSessionRequest.role`,见 [agent-role.ts](./agent-role.ts) 文件头)。
|
|
4881
|
+
* 挡着这一格的只剩「没人做」:`add` / 表单 / 端点三条写入路都不写它。
|
|
4882
|
+
* ⚠️ **别把「被别人挡着」和「没人做」读成一件事** —— 逐条状态在
|
|
4883
|
+
* [验收记录 9.2](../../../docs/verify/VERIFY_RECORD-45-automation.md)。
|
|
4792
4884
|
*/
|
|
4793
|
-
|
|
4794
|
-
/**
|
|
4795
|
-
|
|
4885
|
+
role?: string;
|
|
4886
|
+
/** 同 {@link role}:方案 44 的清单契约早就到齐了,缺的是写入点。今天永远不带 */
|
|
4887
|
+
skills?: readonly string[];
|
|
4796
4888
|
/**
|
|
4797
|
-
*
|
|
4889
|
+
* 同 {@link role},缺的是写入点。
|
|
4890
|
+
* 连接器的免确认另有一条现成的路:`allowRules` 里的 `mcp__<server>__*`。
|
|
4891
|
+
*/
|
|
4892
|
+
mcpServers?: readonly string[];
|
|
4893
|
+
/**
|
|
4894
|
+
* 权限档 —— **和对话里那枚胶囊同一套取值、同一套词**。
|
|
4895
|
+
*
|
|
4896
|
+
* 界面只暴露四档(`plan` / `default` / `acceptEdits` / `bypass`)。
|
|
4897
|
+
* 不为定时任务发明新词:用户在对话里已经学过这四个词一次,
|
|
4898
|
+
* 再教一套等于让他学两遍同一件事的两种说法。
|
|
4899
|
+
*/
|
|
4900
|
+
permission: PermissionLevel;
|
|
4901
|
+
allowTools: readonly string[];
|
|
4902
|
+
allowOperations: readonly OperationType[];
|
|
4903
|
+
allowRules: readonly string[];
|
|
4904
|
+
/** 无人看着的一轮不该比有人看着的更长(交互缺省是 50) */
|
|
4905
|
+
maxTurns: number;
|
|
4906
|
+
/**
|
|
4907
|
+
* **必填,没有默认值。** 这是整个方案唯一一处刻意增加摩擦的地方:
|
|
4908
|
+
* 一个假默认会让「一觉醒来 40 美元」变成我们的错。
|
|
4909
|
+
*/
|
|
4910
|
+
maxBudgetUsd: number;
|
|
4911
|
+
/**
|
|
4912
|
+
* 墙钟超时。前两把尺子拦不住「一条 `terminal` 命令挂住了」——
|
|
4913
|
+
* 那种情况轮次和花费都不增长,这把尺子是唯一能拦它的。
|
|
4914
|
+
*/
|
|
4915
|
+
timeoutMs: number;
|
|
4916
|
+
trigger: ScheduleTrigger;
|
|
4917
|
+
/**
|
|
4918
|
+
* 生效区间(`YYYY-MM-DD`,含首尾),留空表示始终生效。
|
|
4919
|
+
*
|
|
4920
|
+
* ⚠️ **两个平台在这个字段上语义不同**:Windows 有原生的 `/SD` `/ED`,过了截止日
|
|
4921
|
+
* 系统压根不会调我们;launchd 没有这个概念,所以 macOS 上我们会被调起来、
|
|
4922
|
+
* 然后由执行器自己判出区间外并退 0。
|
|
4923
|
+
*/
|
|
4924
|
+
startDate?: string;
|
|
4925
|
+
endDate?: string;
|
|
4926
|
+
osBackend: ScheduleBackendKind;
|
|
4927
|
+
/** 注册进 OS 时用的标识(Windows 的 `epoch\<id>` / launchd 的 `Label`) */
|
|
4928
|
+
osTaskId: string;
|
|
4929
|
+
/** 没注册进 OS 时不带(`osBackend: 'host'`,或者注册失败) */
|
|
4930
|
+
osRegisteredAt?: number;
|
|
4931
|
+
/**
|
|
4932
|
+
* **真正注册进去的那条 argv**,逐字。`doctor` 靠它比对 OS 侧有没有漂。
|
|
4798
4933
|
*
|
|
4799
|
-
*
|
|
4800
|
-
*
|
|
4801
|
-
* 那些记录是在一句只说了「项目指令会加载」的问话下拿到的,把它们追认成执行类
|
|
4802
|
-
* 等于替用户补签一份他没看过的同意书。代价(老用户的项目 hook 会停)由
|
|
4803
|
-
* `trust_gate.exec_migrated` 那条诊断明说,不静默。
|
|
4934
|
+
* 存下来而不是每次现算:现算的结果依赖**此刻**的 `process.execPath` 和入口路径,
|
|
4935
|
+
* 而漂移检查要问的恰恰是「当初写进去的是什么」。
|
|
4804
4936
|
*/
|
|
4805
|
-
|
|
4937
|
+
osCommand: readonly string[];
|
|
4938
|
+
lastRunAt?: number;
|
|
4939
|
+
lastStatus?: ScheduleRunStatus;
|
|
4940
|
+
}
|
|
4941
|
+
/** 一次运行的记录 */
|
|
4942
|
+
interface ScheduleRun {
|
|
4943
|
+
id: string;
|
|
4944
|
+
scheduleId: string;
|
|
4945
|
+
/** 真的起了会话才有;三种 `skipped-*` / `missed` 都没有 */
|
|
4946
|
+
sessionId?: string;
|
|
4947
|
+
startedAt: number;
|
|
4948
|
+
endedAt?: number;
|
|
4949
|
+
status: ScheduleRunStatus;
|
|
4950
|
+
exitCode?: number;
|
|
4951
|
+
reason?: string;
|
|
4952
|
+
costUsd: number;
|
|
4953
|
+
tokens?: TokenUsage;
|
|
4954
|
+
turns: number;
|
|
4955
|
+
/** 欠条。空数组 = 这一轮没撞上需要确认的操作 */
|
|
4956
|
+
pendingApprovals: readonly SchedulePendingApproval[];
|
|
4957
|
+
/** 录像(`<runId>.ndjson`)的绝对路径;没起会话时不带 */
|
|
4958
|
+
logPath?: string;
|
|
4806
4959
|
/**
|
|
4807
|
-
*
|
|
4808
|
-
*
|
|
4809
|
-
* 对不上 = 那些文件在授信之后被改过 → `exec` 退回未授,重新问一次。
|
|
4810
|
-
* `exec` 为假时这一格不该存在。
|
|
4811
|
-
*
|
|
4812
|
-
* ## ⚠️ 这是对 [manager.ts](../../core/src/trust/manager.ts) 文件头「两条明确不做」
|
|
4813
|
-
* 第 2 条的一次**窄口推翻**,不是把它作废
|
|
4814
|
-
*
|
|
4815
|
-
* 那一条 2026-08-08 写着:记内容 hash「代价是『改一行 AGENTS.md 就被打断一次』,
|
|
4816
|
-
* 这个打断比它挡住的风险更贵」。**那个结论在读类上照旧成立,一个字不改** ——
|
|
4817
|
-
* 指令文件天天在动,为它记 hash 会把授信变成一件每天都要重做的事。
|
|
4818
|
-
*
|
|
4819
|
-
* 翻的只有执行类那一半,因为它的两项输入都变了:
|
|
4820
|
-
*
|
|
4821
|
-
* | | 读类(AGENTS.md) | 执行类(hooks.json / policies) |
|
|
4822
|
-
* | --- | ---------------------- | -------------------------------- |
|
|
4823
|
-
* | 改动频率 | 每天 | 几乎不动 |
|
|
4824
|
-
* | 改动后果 | 多一段文本 | **多一条会跑的命令** |
|
|
4960
|
+
* `epoch schedule run` 手动跑的那一次。
|
|
4825
4961
|
*
|
|
4826
|
-
*
|
|
4827
|
-
*
|
|
4828
|
-
* 而它的 `AGENTS.md` 压根不过闸门 —— 同一条判据的两侧,它也是这么分的。
|
|
4962
|
+
* 标出来是因为它**不该影响「上一次到点跑成没有」的判断** ——
|
|
4963
|
+
* 用户手点一次成功,不等于明天 9 点那次会成功。
|
|
4829
4964
|
*/
|
|
4830
|
-
|
|
4831
|
-
}
|
|
4832
|
-
/** {@link TrustManager.record} 的可选档位。不给 = 只授读类 */
|
|
4833
|
-
interface TrustGrant {
|
|
4834
|
-
/** 连执行类一起授。**调用方必须真的为这一档单独问过用户** */
|
|
4835
|
-
exec?: boolean;
|
|
4836
|
-
/** 授 `exec` 时一并记下的内容指纹 */
|
|
4837
|
-
execHash?: string;
|
|
4965
|
+
manual: boolean;
|
|
4838
4966
|
}
|
|
4839
4967
|
/**
|
|
4840
|
-
*
|
|
4968
|
+
* 宿主注入的「怎么以无界面方式唤起我自己」(方案 45 §2.6)。
|
|
4841
4969
|
*
|
|
4842
|
-
*
|
|
4843
|
-
*
|
|
4844
|
-
*
|
|
4970
|
+
* 把引擎 `import` 进 Electron / VS Code 扩展的宿主**没有 `epoch` 可执行文件**,
|
|
4971
|
+
* 而**库自己分辨不出宿主打算怎么被唤起** —— 所以这件事必须由宿主说。
|
|
4972
|
+
*
|
|
4973
|
+
* ⚠️ **绝对不许拿 `process.execPath` + `argv[1]` 去猜。** 在 Electron 里那两个值
|
|
4974
|
+
* 是「Electron 的 exe」+「宿主的 main.js」,注册进去的后果是**到点了给用户弹出
|
|
4975
|
+
* 整个应用的主窗口**(甚至一天弹 24 次)。那不是「不够好」,是把定时任务变成骚扰。
|
|
4845
4976
|
*/
|
|
4846
|
-
interface
|
|
4847
|
-
/** 记录里授过执行类吗 */
|
|
4848
|
-
granted: boolean;
|
|
4977
|
+
interface ScheduleRunnerSpec {
|
|
4849
4978
|
/**
|
|
4850
|
-
*
|
|
4979
|
+
* 无界面入口的 argv 前缀,绝对路径。我们在后面追加
|
|
4980
|
+
* `schedule fire <id> --home <dir>`。
|
|
4851
4981
|
*
|
|
4852
|
-
*
|
|
4853
|
-
*
|
|
4854
|
-
*/
|
|
4855
|
-
hash?: string;
|
|
4856
|
-
}
|
|
4857
|
-
interface TrustManager {
|
|
4858
|
-
/** 查某个目录的信任状态(含父目录的 directory-tree 继承) */
|
|
4859
|
-
check(dir: string): TrustLevel;
|
|
4860
|
-
/**
|
|
4861
|
-
* 查某个目录的执行类授信(含继承)。**和 {@link check} 是两个问题**:
|
|
4862
|
-
* 这一个只在 `check()` 回 `'trusted'` 时才可能授过,反过来不成立。
|
|
4982
|
+
* 那个入口必须**不建窗口、跑完就退**(Electron 里就是在 `whenReady` 之前
|
|
4983
|
+
* 分叉出去)。
|
|
4863
4984
|
*/
|
|
4864
|
-
|
|
4865
|
-
/**
|
|
4866
|
-
|
|
4867
|
-
list(): readonly TrustRecord[];
|
|
4868
|
-
revoke(dir: string): void;
|
|
4985
|
+
argv: readonly string[];
|
|
4986
|
+
/** 注册进 OS 时给人看的名字前缀。不给就用 `epoch` */
|
|
4987
|
+
label?: string;
|
|
4869
4988
|
}
|
|
4870
4989
|
|
|
4871
4990
|
/**
|
|
4872
|
-
*
|
|
4991
|
+
* 内置模板六张([方案 45](../../../docs/verify/VERIFY_RECORD-45-automation.md) §7.3)。
|
|
4873
4992
|
*
|
|
4874
|
-
*
|
|
4993
|
+
* 判据是「**这件事必须每天/每周做一次,而且做完是一份能读的东西**」——
|
|
4994
|
+
* 所以 WorkBuddy 那 12 张消费级的(儿童睡前故事 / 父母联系提醒 / 萌宠壁纸)
|
|
4995
|
+
* 一张都没抄,换成开发者场景。
|
|
4875
4996
|
*
|
|
4876
|
-
*
|
|
4877
|
-
* 这个「依赖倒置接口」全都直接用 `ModelMessage`。后果有三个:
|
|
4878
|
-
* 1. 依赖倒置是假的 —— 想塞一个脚本化的假 provider 做测试,得先 import 'ai'
|
|
4879
|
-
* 2. AI SDK 的消息形状是**联合类型 + 部件数组**,`content` 可能是 string
|
|
4880
|
-
* 也可能是 `Array<{type:'text'|'tool-call'|...}>`,于是 loop 里到处
|
|
4881
|
-
* `as Array<Record<string, unknown>>` 地摸字段
|
|
4882
|
-
* 3. SDK 换大版本(v5→v6→v7 每次都动过消息形状)要改的是整个 agent 层,
|
|
4883
|
-
* 而不是一个转换函数
|
|
4997
|
+
* ## ⚠️ 三条硬约束都摊在类型上,不靠注释
|
|
4884
4998
|
*
|
|
4885
|
-
*
|
|
4886
|
-
*
|
|
4999
|
+
* | §7.3 那句话 | 落在类型的哪一格 |
|
|
5000
|
+
* | ------------------------------- | -------------------------------------------------------------------- |
|
|
5001
|
+
* | 模板**不联网** | 这是一份 `const`,没有取数入口 —— 在线模板市场 42 已定不做 |
|
|
5002
|
+
* | 一律不预设「跳过全部检查」 | {@link ScheduleTemplate.permission} 的取值里**没有** `bypass` |
|
|
5003
|
+
* | 填进表单后**必须让用户过一遍** | 整份形状里**没有** `maxBudgetUsd`,而它是必填 —— 表单因此保存不了 |
|
|
4887
5004
|
*
|
|
4888
|
-
*
|
|
5005
|
+
* 最后一条是三条里最要紧的,而它是**结构性**的而不是靠一句提示:预算必填、
|
|
5006
|
+
* 没有默认值(§3.5),于是一张模板卡填进表单之后那个保存按钮**天然是灰的**,
|
|
5007
|
+
* 用户不看完那一屏就出不去。写成「模板里预填 0.50 + 一句『请检查』」的话,
|
|
5008
|
+
* 「必须过一遍」就退化成一句没人读的话。
|
|
5009
|
+
*
|
|
5010
|
+
* ## 为什么住在 protocol,而不是「core 一份 + 这里抄一份」
|
|
5011
|
+
*
|
|
5012
|
+
* 任务书给的两条路是「从 server 端点下发」或者「抄一份到 protocol + 一条逐项相等
|
|
5013
|
+
* 的用例」(照 `WireScheduleIssueCode` 的先例)。**两条都不用,因为这一份和那个
|
|
5014
|
+
* 先例不是同一种东西**:
|
|
5015
|
+
*
|
|
5016
|
+
* - `WireScheduleIssueCode` 的真源在 core**必须**在 core —— 那张码表是
|
|
5017
|
+
* `validateSchedule()` 的出口集合,它和判定逻辑长在一起,搬不动;
|
|
5018
|
+
* - 而这一份是**纯数据、零判定**:六个触发器、六档权限、六份最小清单。
|
|
5019
|
+
* 它谁都不依赖。而 [AGENTS.md](../../../AGENTS.md) 的分层表里 protocol 那一行
|
|
5020
|
+
* 写着「跨包共享的东西**只能**放这里」。
|
|
5021
|
+
*
|
|
5022
|
+
* 放这儿的直接收益是**不存在第二份**:`packages/web` 够得着它(只依赖
|
|
5023
|
+
* protocol + view)、cli 够得着、server 够得着、假服务端也够得着。抄一份到
|
|
5024
|
+
* protocol 再钉一条「逐项相等」的用例,是在为一个可以不存在的分叉修围栏;
|
|
5025
|
+
* 走一条 server 端点则要为一份**从不改变的常量**加一条 HTTP 往返和一个失败态
|
|
5026
|
+
* (「模板拉不下来」——而它明明就在包里)。
|
|
5027
|
+
*
|
|
5028
|
+
* ## ⚠️ 这里一个中文字都没有,措辞在 web 那份 catalog 里
|
|
5029
|
+
*
|
|
5030
|
+
* 名字 / 说明 / **提示词**三样都是文案:提示词要跟着用户的界面语言走
|
|
5031
|
+
* (一个英文界面的用户不该收到一份中文报告),而 protocol 里没有 `t()`、
|
|
5032
|
+
* 也不该有。所以这一份只给 `id`,措辞表在
|
|
5033
|
+
* `web/src/automation/labels.ts` 的 `TEMPLATE_TEXT` 上 —— 那张表
|
|
5034
|
+
* `satisfies Record<ScheduleTemplateId, …>`,**加一张模板而不给它文案会当场编译不过**。
|
|
4889
5035
|
*/
|
|
4890
5036
|
|
|
4891
|
-
/**
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
/**
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
5037
|
+
/**
|
|
5038
|
+
* 六张模板的 id。
|
|
5039
|
+
*
|
|
5040
|
+
* 它同时是**文案表的键**(web 那侧 `satisfies Record<ScheduleTemplateId, …>`),
|
|
5041
|
+
* 所以这个联合就是「有几张模板」的唯一真源 —— 加一张要改的地方由编译器点出来。
|
|
5042
|
+
*/
|
|
5043
|
+
type ScheduleTemplateId = 'repo-sweep' | 'weekly-digest' | 'dep-checkup' | 'stale-docs' | 'test-flake' | 'todo-digest';
|
|
5044
|
+
/**
|
|
5045
|
+
* 一张模板 = 一条任务除了「名字 / 提示词 / 预算」之外的那些格子。
|
|
5046
|
+
*
|
|
5047
|
+
* 三样刻意缺席,各有各的理由:
|
|
5048
|
+
*
|
|
5049
|
+
* - `name` / `prompt` —— 是**文案**,见文件头最后一节;
|
|
5050
|
+
* - `maxBudgetUsd` —— 必填且没有默认值(§3.5),见文件头那张表第三行;
|
|
5051
|
+
* - `workDir` —— 模板不知道用户的仓库在哪儿。留空的语义是
|
|
5052
|
+
* `~/.epoch/automation/<id>/`(`ScheduleDefinition.workDir` 上的判据),
|
|
5053
|
+
* 而**清单里那条 `file_write(./reports/**)` 是相对的**,所以它跟着工作区走 ——
|
|
5054
|
+
* 不管用户填不填,报告都落在那条任务自己的目录下面。
|
|
5055
|
+
*/
|
|
5056
|
+
interface ScheduleTemplate {
|
|
5057
|
+
id: ScheduleTemplateId;
|
|
5058
|
+
trigger: ScheduleTrigger;
|
|
4904
5059
|
/**
|
|
4905
|
-
*
|
|
4906
|
-
*
|
|
4907
|
-
*
|
|
5060
|
+
* 权限档。**取值里没有 `bypass`**(§7.3:模板一律不预设「跳过全部检查」),
|
|
5061
|
+
* 也没有 `auto`(界面压根不暴露它,§3.2)。
|
|
5062
|
+
*
|
|
5063
|
+
* 两档各有各的用法,而分界线是 by-level 那张表:
|
|
5064
|
+
*
|
|
5065
|
+
* - `plan`(只读)—— 「读一遍然后写一份报告」的那几张。它天然零授权面,
|
|
5066
|
+
* 清单里只要一条 `file_write`(那条走**第 2 层**规则匹配,所以在只读档下
|
|
5067
|
+
* 照样放行 —— §3.2 那句「只读 + `file_write(<任务目录>/**)`」说的就是它);
|
|
5068
|
+
* - `default`(按需确认)—— 要跑命令的那几张。**除了 `bypass`,写命令在每一档
|
|
5069
|
+
* 下都要确认**,所以它们只能靠清单逐条放行,而这正是清单存在的理由。
|
|
4908
5070
|
*/
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
5071
|
+
permission: 'plan' | 'default' | 'acceptEdits';
|
|
5072
|
+
/** 整个工具免确认(粗)。多数模板不用它 —— 逐条列出来的清单才审得动 */
|
|
5073
|
+
allowTools?: readonly string[];
|
|
5074
|
+
allowOperations?: readonly OperationType[];
|
|
5075
|
+
/** 规则(细)。⚠️ **路径一律写成相对的**,理由见 {@link ScheduleTemplate} */
|
|
5076
|
+
allowRules?: readonly string[];
|
|
5077
|
+
/**
|
|
5078
|
+
* 墙钟超时(毫秒)。不给就用引擎缺省的 15 分钟。
|
|
5079
|
+
*
|
|
5080
|
+
* 只有「测试 flake 复盘」给了 —— 它真的要跑一遍测试,而缺省那把尺子会在
|
|
5081
|
+
* 半路把它砍掉。**这一格存在的意义就是让那一张不至于生下来就超时**。
|
|
5082
|
+
*/
|
|
5083
|
+
timeoutMs?: number;
|
|
4915
5084
|
}
|
|
4916
5085
|
/**
|
|
4917
|
-
*
|
|
4918
|
-
*
|
|
4919
|
-
*
|
|
4920
|
-
* 字段名是 `mediaType` 不是 `mimeType` —— 与 `@ai-sdk/provider-utils` 的
|
|
4921
|
-
* `ImagePart` 对齐,省掉边界上的一次改名。
|
|
5086
|
+
* 六张。顺序就是界面上卡片的顺序:**两张每天的排在前面** ——
|
|
5087
|
+
* 「每天一次」比「每周一次」更容易让人第一次就试出效果,而这一屏的空状态
|
|
5088
|
+
* 要答的问题是「这东西能替我做什么」。
|
|
4922
5089
|
*/
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
image: string;
|
|
4926
|
-
mediaType?: string;
|
|
4927
|
-
}
|
|
4928
|
-
/** 附件为什么只有路径没有内容 */
|
|
4929
|
-
type FileOmitReason =
|
|
4930
|
-
/** 探测到 NUL 字节,是二进制 */
|
|
4931
|
-
'binary'
|
|
4932
|
-
/** 权限判定没放行(与 `file_read` 同一条判定) */
|
|
4933
|
-
| 'denied'
|
|
4934
|
-
/** 读不到(不存在 / 没权限 / 是目录) */
|
|
4935
|
-
| 'unreadable'
|
|
4936
|
-
/** 这一条消息附的文件个数或总量超了上限 */
|
|
4937
|
-
| 'over-limit'
|
|
5090
|
+
declare const SCHEDULE_TEMPLATES: readonly ScheduleTemplate[];
|
|
5091
|
+
|
|
4938
5092
|
/**
|
|
4939
|
-
*
|
|
5093
|
+
* 「还欠着的那几张欠条」——
|
|
5094
|
+
* [方案 45](../../../docs/verify/VERIFY_RECORD-45-automation.md) §八 ① / ③ 的素材。
|
|
5095
|
+
*
|
|
5096
|
+
* 三个界面读的是同一个数:dock 上那一条「自动化待确认」、侧栏「自动化」入口上
|
|
5097
|
+
* 那个点、TUI 启动时那一行。**它们必须是同一个数** —— 三处各数一遍的下场是
|
|
5098
|
+
* 侧栏有点、点进去 dock 上什么都没有。
|
|
5099
|
+
*
|
|
5100
|
+
* ## 一、为什么这份投影住在 protocol
|
|
4940
5101
|
*
|
|
4941
|
-
*
|
|
4942
|
-
*
|
|
4943
|
-
*
|
|
4944
|
-
* `
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
/**
|
|
4948
|
-
* 文件附件部件 —— 用户用 `@路径` 提到的文件(方案 25)。
|
|
5102
|
+
* 它是**纯集合运算**:手上有哪几条任务、每条最近跑过什么、清单里已经有哪几条规则。
|
|
5103
|
+
* 没有日期算术、不读盘、不查权限,一个字的 core 知识都不需要。
|
|
5104
|
+
* 而它的消费者有四个:`server`(那条端点)、`cli`(`--fix` 和 TUI 那行开机提示)、
|
|
5105
|
+
* `packages/web/dev` 那台假服务端、以及将来任何一个嵌入宿主。
|
|
5106
|
+
* [AGENTS.md](../../../AGENTS.md) 分层表里 protocol 那一行写着
|
|
5107
|
+
* 「跨包共享的东西**只能**放这里」,这就是那一行说的情况。
|
|
4949
5108
|
*
|
|
4950
|
-
*
|
|
5109
|
+
* ⚠️ **这一份落地之前,那个集合运算在仓库里有三份**(`server` 的 `fixSchedule`、
|
|
5110
|
+
* `cli` 的 `applyFix`、假服务端的 `fakeScheduleFix`),逐字一样的六行。
|
|
5111
|
+
* 三份都是「往授权清单里加规则」这条路上的判定 —— 也就是**提权路径上的重复实现**。
|
|
5112
|
+
* 收成一份是这个文件存在的第二个理由,比第一个理由更硬。
|
|
4951
5113
|
*
|
|
4952
|
-
*
|
|
4953
|
-
* 「用户打的字」和「附的文件」于是压错东西、token 统计把文件算成用户输入、
|
|
4954
|
-
* artifact 化那条路完全绕过、web 宿主收到的 wire 载荷里两者无法区分。
|
|
4955
|
-
* 单独一种部件之后,这四件事各自都有地方判。
|
|
5114
|
+
* ## 二、「还欠着」= 算得出规则、而且规则还不在清单里
|
|
4956
5115
|
*
|
|
4957
|
-
*
|
|
5116
|
+
* 两道筛,**缺一道这个数就会自己卡住**:
|
|
4958
5117
|
*
|
|
4959
|
-
*
|
|
4960
|
-
*
|
|
4961
|
-
*
|
|
5118
|
+
* 1. **`suggestedRule` 必须有值。** 算不出精确规则的那几条(目标为空的操作)
|
|
5119
|
+
* 进不了这份清单 —— dock 上唯一的动作是「加进授权清单」,而对它们那一下
|
|
5120
|
+
* 什么都加不了。一条点了没用的通知比没有通知坏(设计稿决定 20 ①),
|
|
5121
|
+
* 而且它**永远清不掉**:没有任何动作能让它消失。
|
|
5122
|
+
* ⚠️ 它们并没有被藏起来:「运行记录」那一屏上那一轮照旧列着它们、
|
|
5123
|
+
* 照旧写着「这一条反推不出精确规则」。这里筛掉的是**通知**,不是事实。
|
|
5124
|
+
* 2. **规则还不在 `allowRules` 里。** 用户按过那一下之后规则就进清单了,
|
|
5125
|
+
* 于是这个数**自己减一**。这条让整件事不需要「读标记」那套东西 ——
|
|
5126
|
+
* 没有迁移、没有「哪个界面算看过」这个产品判断、也没有一个只能靠
|
|
5127
|
+
* 「我看过了」来清的角标。
|
|
5128
|
+
*
|
|
5129
|
+
* ## ⚠️ 三、这个数**不含**「上一次没跑成」的那几条,而那是一条判断不是遗漏
|
|
5130
|
+
*
|
|
5131
|
+
* `failed` / `timeout` / `budget` 三档同样是「你该知道的事」,但它们**没有下一步
|
|
5132
|
+
* 动作**:屏幕上给不出一个按钮,用户只能去看运行记录。把它们算进来的话,这个点
|
|
5133
|
+
* 会一直亮着,直到那条任务下一次跑成 —— 而在那之前它已经不再指向任何可做的事,
|
|
5134
|
+
* 也就退化成了一个「有东西发生过」的常亮灯。
|
|
5135
|
+
*
|
|
5136
|
+
* 三档失败在「运行记录」那一屏上有自己的三态色(✅ ⚠️ ❌),一条都没藏。
|
|
5137
|
+
* 📮 真要给它们一个计数,前置是一份**读标记**(`schedule` 那个 namespace 加一次
|
|
5138
|
+
* 迁移 + 一条写端点 + 「web 打开那个 tab 算不算看过、TUI 启动算不算」这个产品
|
|
5139
|
+
* 判断)。那是独立的一次改动,不该顺手塞进这一轮。
|
|
4962
5140
|
*/
|
|
4963
|
-
|
|
4964
|
-
type: 'file';
|
|
4965
|
-
/** 用户敲的那个路径(相对工作区,`/` 分隔),原样保留用于显示 */
|
|
4966
|
-
path: string;
|
|
4967
|
-
/** 文件内容。**缺席时看 `omitted`** */
|
|
4968
|
-
text?: string;
|
|
4969
|
-
/** 没给内容的原因;给了 `text` 时不出现 */
|
|
4970
|
-
omitted?: FileOmitReason;
|
|
4971
|
-
/** 内容被截断到多少字节。给了就是截断过 —— 截断**永远不静默** */
|
|
4972
|
-
truncatedTo?: number;
|
|
4973
|
-
/** 文件原始字节数,用于展示 */
|
|
4974
|
-
bytes?: number;
|
|
4975
|
-
}
|
|
5141
|
+
|
|
4976
5142
|
/**
|
|
4977
|
-
*
|
|
5143
|
+
* 一条**算得出规则**的欠条。
|
|
4978
5144
|
*
|
|
4979
|
-
*
|
|
4980
|
-
*
|
|
4981
|
-
* 那才是同构位置的唯一真源。
|
|
5145
|
+
* `suggestedRule` 从可选收成必填 —— 这不是换个写法:它让「dock 上那个按钮
|
|
5146
|
+
* 一定加得进东西」变成一件类型上成立的事,而不是一句要人记住的注释。
|
|
4982
5147
|
*/
|
|
4983
|
-
type
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
/** 引用的是当前这段会话自己 */
|
|
4987
|
-
| 'self-reference'
|
|
4988
|
-
/** 超过每条消息能引用的会话个数 */
|
|
4989
|
-
| 'too-many'
|
|
4990
|
-
/** 会话可读性判定不放行(方案 48 的那一处判定,见 core/session/authorization.ts) */
|
|
4991
|
-
| 'not-authorized'
|
|
4992
|
-
/** 这一条消息的附件总量已经用完 */
|
|
4993
|
-
| 'budget-exceeded'
|
|
5148
|
+
type FixableApproval = SchedulePendingApproval & {
|
|
5149
|
+
suggestedRule: string;
|
|
5150
|
+
};
|
|
4994
5151
|
/**
|
|
4995
|
-
*
|
|
4996
|
-
*
|
|
4997
|
-
* 判据逐字同 {@link FileOmitReason} 的 `not-restored`:内容当初是过了可读性
|
|
4998
|
-
* 判定才附上的,而那次判定发生在另一个上下文里。恢复时无声地再读一遍等于让
|
|
4999
|
-
* 一次旧的授权在新的上下文里继续生效。模型仍然看得见「用户引用过这段会话」,
|
|
5000
|
-
* 要内容就自己调 `session_search`(方案 48 那条路,有工具审批)。
|
|
5152
|
+
* dock / 侧栏 / TUI 三处共用的一条。
|
|
5001
5153
|
*
|
|
5002
|
-
*
|
|
5154
|
+
* 带 `scheduleName` 而不是只带 id:这条通知的措辞必须**有宾语**
|
|
5155
|
+
* (「《每日仓库巡检》差一条授权」,不是「有 1 条待确认」)——
|
|
5156
|
+
* 判据和跨会话待办条那一条同源(`web/src/dock/crossbar.tsx` 文件头
|
|
5157
|
+
* 「措辞必须带宾语」那一节):一个不带宾语的计数器逼用户挨个点开去看。
|
|
5003
5158
|
*/
|
|
5004
|
-
|
|
5159
|
+
interface SchedulePendingItem {
|
|
5160
|
+
scheduleId: string;
|
|
5161
|
+
scheduleName: string;
|
|
5162
|
+
/** 欠条挂在哪一次运行上。`POST /api/schedules/:id/fix` 收的就是它 */
|
|
5163
|
+
runId: string;
|
|
5164
|
+
/** 那一次运行的开始时刻(epoch 毫秒)。界面上那句「已欠了多久」 */
|
|
5165
|
+
at: number;
|
|
5166
|
+
/** 还没进清单的那几条,**至少一条**(空的那几轮压根不进这份清单) */
|
|
5167
|
+
approvals: readonly FixableApproval[];
|
|
5168
|
+
}
|
|
5005
5169
|
/**
|
|
5006
|
-
*
|
|
5007
|
-
*
|
|
5008
|
-
* 为什么要稳定的码而不是一句话:headless / 嵌入宿主要能路由这些失败
|
|
5009
|
-
* (`docs/HEADLESS.md` 的退出码那套已经是这个思路),而且 TUI 和 Web 要各自渲染。
|
|
5170
|
+
* 这一轮的欠条里,有哪几条规则还不在清单里。**去重,顺序照欠条的顺序。**
|
|
5010
5171
|
*
|
|
5011
|
-
* `
|
|
5172
|
+
* 三个调用点:这个文件自己(算那个计数)、`POST /api/schedules/:id/fix`
|
|
5173
|
+
* (真往清单里写的那一下)、`epoch schedule run --fix`。
|
|
5174
|
+
* ⚠️ **三处必须是同一个函数**:它决定的是「往授权清单里加哪几条规则」,
|
|
5175
|
+
* 而三份实现走散的方向可能是提权(一边加了另一边没加)。
|
|
5012
5176
|
*/
|
|
5013
|
-
declare
|
|
5177
|
+
declare function unfixedRules(allowRules: readonly string[], approvals: readonly SchedulePendingApproval[]): string[];
|
|
5014
5178
|
/**
|
|
5015
|
-
*
|
|
5016
|
-
*
|
|
5017
|
-
* ## 塞进来的是那段会话的「当前面」,不是原始全量(§二)
|
|
5018
|
-
*
|
|
5019
|
-
* 一个跑了两小时、压缩过三次的会话,`messages` 表里有 800 条,但模型当时能看到
|
|
5020
|
-
* 的只有 60 条(其余 `active = 0`)。这里带的是**那 60 条**。三条理由:800 条
|
|
5021
|
-
* 任何预算都塞不下;那 60 条就是这段会话的结论(压缩器已经把前面浓缩进去了);
|
|
5022
|
-
* 原始全量里有已经被推翻的内容(「先试了 A 不行改用 B」,压缩之后留下的是 B)。
|
|
5179
|
+
* 手上那几条任务 + 最近那些运行 → 该通知的清单。
|
|
5023
5180
|
*
|
|
5024
|
-
* ##
|
|
5181
|
+
* ## ⚠️ 同一条任务只报**最近**那一次,不是每一次
|
|
5025
5182
|
*
|
|
5026
|
-
*
|
|
5027
|
-
*
|
|
5183
|
+
* 一条「每天 9 点」的任务每天记同一条欠条,一周就是七条一模一样的通知。
|
|
5184
|
+
* 所以按任务收拢、取最近那一次。这不会漏掉东西:修最近那一次加进去的规则
|
|
5185
|
+
* 通常把老的那几次也一起覆盖了;万一老的那次欠的是**另一条**规则,
|
|
5186
|
+
* 它在下一次取数时自然浮上来成为「最近的那条未解决的」。
|
|
5028
5187
|
*
|
|
5029
|
-
*
|
|
5188
|
+
* ⚠️ 判的是「最近的**还欠着的**那一次」,不是「最近的那一次」:一条任务昨天
|
|
5189
|
+
* 卡了、今天跑成了,昨天那条欠条**仍然欠着**(清单里那条规则还是没加),
|
|
5190
|
+
* 下一次到点还会再卡一遍。拿「最近一次的状态」当判据会把它静默吞掉。
|
|
5030
5191
|
*
|
|
5031
|
-
*
|
|
5032
|
-
*
|
|
5033
|
-
* `session_search`(方案 48)。这保证了 `@:` 永远不会成为一条比 `session_search`
|
|
5034
|
-
* **更宽松**的读取通道,和 `@文件` 对 `file_read` 那条判据逐字对应。
|
|
5192
|
+
* @param defs 全部任务(`ScheduleControl.list()`)
|
|
5193
|
+
* @param runs 最近那些运行,**最新的在前**(`ScheduleControl.recentRuns()` 的顺序)
|
|
5035
5194
|
*/
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
/** 用户敲的那个引用串(`@:` 后面那一截),原样保留用于显示 */
|
|
5039
|
-
ref: string;
|
|
5040
|
-
/** 匹配到的会话 id。**没匹配上时缺席** */
|
|
5041
|
-
sessionId?: string;
|
|
5042
|
-
/** 那段会话的标题(空标题时是 sessionId,见方案 53 §5.3) */
|
|
5043
|
-
title?: string;
|
|
5044
|
-
/** 那段会话的工作目录,用于让人 / 模型看出「这是哪个项目的会话」 */
|
|
5045
|
-
cwd?: string;
|
|
5046
|
-
/** 当前面的正文。**缺席时看 `omitted`** */
|
|
5047
|
-
text?: string;
|
|
5048
|
-
/** 没给内容的原因;给了 `text` 时不出现 */
|
|
5049
|
-
omitted?: SessionOmitReason;
|
|
5050
|
-
/** 内容被截断到多少字节。给了就是截断过 —— 截断**永远不静默** */
|
|
5051
|
-
truncatedTo?: number;
|
|
5052
|
-
/** 当前面有多少条消息(`active = 1` 的那些) */
|
|
5053
|
-
messageCount?: number;
|
|
5054
|
-
/** 那段会话一共多少条(含被压掉的)。两个数不等就说明它压缩过 */
|
|
5055
|
-
totalMessages?: number;
|
|
5056
|
-
}
|
|
5057
|
-
type EpochContentPart = EpochTextPart | EpochImagePart | EpochFilePart | EpochSessionPart;
|
|
5058
|
-
/** 用户消息内容 —— 纯文本或多模态部件数组 */
|
|
5059
|
-
type EpochUserContent = string | EpochContentPart[];
|
|
5195
|
+
declare function collectPendingApprovals(defs: readonly ScheduleDefinition[], runs: readonly ScheduleRun[]): SchedulePendingItem[];
|
|
5196
|
+
|
|
5060
5197
|
/**
|
|
5061
|
-
*
|
|
5198
|
+
* 模型在对话里交上来的一份定时任务**草稿**。
|
|
5062
5199
|
*
|
|
5063
|
-
*
|
|
5064
|
-
*
|
|
5065
|
-
*
|
|
5066
|
-
|
|
5067
|
-
declare function filePartSummary(part: EpochFilePart): string;
|
|
5068
|
-
/**
|
|
5069
|
-
* 历史里的图被**摘掉**之后留在原位的那一行(2026-09-12)。
|
|
5200
|
+
* `epoch schedule add` 和自动化面板那张表单是两条**人操作**的写入路。这一份是
|
|
5201
|
+
* 第三条来路,而它**不是写入路** —— 草稿谁都落不了库,它只是一份摆到人眼前的
|
|
5202
|
+
* 提案。真正落库的仍然是那两条:人点「审阅并保存」之后走的是同一个
|
|
5203
|
+
* `POST /api/schedules`,人粘贴命令行走的是同一个 `epoch schedule add`。
|
|
5070
5204
|
*
|
|
5071
|
-
* ##
|
|
5205
|
+
* ## ⚠️ 三条硬约束都摊在类型上,不靠注释(照 [schedule-templates.ts](./schedule-templates.js) 那三条办)
|
|
5072
5206
|
*
|
|
5073
|
-
*
|
|
5074
|
-
*
|
|
5075
|
-
* `
|
|
5076
|
-
*
|
|
5077
|
-
*
|
|
5078
|
-
* 地吞掉**(后者实测存在,判据全文在 `core/provider/capability-probe.ts` 的
|
|
5079
|
-
* 文件头)。摘掉它们的是 `AgentLoop`,留下的那一行是这一个。
|
|
5207
|
+
* | 要守住的 | 落在类型的哪一格 |
|
|
5208
|
+
* | ------------------------------ | -------------------------------------------------------------------- |
|
|
5209
|
+
* | 模型不许把任务放到「跳过全部检查」 | {@link ScheduleDraftPermission} 的取值里**没有** `bypass` |
|
|
5210
|
+
* | 填进表单后**必须让用户过一遍** | 整份形状里**没有** `maxBudgetUsd`,而它是必填 —— 表单因此保存不了 |
|
|
5211
|
+
* | 草稿不是写入路 | 这里只有数据,没有 id 生成、没有 `enabled`、没有 `createdAt` |
|
|
5080
5212
|
*
|
|
5081
|
-
*
|
|
5213
|
+
* 第二条是三条里最要紧的,而它和模板那一条**是同一条**:预算必填、没有默认值
|
|
5214
|
+
* (方案 45 §3.5),于是一份草稿填进表单之后那个保存按钮**天然是灰的**,
|
|
5215
|
+
* 用户不看完那一屏就出不去。写成「草稿里预填 0.50 + 一句『请检查』」的话,
|
|
5216
|
+
* 「必须过一遍」就退化成一句没人读的话 —— 而这一格错了的后果很具体:
|
|
5217
|
+
* 一觉醒来 40 美元。
|
|
5082
5218
|
*
|
|
5083
|
-
*
|
|
5084
|
-
* 审计 / `messages.content` 那一列),而且它有前科:图片真的送达之后,它成了
|
|
5085
|
-
* 「唯一还在撒谎的地方」(判据在 {@link ContentToTextOptions.imagesCarriedAsParts})。
|
|
5086
|
-
* 这一行要说的是另一件事 —— 图**确实没给它**,以及为什么。
|
|
5219
|
+
* ## 为什么住在 protocol
|
|
5087
5220
|
*
|
|
5088
|
-
*
|
|
5089
|
-
*
|
|
5090
|
-
*
|
|
5221
|
+
* 和 `SCHEDULE_TEMPLATES` 同一个理由,而且更硬一档:这一份的**两端都要读它** ——
|
|
5222
|
+
* core 那侧要解析模型发来的入参(`tools/builtin/schedule-draft.ts`),
|
|
5223
|
+
* `packages/web` 那侧要从那次工具调用的入参里把它读回来渲染成一张卡片
|
|
5224
|
+
* (`thread/rows.ts`,照 `todo` 的先例)。web 只依赖 protocol + view,够不着 core。
|
|
5091
5225
|
*
|
|
5092
|
-
* ⚠️
|
|
5093
|
-
* 不同:那一档有 `file_read` 真的能拿到内容,这一档模型没有任何办法看见像素。
|
|
5094
|
-
* 给一个不存在的出口,比不给更糟。
|
|
5226
|
+
* ## ⚠️ 这里一个中文字都没有
|
|
5095
5227
|
*
|
|
5096
|
-
*
|
|
5228
|
+
* 判据同 schedule-templates.ts 最后一节:措辞在 `locales/*.yaml` 的
|
|
5229
|
+
* `web.ui.thread.draft_*` 底下(卡片)和 core 的工具输出里(给模型看的那半,
|
|
5230
|
+
* 按 `locales/zh.yaml` 的范围说明不进 catalog)。
|
|
5097
5231
|
*/
|
|
5098
|
-
|
|
5232
|
+
|
|
5099
5233
|
/**
|
|
5100
|
-
*
|
|
5101
|
-
*
|
|
5102
|
-
*
|
|
5103
|
-
*
|
|
5104
|
-
* 它的读者有三个,**没有一个是界面**:模型(经 sdk-adapter 进上下文)、
|
|
5105
|
-
* `messages.content` 那一列(存盘 / 压缩 / 审计 / FTS)、以及宿主的日志。
|
|
5106
|
-
* 界面要展示这一部件时读的是结构化字段(`omitted` / `title` / `messageCount`),
|
|
5107
|
-
* 自己组措辞 —— TUI 那一侧的中文在宿主用 `t()` 拼,见 locales 的 `mention.*`。
|
|
5108
|
-
*
|
|
5109
|
-
* > ⚠️ 「没有一个是界面」这句话 2026-08-24 之前**有个洞**:`contentToText()`
|
|
5110
|
-
* > 会把这一行塞进 tui / web 的气泡里,于是它事实上也是界面的。洞补在
|
|
5111
|
-
* > §34.4 —— 界面改走 `contentToDisplayText()`,那一份把这一行换成
|
|
5112
|
-
* > catalog 的 `message_part.session*`。所以上面那句话现在是真的了。
|
|
5113
|
-
*
|
|
5114
|
-
* 所以这里直接把**那五个稳定错误码**原样写进去(§4.2):宿主能 grep,模型能读,
|
|
5115
|
-
* 而且它不必跟着界面语言变 —— 一条落库的记录跟着 `EPOCH_LANG` 变语言,
|
|
5116
|
-
* 等于把同一段会话的历史劈成两半(`goal/wording.ts` 那条豁免记的是同一个坑)。
|
|
5234
|
+
* 那个工具的名字。**真源在这儿而不是 core**,因为读它的有两个包:
|
|
5235
|
+
* core 注册工具时用它,web 从工具流里认出那次调用时用它
|
|
5236
|
+
* (`thread/rows.ts`)。
|
|
5117
5237
|
*
|
|
5118
|
-
*
|
|
5119
|
-
*
|
|
5238
|
+
* ⚠️ 这一点上它和隔壁 `TODO_TOOL` 的做法不一样:那个名字今天在
|
|
5239
|
+
* `web/src/thread/rows.ts` 里另写了一份字面量(那时 protocol 里没有落脚点)。
|
|
5240
|
+
* 别照那个抄 —— 两份字面量漂了的表现是「卡片再也不渲染了」,而 typecheck
|
|
5241
|
+
* 一声不响。
|
|
5120
5242
|
*/
|
|
5121
|
-
declare
|
|
5243
|
+
declare const SCHEDULE_DRAFT_TOOL = "schedule_draft";
|
|
5122
5244
|
/**
|
|
5123
|
-
*
|
|
5124
|
-
* 文件和引用的会话同样只折成一行占位,**不吐内容**。
|
|
5125
|
-
*
|
|
5126
|
-
* 投影是**存盘、压缩、审计**看的那一份。把几十 KB 的文件内容塞进来,
|
|
5127
|
-
* `sessions.db` 会跟着一起胖,而回放时那份内容本来就能从 `parts` 里拿。
|
|
5128
|
-
*
|
|
5129
|
-
* ## ⚠️ 这一份**只给模型和落库**,界面不许用它(RECORD-40-i18n §34.4)
|
|
5130
|
-
*
|
|
5131
|
-
* 改造前它一个人喂三类消费者,于是把界面切成 English 的人会在**自己那条消息的
|
|
5132
|
-
* 气泡里**看到 `[图片 image/png]` / `[文件 …:二进制文件,未附内容]`。
|
|
5133
|
-
* 这笔债在 §34.4 记了、§34.4 还掉,处置是**分家**:
|
|
5245
|
+
* 草稿能落在哪几档权限上 —— **`bypass` 不在里面**。
|
|
5134
5246
|
*
|
|
5135
|
-
*
|
|
5136
|
-
*
|
|
5137
|
-
*
|
|
5138
|
-
*
|
|
5139
|
-
* | `messages.content` 那一列(存盘 / 压缩 / 审计 / FTS) | **这一个** | 一条落库的记录跟着 `EPOCH_LANGUAGE` 变语言,等于把同一段会话的历史劈成两半 |
|
|
5140
|
-
* | 界面(tui 的历史项、web 的气泡) | `contentToDisplayText(content, t)` | 那一份走 catalog 的 `message_part.*`,住在两个宿主里 |
|
|
5247
|
+
* 不是「运行期会拒绝」,是那一档在类型上根本不存在(判据同
|
|
5248
|
+
* `ScheduleTemplate.permission`)。理由比模板那一侧更直接:`bypass` 要一次
|
|
5249
|
+
* 二次确认(`bypass-not-confirmed`),而那次确认的**对象是人**;让模型提议
|
|
5250
|
+
* 一个「等着人去勾确认框」的档位,等于把那道确认变成一道例行手续。
|
|
5141
5251
|
*
|
|
5142
|
-
*
|
|
5143
|
-
* (兜底文案挪给渲染端):这个包的依赖白名单是空的,`t()` 只能由有它的那一层调。
|
|
5252
|
+
* 用户自己想要那一档时照旧走表单 —— 那条路一个字都没改。
|
|
5144
5253
|
*/
|
|
5145
|
-
|
|
5254
|
+
type ScheduleDraftPermission = Exclude<PermissionLevel, 'bypass'>;
|
|
5255
|
+
/** 草稿里那份清单。三个字段各自**可选**:没提到的一格 = 「这一格我没意见」 */
|
|
5256
|
+
interface ScheduleDraftAllowlist {
|
|
5257
|
+
allowTools?: readonly string[];
|
|
5258
|
+
allowOperations?: readonly OperationType[];
|
|
5259
|
+
allowRules?: readonly string[];
|
|
5260
|
+
}
|
|
5261
|
+
/** 起草一条新任务 */
|
|
5262
|
+
interface ScheduleDraftCreate extends ScheduleDraftAllowlist {
|
|
5263
|
+
action: 'create';
|
|
5264
|
+
name: string;
|
|
5265
|
+
prompt: string;
|
|
5266
|
+
trigger: ScheduleTrigger;
|
|
5267
|
+
/** 工作目录。不给就是引擎那份缺省(`~/.epoch/automation/<id>/`) */
|
|
5268
|
+
workDir?: string;
|
|
5269
|
+
permission: ScheduleDraftPermission;
|
|
5146
5270
|
/**
|
|
5147
|
-
*
|
|
5148
|
-
* base64,最终变成 provider 的原生图片部件)。
|
|
5149
|
-
*
|
|
5150
|
-
* 默认 `false` —— 也就是改造前的行为,落库那一列要的正是它:那一行
|
|
5151
|
-
* `[图片 image/png]` 是**这一轮带过一张图**的唯一记录(`parts` 列另存,
|
|
5152
|
-
* 但 FTS / 预览 / 标题只看 content)。
|
|
5153
|
-
*
|
|
5154
|
-
* `true` 时图片那一支整个**不折占位符**。判据是那句占位符的含义 ——
|
|
5155
|
-
* 「这里本来有张图,但你看不到」。图真的在旁边时它就成了一句假话,而且是
|
|
5156
|
-
* 会被照着信的那种:2026-09-01 实测,`@ai-sdk/deepseek@3.0.20` 把图片
|
|
5157
|
-
* 静默丢了之后,模型读到的就是这一行,于是它逐字回答「我只看到
|
|
5158
|
-
* 「[图片 image/jpeg]」这个占位符,图片没有传到我这边」。**升级 SDK 让图片
|
|
5159
|
-
* 真的送达之后,这一行反而成了唯一还在撒谎的地方。**
|
|
5271
|
+
* 为什么建议这一条,一句话。**给人看的**,会印在卡片上。
|
|
5160
5272
|
*
|
|
5161
|
-
*
|
|
5162
|
-
*
|
|
5163
|
-
* 而且是模型唯一能知道「有个东西没给我」的途径。
|
|
5273
|
+
* 必填,而这是刻意的:一条凭空出现在对话里的定时任务,用户第一个问题
|
|
5274
|
+
* 一定是「你为什么觉得我需要它」。答不上来的提案不该提。
|
|
5164
5275
|
*/
|
|
5165
|
-
|
|
5276
|
+
note: string;
|
|
5166
5277
|
}
|
|
5167
|
-
declare function contentToText(content: EpochUserContent, opts?: ContentToTextOptions): string;
|
|
5168
5278
|
/**
|
|
5169
|
-
*
|
|
5170
|
-
*
|
|
5171
|
-
* ## 它修的是什么
|
|
5172
|
-
*
|
|
5173
|
-
* 喂模型的那份历史只有 user / assistant 两种角色的**文本**
|
|
5174
|
-
* (`runtime/src/agent-session.ts` 的累积、`session-store.ts` 的 `loadHistory()`),
|
|
5175
|
-
* 工具调用和结果一条都不在里面 —— 这是刻意的(孤立的 tool 消息会被 provider 拒掉,
|
|
5176
|
-
* 全量 transcript 是另一个体量的事)。代价此前没人算:一段五条消息的会话,
|
|
5177
|
-
* 模型看到的是**五条又长又具体的指令,配四句十来个字的「办完了」**。
|
|
5178
|
-
* 早期那几条指令的字数远大于「它已经做完了」的证据量,于是模型把它们当成还在
|
|
5179
|
-
* 生效的待办,回头去做 —— 而用户刚发的那一条被摊薄。实测在
|
|
5180
|
-
* `packages/runtime/__tests__/context-recency.test.ts`。
|
|
5181
|
-
*
|
|
5182
|
-
* 这一行就是补那份证据:不复原过程,只留一句「这一轮真的调了这些工具」。
|
|
5279
|
+
* 提议改一条**已有**的任务。
|
|
5183
5280
|
*
|
|
5184
|
-
*
|
|
5281
|
+
* 每一格都可选(`undefined` = 这一格不动),但至少要动一格 —— 一份什么都不改的
|
|
5282
|
+
* 提案是在浪费用户的一次点击。这条判定在 core 那侧
|
|
5283
|
+
* (`schedule/draft.ts` 的 `draft-update-empty`)。
|
|
5185
5284
|
*
|
|
5186
|
-
*
|
|
5187
|
-
* (判据逐字同 `runtime/src/session-file-changes.ts` 的边界 1)。而一条
|
|
5188
|
-
* `EpochToolResult` 存在就意味着它跑完了 —— 拿结果当输入,「执行过没有」这件事
|
|
5189
|
-
* 不需要另外判一次。
|
|
5285
|
+
* ## ⚠️ 「放宽」和别的改动在类型上长得一样,所以判定必须在别处
|
|
5190
5286
|
*
|
|
5191
|
-
*
|
|
5192
|
-
*
|
|
5193
|
-
*
|
|
5287
|
+
* 把超时从 10 分钟调到 30 分钟,和往清单里加一条 `terminal(rm:*)`,在这个形状里
|
|
5288
|
+
* 是同一种东西:几个可选字段。而后者是**提权**。所以两个宿主都必须自己把
|
|
5289
|
+
* 「这次提案放宽了什么」单独标出来(web 的卡片、core 的工具输出),
|
|
5290
|
+
* 不许把它混在一列改动里 —— 用户扫一眼就点保存的那一下,正是这条要防的。
|
|
5291
|
+
*/
|
|
5292
|
+
interface ScheduleDraftUpdate extends ScheduleDraftAllowlist {
|
|
5293
|
+
action: 'update';
|
|
5294
|
+
/** 改哪一条。模型得先用 `action: 'list'` 看见 id,界面上那串它猜不出来 */
|
|
5295
|
+
id: string;
|
|
5296
|
+
name?: string;
|
|
5297
|
+
prompt?: string;
|
|
5298
|
+
trigger?: ScheduleTrigger;
|
|
5299
|
+
workDir?: string;
|
|
5300
|
+
permission?: ScheduleDraftPermission;
|
|
5301
|
+
enabled?: boolean;
|
|
5302
|
+
maxTurns?: number;
|
|
5303
|
+
timeoutMs?: number;
|
|
5304
|
+
/** 同 {@link ScheduleDraftCreate.note},必填 */
|
|
5305
|
+
note: string;
|
|
5306
|
+
}
|
|
5307
|
+
type ScheduleDraft = ScheduleDraftCreate | ScheduleDraftUpdate;
|
|
5308
|
+
/**
|
|
5309
|
+
* 一次提案里「放宽了什么」的分类。
|
|
5194
5310
|
*
|
|
5195
|
-
*
|
|
5311
|
+
* 只有三类,而三类都是**授权面变大**的方向:档位往松走、清单多了条目、
|
|
5312
|
+
* 从停用变成启用。别往里加「超时变长」这种 —— 它花的是钱和时间,不是权限,
|
|
5313
|
+
* 而把两种东西混进同一枚警示标,那枚标就不再有信息量。
|
|
5196
5314
|
*
|
|
5197
|
-
*
|
|
5198
|
-
*
|
|
5199
|
-
*
|
|
5200
|
-
*
|
|
5315
|
+
* 判定在 core(`schedule/draft.ts` 的 `draftWidening`),渲染在 web
|
|
5316
|
+
* (`thread/schedule-draft.tsx`)—— 两侧各算一遍是刻意的,判据同
|
|
5317
|
+
* `web/src/thread/rows.ts` 文件头那条:web 读的是**模型发出的原始入参**,
|
|
5318
|
+
* 而不是 core 归一化之后的结论。
|
|
5319
|
+
*/
|
|
5320
|
+
type ScheduleWideningKind = 'permission' | 'allowlist' | 'enabled';
|
|
5321
|
+
|
|
5322
|
+
/**
|
|
5323
|
+
* 工作区信任契约。
|
|
5201
5324
|
*
|
|
5202
|
-
*
|
|
5203
|
-
*
|
|
5204
|
-
*
|
|
5325
|
+
* 为什么需要:clone 一个仓库就等于让它的 `AGENTS.md` 进 system prompt、
|
|
5326
|
+
* 它的 hook 配置被执行、它的 policy 覆盖本地权限判定。这是漏洞,不是缺功能。
|
|
5327
|
+
* 信任判定把「加载项目级配置」这件事收到一次显式用户决策后面。
|
|
5205
5328
|
*
|
|
5206
|
-
*
|
|
5207
|
-
*
|
|
5329
|
+
* 竞品:Codex `codex-rs/tui/src/onboarding/trust_directory.rs`;
|
|
5330
|
+
* Gemini CLI `packages/cli/src/config/trustedFolders.ts` + `packages/core/src/utils/trust.ts`。
|
|
5331
|
+
* 两家都只是读过的参照物:契约形状跟它们都不一样(上游是 TRUST_FOLDER / TRUST_PARENT /
|
|
5332
|
+
* DO_NOT_TRUST 三枚举 + 靠 TRUST_PARENT 取 dirname 来编码作用域,这里是三态字面量 +
|
|
5333
|
+
* 独立的 scope 字段 + decidedAt)。本文件是独立实现,不带 SPDX 头。
|
|
5334
|
+
* 判定证据见 docs/verify/VERIFY_RECORD-D-license-attribution.md
|
|
5208
5335
|
*
|
|
5209
|
-
*
|
|
5210
|
-
*
|
|
5336
|
+
* 这里只有类型,实现在 core —— 注意实现那边(`core/src/trust/manager.ts`)**是**
|
|
5337
|
+
* gemini 的衍生作品(搬了「归一成生效目录再取最长匹配」那条算法),带头、在 NOTICE 里。
|
|
5338
|
+
* 契约与实现的授权状态不同是对的,不是漏登记。
|
|
5211
5339
|
*/
|
|
5212
|
-
|
|
5340
|
+
/** 一个目录的信任状态 */
|
|
5341
|
+
type TrustLevel =
|
|
5342
|
+
/** 用户明确信任:加载项目指令 / hook / policy */
|
|
5343
|
+
'trusted'
|
|
5344
|
+
/** 用户明确拒绝:只当普通工作目录,不加载任何项目级配置 */
|
|
5345
|
+
| 'untrusted'
|
|
5346
|
+
/** 还没问过 */
|
|
5347
|
+
| 'unknown';
|
|
5348
|
+
/** 用户已经做过决策的信任状态(排除「还没问过」) */
|
|
5349
|
+
type DecidedTrustLevel = Exclude<TrustLevel, 'unknown'>;
|
|
5350
|
+
/** 信任决策的作用域 */
|
|
5351
|
+
type TrustScope =
|
|
5352
|
+
/** 只这个目录 */
|
|
5353
|
+
'directory'
|
|
5354
|
+
/** 这个目录及其所有子目录 */
|
|
5355
|
+
| 'directory-tree';
|
|
5213
5356
|
/**
|
|
5214
|
-
*
|
|
5215
|
-
*
|
|
5216
|
-
* ## 为什么这条胶水也得住在这儿
|
|
5217
|
-
*
|
|
5218
|
-
* 因为它有**两个调用方,走两条路,产出必须逐字节相同**:
|
|
5357
|
+
* 一次授信授到哪一档(2026-09-11)。**两档是相加的:`exec` 蕴含 `read`。**
|
|
5219
5358
|
*
|
|
5220
|
-
*
|
|
5221
|
-
* - 恢复:`runtime/src/session-store.ts` 的 `loadHistory()` 从 DB 行折回来的那一轮。
|
|
5359
|
+
* ## 为什么拆
|
|
5222
5360
|
*
|
|
5223
|
-
*
|
|
5224
|
-
*
|
|
5225
|
-
*
|
|
5226
|
-
*
|
|
5361
|
+
* 在它之前 `trusted` 是一个布尔,而那一个布尔后面挂着七样东西 —— 其中
|
|
5362
|
+
* `.epoch/hooks.json` 和 `.claude/settings.json` 的每一项都是一条要 spawn 子进程的
|
|
5363
|
+
* 命令(判据在 [core/hook/sources.ts](../../core/src/hook/sources.ts) 的文件头)。
|
|
5364
|
+
* 而用户被问到的那句话逐字是「本目录的项目指令会加载」(`trust_prompt.granted`),
|
|
5365
|
+
* **从来没提过 hook**。也就是说「往上下文里注入一段文本」和「在这台机器上执行
|
|
5366
|
+
* 任意命令」共用了同一次点击,而问话只描述了前者。这不是 UX 问题,是知情同意错位。
|
|
5227
5367
|
*
|
|
5228
|
-
*
|
|
5229
|
-
*
|
|
5368
|
+
* | 档 | 放开什么 | 风险 |
|
|
5369
|
+
* | --- | --------------------------------------------------------------- | ---------- |
|
|
5370
|
+
* | `read` | 指令文件 / 技能 / 角色 / 命令 | 注入上下文 |
|
|
5371
|
+
* | `exec` | `hooks.json` / `.claude/settings*.json` / `policies/` / settings 的 allow 那一半 | 执行、放宽判定 |
|
|
5230
5372
|
*
|
|
5231
|
-
*
|
|
5232
|
-
*/
|
|
5233
|
-
declare function assistantTurnText(text: string, results: readonly Pick<EpochToolResult, 'toolName'>[]): string;
|
|
5234
|
-
/**
|
|
5235
|
-
* 部件类型的穷尽性守卫 —— **这是「加了一种部件却漏了一处分支」的那道会红的门禁**。
|
|
5373
|
+
* ## ⚠️ 两条边界,别把它当别的东西
|
|
5236
5374
|
*
|
|
5237
|
-
*
|
|
5238
|
-
*
|
|
5239
|
-
*
|
|
5240
|
-
*
|
|
5241
|
-
*
|
|
5375
|
+
* 1. **`exec` 不是权限档。** 它放开的是「这个仓库自带的 hook / policy 算不算数」,
|
|
5376
|
+
* 不是「这次操作要不要问用户」——[两套机制照旧正交](../../core/src/trust/gate.ts)。
|
|
5377
|
+
* 未授 `exec` 的目录里 agent 照样能跑 bash,只是仓库里那份 `hooks.json` 不生效;
|
|
5378
|
+
* 2. **`read` 不是「安全档」。** 一份 `AGENTS.md` 能指挥 agent 去做任何它有权限做的
|
|
5379
|
+
* 事,包括执行命令。拆开不是因为 `read` 无害,是因为**两者的问话不该合并成一次**。
|
|
5242
5380
|
*/
|
|
5243
|
-
|
|
5244
|
-
|
|
5381
|
+
type TrustTier = 'read' | 'exec';
|
|
5382
|
+
interface TrustRecord {
|
|
5383
|
+
/** 绝对路径(已 resolve) */
|
|
5384
|
+
path: string;
|
|
5385
|
+
level: DecidedTrustLevel;
|
|
5386
|
+
/**
|
|
5387
|
+
* 这条决策管到哪儿。
|
|
5388
|
+
*
|
|
5389
|
+
* `check()` 会把它折叠进 `TrustLevel`,所以判定方用不上;但**列出记录**的一方
|
|
5390
|
+
* (`epoch trust` 要显示「仅此目录 / 含子目录」)必须能看见 —— 少了它,
|
|
5391
|
+
* `list()` 返回的就不是一条完整的决策,而是一条读不懂的决策。
|
|
5392
|
+
*/
|
|
5393
|
+
scope: TrustScope;
|
|
5394
|
+
/** 记录时间戳(ms) */
|
|
5395
|
+
decidedAt: number;
|
|
5396
|
+
/**
|
|
5397
|
+
* 执行类也授了吗({@link TrustTier})。`level: 'untrusted'` 时无意义。
|
|
5398
|
+
*
|
|
5399
|
+
* ⚠️ **缺省(`undefined`)= 没授**,而这一格正是存量记录的迁移:拆档之前写下的
|
|
5400
|
+
* 记录没有这一格,于是升级之后它们**只剩读类** —— 判据就是上面那张表下面那段:
|
|
5401
|
+
* 那些记录是在一句只说了「项目指令会加载」的问话下拿到的,把它们追认成执行类
|
|
5402
|
+
* 等于替用户补签一份他没看过的同意书。代价(老用户的项目 hook 会停)由
|
|
5403
|
+
* `trust_gate.exec_migrated` 那条诊断明说,不静默。
|
|
5404
|
+
*/
|
|
5405
|
+
exec?: boolean;
|
|
5406
|
+
/**
|
|
5407
|
+
* 授 `exec` 那一刻,执行类输入文件的内容指纹(见 core 的 `trust/exec-hash.ts`)。
|
|
5408
|
+
*
|
|
5409
|
+
* 对不上 = 那些文件在授信之后被改过 → `exec` 退回未授,重新问一次。
|
|
5410
|
+
* `exec` 为假时这一格不该存在。
|
|
5411
|
+
*
|
|
5412
|
+
* ## ⚠️ 这是对 [manager.ts](../../core/src/trust/manager.ts) 文件头「两条明确不做」
|
|
5413
|
+
* 第 2 条的一次**窄口推翻**,不是把它作废
|
|
5414
|
+
*
|
|
5415
|
+
* 那一条 2026-08-08 写着:记内容 hash「代价是『改一行 AGENTS.md 就被打断一次』,
|
|
5416
|
+
* 这个打断比它挡住的风险更贵」。**那个结论在读类上照旧成立,一个字不改** ——
|
|
5417
|
+
* 指令文件天天在动,为它记 hash 会把授信变成一件每天都要重做的事。
|
|
5418
|
+
*
|
|
5419
|
+
* 翻的只有执行类那一半,因为它的两项输入都变了:
|
|
5420
|
+
*
|
|
5421
|
+
* | | 读类(AGENTS.md) | 执行类(hooks.json / policies) |
|
|
5422
|
+
* | --- | ---------------------- | -------------------------------- |
|
|
5423
|
+
* | 改动频率 | 每天 | 几乎不动 |
|
|
5424
|
+
* | 改动后果 | 多一段文本 | **多一条会跑的命令** |
|
|
5425
|
+
*
|
|
5426
|
+
* 也就是说,同一个做法在这一侧的打断成本低一个量级、挡住的东西贵一个量级。
|
|
5427
|
+
* 参照物:codex 对 hook 记 `trusted_hash`(`codex-rs/config/src/hook_config.rs`),
|
|
5428
|
+
* 而它的 `AGENTS.md` 压根不过闸门 —— 同一条判据的两侧,它也是这么分的。
|
|
5429
|
+
*/
|
|
5430
|
+
execHash?: string;
|
|
5431
|
+
}
|
|
5432
|
+
/** {@link TrustManager.record} 的可选档位。不给 = 只授读类 */
|
|
5433
|
+
interface TrustGrant {
|
|
5434
|
+
/** 连执行类一起授。**调用方必须真的为这一档单独问过用户** */
|
|
5435
|
+
exec?: boolean;
|
|
5436
|
+
/** 授 `exec` 时一并记下的内容指纹 */
|
|
5437
|
+
execHash?: string;
|
|
5438
|
+
}
|
|
5245
5439
|
/**
|
|
5246
|
-
*
|
|
5247
|
-
*
|
|
5248
|
-
* `content` 永远是字符串(tool 消息为空串,内容在 `toolResults` 里),
|
|
5249
|
-
* 这样「取这条消息的文本」永远不需要判类型。
|
|
5440
|
+
* {@link TrustManager.checkExec} 的答案 —— **记录里写的,不含新鲜度校验**。
|
|
5250
5441
|
*
|
|
5251
|
-
*
|
|
5252
|
-
*
|
|
5253
|
-
* `
|
|
5254
|
-
* `parts` 只在 sdk-adapter 那一处被读走。两者的关系由 `contentToText` 保证。
|
|
5442
|
+
* 两格一起交出来是因为校验方(`core/src/trust/gate.ts`)需要拿 {@link hash} 去和
|
|
5443
|
+
* 现算的值比;拆成两次调用的话,两次之间可能命中**不同的记录**
|
|
5444
|
+
* (`bestRecord()` 是按最长匹配现算的),于是拿 A 记录的授信配上 B 记录的指纹。
|
|
5255
5445
|
*/
|
|
5256
|
-
interface
|
|
5257
|
-
|
|
5258
|
-
|
|
5446
|
+
interface TrustExecGrant {
|
|
5447
|
+
/** 记录里授过执行类吗 */
|
|
5448
|
+
granted: boolean;
|
|
5259
5449
|
/**
|
|
5260
|
-
*
|
|
5261
|
-
*
|
|
5450
|
+
* 授的时候记下的内容指纹。
|
|
5451
|
+
*
|
|
5452
|
+
* ⚠️ **`undefined` = 校验不了,而这一档照旧放行**(判据在 gate 的 `decide()` 上)。
|
|
5453
|
+
* 今天所有写入口都会带上它,所以这一档只可能来自手改 `trust.json`。
|
|
5262
5454
|
*/
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
/**
|
|
5267
|
-
|
|
5455
|
+
hash?: string;
|
|
5456
|
+
}
|
|
5457
|
+
interface TrustManager {
|
|
5458
|
+
/** 查某个目录的信任状态(含父目录的 directory-tree 继承) */
|
|
5459
|
+
check(dir: string): TrustLevel;
|
|
5268
5460
|
/**
|
|
5269
|
-
*
|
|
5270
|
-
*
|
|
5271
|
-
* **只给人看,不回灌模型**:`sdk-adapter.ts` 一个字都不读它。加它是因为
|
|
5272
|
-
* 刷新页面之前屏幕上有「深度思考」那一段,刷新之后整段消失 —— 而那不是
|
|
5273
|
-
* 「这一轮没想」,是我们没记。
|
|
5274
|
-
*
|
|
5275
|
-
* ⚠️ **它属于某一步,不属于整轮。** 一轮里模型可以想一次、调个工具、再想一次,
|
|
5276
|
-
* 每次的思考挂在**它自己那条 assistant 行**上,位置就是它真实的时序位置。
|
|
5277
|
-
* 拿它当「整轮的思考」拼成一根,回放时那一段会被钉到最前面,讲的却可能是
|
|
5278
|
-
* 二十步之后的事(web 的 `thoughtParts` 治过同一个病,判据在
|
|
5279
|
-
* `web/src/state/reducer.ts` 的 `trackThought()`)。
|
|
5280
|
-
*
|
|
5281
|
-
* 落盘走 `messages.reasoning` 列 —— 那一列**从会话库 v1 就在**,只是一直没人
|
|
5282
|
-
* 写过,所以这一档不需要迁移。
|
|
5461
|
+
* 查某个目录的执行类授信(含继承)。**和 {@link check} 是两个问题**:
|
|
5462
|
+
* 这一个只在 `check()` 回 `'trusted'` 时才可能授过,反过来不成立。
|
|
5283
5463
|
*/
|
|
5284
|
-
|
|
5464
|
+
checkExec(dir: string): TrustExecGrant;
|
|
5465
|
+
/** 记录一次决策 */
|
|
5466
|
+
record(dir: string, level: DecidedTrustLevel, scope: TrustScope, grant?: TrustGrant): void;
|
|
5467
|
+
list(): readonly TrustRecord[];
|
|
5468
|
+
revoke(dir: string): void;
|
|
5285
5469
|
}
|
|
5286
5470
|
|
|
5287
5471
|
/**
|
|
@@ -6016,8 +6200,20 @@ type WireAgentEvent = (FunctionKeys<PlainAgentEvent> extends never ? PlainAgentE
|
|
|
6016
6200
|
/**
|
|
6017
6201
|
* 一个会话的回合状态。
|
|
6018
6202
|
*
|
|
6019
|
-
* `running` 时第二个 `POST /messages`
|
|
6020
|
-
*
|
|
6203
|
+
* `running` 时第二个 `POST /messages` **不会再开一轮**:`AgentSession` 的历史累积
|
|
6204
|
+
* 发生在 `run()` 末尾,两轮并发跑会让历史顺序变成不确定的。
|
|
6205
|
+
*
|
|
6206
|
+
* ⚠️ 这句话的**后半截改过两次,别再照着旧版本推断**:
|
|
6207
|
+
*
|
|
6208
|
+
* | 时期 | 第二个 POST 的下场 |
|
|
6209
|
+
* | --- | --- |
|
|
6210
|
+
* | 方案 20 | 直接 409(那时只有一个会话,409 是唯一能给的答复) |
|
|
6211
|
+
* | 方案 30 §2.3 | **排队**,等这一轮跑完再开(`{ok:true, queued:true}`) |
|
|
6212
|
+
* | 2026-09-16 | 默认被**拌进正在跑的那一轮**(`steered: true`),排队只剩斜杠命令那一档 |
|
|
6213
|
+
*
|
|
6214
|
+
* 三版共同的不变量只有一条:**一个会话同一时刻只有一个 `run()`**。
|
|
6215
|
+
* 「409」和「排队」都是那条不变量在某一版的**实现**,不是契约。
|
|
6216
|
+
* 今天的两档分别在 `WireSendMessageResponse.steered` 和 `SessionHub.steerFrom` 上。
|
|
6021
6217
|
*/
|
|
6022
6218
|
type WireTurnState = 'idle' | 'running' | 'awaiting-approval' | 'awaiting-question';
|
|
6023
6219
|
/**
|
|
@@ -6127,7 +6323,8 @@ type WireHubEvent =
|
|
|
6127
6323
|
type: 'session-cooled';
|
|
6128
6324
|
}
|
|
6129
6325
|
/**
|
|
6130
|
-
* **会话列表变了** ——
|
|
6326
|
+
* **会话列表变了** —— 多了一段、少了一段,或者有一段被收进 / 移出归档
|
|
6327
|
+
* (2026-09-11;归档那一档 2026-09-16 补)。
|
|
6131
6328
|
*
|
|
6132
6329
|
* ## 它治的是「不是我建的那一段」
|
|
6133
6330
|
*
|
|
@@ -6156,12 +6353,25 @@ type WireHubEvent =
|
|
|
6156
6353
|
* (`reducer.ts` 的 `applyFrame`:别的会话的帧会进 `parked`,而 `parkFrame`
|
|
6157
6354
|
* 只认 `session-state` / `session-cooled`,别的 Hub 事件在那儿被丢掉)。
|
|
6158
6355
|
*
|
|
6159
|
-
* ## ⚠️
|
|
6356
|
+
* ## ⚠️ 「多一段 / 少一段」不等于「建了 / 删了」(2026-09-16 补的第三个产地)
|
|
6357
|
+
*
|
|
6358
|
+
* 原文写的是「它的两个产地都在**进出 Hub** 这条路上」,并据此把整条
|
|
6359
|
+
* `PATCH /api/sessions/:id` 划到覆盖范围之外(「压根不经过 Hub,只动 DB」)。
|
|
6360
|
+
* **那句推论是错的**:这一帧的宾语是**那条列表端点答出来的那几行**,不是 Hub 的表。
|
|
6361
|
+
* 而列表默认**不列归档的会话**,于是「收进归档」在列表上就是少一行、「取回」
|
|
6362
|
+
* 就是多一行 —— 和 DELETE / POST 那两个产地一模一样。
|
|
6160
6363
|
*
|
|
6161
|
-
*
|
|
6162
|
-
*
|
|
6163
|
-
*
|
|
6164
|
-
*
|
|
6364
|
+
* 症状是这一轮的正题(2026-09-16 用户实测):在设置页「会话」那一分区点「取回」,
|
|
6365
|
+
* 左边侧栏那份列表一动不动,**重启应用才看得见**取回来的那一段 —— 和上面那个
|
|
6366
|
+
* 飞书宿主的原话逐字同形。产地在 `server/src/api.ts` 的 `patchSession`,
|
|
6367
|
+
* **只在 `archived` 真的翻了的时候发**。
|
|
6368
|
+
*
|
|
6369
|
+
* ## ⚠️ 覆盖不到的那一档:**标题改了仍然不发这一帧**
|
|
6370
|
+
*
|
|
6371
|
+
* 同一条端点上的另一半:改标题不改变「列哪几段」,只改变某一行上的一格。
|
|
6372
|
+
* 于是另一个标签页上那一行的标题仍然会过期到下一次重取。📮 那一档要治得给它
|
|
6373
|
+
* 自己的产地(一帧带得动「哪一行的哪一格变了」的事件),别顺手挂到这一帧上 ——
|
|
6374
|
+
* 让所有标签页因为别人改了个名字就重取一次整份列表,是拿错误的粒度换方便。
|
|
6165
6375
|
*/
|
|
6166
6376
|
| {
|
|
6167
6377
|
type: 'sessions-changed';
|
|
@@ -7345,8 +7555,59 @@ interface WireConfigResponse {
|
|
|
7345
7555
|
* 而其中第一条是一句**关于浏览器在哪儿**的推断。
|
|
7346
7556
|
*/
|
|
7347
7557
|
nativeDirPicker: boolean;
|
|
7558
|
+
/**
|
|
7559
|
+
* **上次停留的那段会话**(2026-09-14)。首屏该开的是它,不是 {@link sessionId}。
|
|
7560
|
+
*
|
|
7561
|
+
* ## 它和 `sessionId` 的分工
|
|
7562
|
+
*
|
|
7563
|
+
* `sessionId` 是**这个进程**装配出来的引导会话,恒新、恒空 —— 首屏照它开,
|
|
7564
|
+
* 用户每启动一次就看见一段「未命名会话」,昨天那段活得自己去侧栏里翻。
|
|
7565
|
+
* 这一格是**上一个进程退出时界面停在哪儿**(服务端那份 `~/.epoch/last-sessions.json`
|
|
7566
|
+
* 记的,由 `PUT /api/last-session` 写)。第一方界面的口径是
|
|
7567
|
+
* `lastSessionId ?? sessionId`。
|
|
7568
|
+
*
|
|
7569
|
+
* ## ⚠️ 没记过 / 那段已经被删了 → **不带这个键**(同 `maxCostUsd` 那一族)
|
|
7570
|
+
*
|
|
7571
|
+
* 服务端在下发之前会按**和 404 同一份判据**查一次它还在不在,查不到就整个不带。
|
|
7572
|
+
* 所以客户端那句 `?? sessionId` 就是「找不到就开一段新的」,**不需要**第二段
|
|
7573
|
+
* 兜底逻辑,更不该自己再去 `GET /api/sessions/:id` 探一发 —— 那是同一个问题的
|
|
7574
|
+
* 第二个说法,而两个说法迟早不一致。
|
|
7575
|
+
*
|
|
7576
|
+
* ⚠️ 它是**可选键**,所以故意不在 {@link WIRE_CONFIG_FIELDS} 上(见那张表上的 ⚠️)。
|
|
7577
|
+
*/
|
|
7578
|
+
lastSessionId?: string;
|
|
7348
7579
|
}
|
|
7349
7580
|
declare const WIRE_CONFIG_FIELDS: WireFields<WireConfigResponse>;
|
|
7581
|
+
/**
|
|
7582
|
+
* `PUT /api/last-session` —— 「我现在停在这段会话上」(2026-09-14)。
|
|
7583
|
+
* 服务端记下来,**下一次启动**的 {@link WireConfigResponse.lastSessionId} 就是它。
|
|
7584
|
+
*
|
|
7585
|
+
* ## 三件容易理解反的事
|
|
7586
|
+
*
|
|
7587
|
+
* 1. **URL 上没有 `:sessionId`,而且不该有。** 落点是 `~/.epoch/last-sessions.json`
|
|
7588
|
+
* ——一台机器一份、按项目根分格的**进程级**记录。挂到 `/sessions/:id/` 底下会
|
|
7589
|
+
* 得到一条名字是会话级、行为是机器级的端点(判据逐字同 `mcp/:name/reconnect`
|
|
7590
|
+
* 那一条)。会话 id 是**载荷**,因为它是被记的那个**值**。
|
|
7591
|
+
* 2. **它不改这段会话的任何东西**,也不要求它是活的 —— 只要求它在会话库里还在
|
|
7592
|
+
* (不在就 404,判据同别处那道 `unknown-session`)。校验在服务端的理由:
|
|
7593
|
+
* 客户端因此可以无脑上报,不必先探一发「这个 id 还有效吗」。
|
|
7594
|
+
* 3. **`PUT` 不是 `POST`。** 同一个项目反复报同一段会话是常态(每次切会话、
|
|
7595
|
+
* 每次刷新都报一次),而结果只有一个格子被覆盖 —— 幂等的写该是 `PUT`。
|
|
7596
|
+
* 非 GET 这件事本身也是必须的:它要过 `auth.ts` 那道 Origin 闸,因为它会往
|
|
7597
|
+
* 用户磁盘上写文件。
|
|
7598
|
+
*/
|
|
7599
|
+
interface WireLastSessionRequest {
|
|
7600
|
+
sessionId: string;
|
|
7601
|
+
}
|
|
7602
|
+
declare const WIRE_LAST_SESSION_FIELDS: WireFields<WireLastSessionRequest>;
|
|
7603
|
+
/**
|
|
7604
|
+
* 记下来了。**回执里不带 id** —— 带的话就等于在网线上多了一个「当前是哪段」的
|
|
7605
|
+
* 说法,而那件事的真源是 URL(`/s/<id>`),不是这条端点。
|
|
7606
|
+
*/
|
|
7607
|
+
interface WireLastSessionResponse {
|
|
7608
|
+
ok: true;
|
|
7609
|
+
}
|
|
7610
|
+
declare const WIRE_LAST_SESSION_RESPONSE_FIELDS: WireFields<WireLastSessionResponse>;
|
|
7350
7611
|
/**
|
|
7351
7612
|
* 列表里的一行**和工作区的关系** —— 三档(决定 18;`unknown` 那一档是会话库 v7
|
|
7352
7613
|
* 加的,2026-08-19)。
|
|
@@ -7661,6 +7922,18 @@ interface WireSessionSummary {
|
|
|
7661
7922
|
* 普通列举时缺席(所以它是可选的,不进下面那份必填清单)。
|
|
7662
7923
|
*/
|
|
7663
7924
|
snippet?: string;
|
|
7925
|
+
/**
|
|
7926
|
+
* 这段会话被收进归档了吗(方案 64 PR-2)。
|
|
7927
|
+
*
|
|
7928
|
+
* **必填,不是可选**:界面上这一格决定「要不要画那枚归档记号」以及
|
|
7929
|
+
* 「那一行的操作是归档还是取回」,而 `undefined` 在那两处都会被读成 `false` ——
|
|
7930
|
+
* 于是一个归档过的会话在归档列表里长得和普通会话一模一样,操作还是「归档」。
|
|
7931
|
+
*
|
|
7932
|
+
* ⚠️ **Hub 里有、DB 里还没有的那种行(新会话第一条消息落盘之前)一律 `false`。**
|
|
7933
|
+
* 那是**实话**不是兜底:一段还没落盘的会话根本没有归档状态可言,
|
|
7934
|
+
* 而它按定义不可能被归档过(归档写的是 DB 里那一列)。
|
|
7935
|
+
*/
|
|
7936
|
+
archived: boolean;
|
|
7664
7937
|
}
|
|
7665
7938
|
/**
|
|
7666
7939
|
* 上面那张接口的必填清单。**要数字段有几个,数这里,别在别处手抄**(抄过去的那份
|
|
@@ -7904,14 +8177,46 @@ interface WireDeleteSessionResponse {
|
|
|
7904
8177
|
}
|
|
7905
8178
|
declare const WIRE_DELETE_SESSION_FIELDS: WireFields<WireDeleteSessionResponse>;
|
|
7906
8179
|
/**
|
|
7907
|
-
*
|
|
8180
|
+
* 改一段会话的元信息。
|
|
8181
|
+
*
|
|
8182
|
+
* ## 「将来」到了(方案 64 PR-2,2026-09-14)
|
|
7908
8183
|
*
|
|
7909
|
-
*
|
|
7910
|
-
*
|
|
8184
|
+
* 上一版这里写着「今天只有 `title` 一个可改字段……将来加 `pinned` / `archived`
|
|
8185
|
+
* 时不换形状,那时这里多一个可选字段,老客户端一行不用改」。`archived` 那一格
|
|
8186
|
+
* 现在真的来了,而那句预言**兑现了**:老客户端仍然只发 `title`,一个字不用改。
|
|
8187
|
+
*
|
|
8188
|
+
* ⚠️ **两个字段都是可选的,但不能一个都不给。** 服务端对「两个都没给」回 400 ——
|
|
8189
|
+
* 一次什么都不改的 PATCH 是调用方写错了,静默回 200 会让它以为改成功了。
|
|
8190
|
+
* 判据在 `server/src/api.ts` 的 `patchSession` 上。
|
|
8191
|
+
*
|
|
8192
|
+
* ⚠️ `title` 仍然**不许是空串**(那条老判据一个字没变):空标题不是「清空」
|
|
8193
|
+
* 而是「没给」—— 标题空着的会话在列表里只剩一个 uuid,而那正是用户点重命名
|
|
8194
|
+
* 想解决的问题。要只改归档状态就别带 `title` 这个键。
|
|
7911
8195
|
*/
|
|
7912
8196
|
interface WirePatchSessionRequest {
|
|
7913
|
-
title
|
|
8197
|
+
title?: string;
|
|
8198
|
+
/**
|
|
8199
|
+
* 收进归档(`true`)还是从归档里取回(`false`)。
|
|
8200
|
+
*
|
|
8201
|
+
* **归档不是删除**:它只把这段会话从默认列表和模糊搜里收起来,消息一条不少、
|
|
8202
|
+
* 给了 id 照样读得出来。判据是 core 的 `session/authorization.ts` 那张
|
|
8203
|
+
* 「全仓唯一一份」的表 —— 那张表这一份只用、不改。
|
|
8204
|
+
*/
|
|
8205
|
+
archived?: boolean;
|
|
7914
8206
|
}
|
|
8207
|
+
/**
|
|
8208
|
+
* ⚠️ **空清单,而且只能是空的。**
|
|
8209
|
+
*
|
|
8210
|
+
* `wireFields` 收的是**必填**字段(那条约束写在它自己的 JSDoc 第三节上:
|
|
8211
|
+
* 可选键写进清单就等于在运行时把它变成必填,于是一个合法的、没带那个字段的
|
|
8212
|
+
* 请求会被 `conformsToWire()` 判成不合契约)。这张请求上两个字段都是可选的 ——
|
|
8213
|
+
* 只改名的调用方不带 `archived`,只归档的不带 `title`。
|
|
8214
|
+
*
|
|
8215
|
+
* 于是「至少给一个」这条约束**没有地方由类型来守**,它由服务端那道 400 守
|
|
8216
|
+
* (`server/src/api.ts` 的 `patchSession`)。这是刻意的分工:`wireFields`
|
|
8217
|
+
* 管的是「字段在不在」,管不了「这几个里至少要有一个」——
|
|
8218
|
+
* 硬把它塞进来的代价是让两种合法请求各自不合契约。
|
|
8219
|
+
*/
|
|
7915
8220
|
declare const WIRE_PATCH_SESSION_FIELDS: WireFields<WirePatchSessionRequest>;
|
|
7916
8221
|
/**
|
|
7917
8222
|
* 改完之后的那一行 —— **回整行而不是回 `{ok:true}`**。
|
|
@@ -7997,6 +8302,26 @@ interface WireReplayMessage extends EpochMessage {
|
|
|
7997
8302
|
* 「这一轮不知道花了多少」处理 —— 界面上那半行字整个不画,而不是画一行 0。
|
|
7998
8303
|
*/
|
|
7999
8304
|
turnFacts?: EpochTurnFacts;
|
|
8305
|
+
/**
|
|
8306
|
+
* 这一轮**为什么停**。和 {@link WireReplayMessage.turnFacts} 一样,只有一轮里
|
|
8307
|
+
* 最后一条 assistant 行有。
|
|
8308
|
+
*
|
|
8309
|
+
* ## 没有它的时候,刷新一下就再也看不出这一轮出过事(2026-09-16)
|
|
8310
|
+
*
|
|
8311
|
+
* 引擎在**直播**时是说得出话的:截断发一帧 `response-truncated`、跑完工具
|
|
8312
|
+
* 却没说话发一帧 `blank-response`,两者都画成一行 ℹ️。但那些 `notice`
|
|
8313
|
+
* **不落盘**(`TurnStepCollector.observe()` 只收 text / reasoning / tool / finish),
|
|
8314
|
+
* 而回合一结束 web 就拿权威历史整体换掉 pending —— 那一行当场消失。
|
|
8315
|
+
*
|
|
8316
|
+
* 于是用户回头看到的是:一条两个字的回复,底下是一模一样的脚注
|
|
8317
|
+
* 「共 289.2k token · deepseek-flash · 8 次工具 · 42s」,没有任何东西说过
|
|
8318
|
+
* 刚才发生了什么。这一格就是那个记号。
|
|
8319
|
+
*
|
|
8320
|
+
* ⚠️ 类型是 `string` 而不是 {@link FinishReason}:真源是 `messages.finish_reason`
|
|
8321
|
+
* 那一列,它在库里就是 `TEXT`(老库里还可能是 NULL)。读侧要按码分叉的话,
|
|
8322
|
+
* **认不出来的值一律当「不知道」**,别画成任何一档 —— 编一个终局比不画贵。
|
|
8323
|
+
*/
|
|
8324
|
+
finishReason?: string;
|
|
8000
8325
|
}
|
|
8001
8326
|
interface WireMessageListResponse {
|
|
8002
8327
|
messages: readonly WireReplayMessage[];
|
|
@@ -8027,6 +8352,56 @@ interface WireMessageListResponse {
|
|
|
8027
8352
|
* 三种都是「不知道」,界面上一律整格不画 —— 画 0 是一个假事实。
|
|
8028
8353
|
*/
|
|
8029
8354
|
usage?: TokenUsage;
|
|
8355
|
+
/**
|
|
8356
|
+
* 上面那批 `messages` **含正在跑的那一轮**,而且它的已落盘部分对应到这一帧
|
|
8357
|
+
* 广播序号为止(按步增量落盘,2026-09-16)。缺席 = 这段会话此刻没有在本进程
|
|
8358
|
+
* 跑着的轮次。
|
|
8359
|
+
*
|
|
8360
|
+
* ## 它治的是什么
|
|
8361
|
+
*
|
|
8362
|
+
* 2026-09-16 之前,「回合在跑 ⇒ 这一发答不出本轮那几条」是一条不变量,
|
|
8363
|
+
* 浏览器整个 `replay` 分支建立在它上面(`web/src/state/reducer.ts`)——
|
|
8364
|
+
* 因为一轮问答要等 `run()` 末尾才整批落盘。代价是**刷新 / 关掉重开 / 切走
|
|
8365
|
+
* 再切回来**都看不见正在跑的那一轮,`WebState` 上那两块补丁
|
|
8366
|
+
* (`localTail` / `parked`)就是在浏览器里兜这件事,而它们的注释逐字写着
|
|
8367
|
+
* 「治不到的那一档……真要修得让服务端答出在飞的那一轮」。
|
|
8368
|
+
*
|
|
8369
|
+
* 改成按步落盘之后服务端答得出了,于是冒出**去重**这个新问题:同一步既在
|
|
8370
|
+
* 这批 `messages` 里、又在浏览器自己攒的 pending 里。
|
|
8371
|
+
*
|
|
8372
|
+
* ## 为什么钥匙是 `seq`
|
|
8373
|
+
*
|
|
8374
|
+
* 因为它是两边**已经共用**的那一个:每一帧信封上都有、是 SSE 续传的游标,
|
|
8375
|
+
* 而浏览器早就把它锚进了每一个 pending 项(`HistoryItemBase.seq`)。
|
|
8376
|
+
* 落盘的消息 id 在浏览器那侧不存在,`seq` 是唯一对得上的东西。
|
|
8377
|
+
*
|
|
8378
|
+
* ## ⚠️ 服务端取这个数和截断 `messages` 是**同一瞬**的两半
|
|
8379
|
+
*
|
|
8380
|
+
* 这一格的语义是「`messages` 里的在飞部分**恰好**到这一帧为止」。所以服务端
|
|
8381
|
+
* 记的是一对 `(消息 id, seq)`,两个数在同一次 `publish()` 之后一起取,
|
|
8382
|
+
* 然后按那个 id 截断。分两次取的话会出现「行已经写下去、对应那一帧还没发」
|
|
8383
|
+
* 的窗口,而那个窗口里浏览器会把同一步画两遍。
|
|
8384
|
+
*
|
|
8385
|
+
* 浏览器拿到它之后做两件事:丢掉 `localTail`(用户那句话已经在库里了),
|
|
8386
|
+
* 以及丢掉 `seq <= 这个数` 的 pending 项。
|
|
8387
|
+
*/
|
|
8388
|
+
inFlightThroughSeq?: number;
|
|
8389
|
+
/**
|
|
8390
|
+
* 有一轮**没跑完**,它从这条消息 id 开始(按步增量落盘,2026-09-16)。
|
|
8391
|
+
* 缺席 = 没有这种轮次(绝大多数情况)。
|
|
8392
|
+
*
|
|
8393
|
+
* 和上面那一格**互斥**:那一格说「正在跑」,这一格说「跑过但没收尾」。
|
|
8394
|
+
* 两者都由 `sessions.open_turn_from` 那格标记来判,区别只在「这个进程手上
|
|
8395
|
+
* 有没有它」—— 判据全文在 `SessionMeta.openTurnFrom` 上。
|
|
8396
|
+
*
|
|
8397
|
+
* ⚠️ **不能靠 `finishReason` 为空来推断。** 那一列在会话库 v6 之前的行上
|
|
8398
|
+
* 全是 NULL(一台真机上量过 687 条 687 个 NULL),拿它判会把每一段老会话的
|
|
8399
|
+
* 每一轮都标成「没跑完」。
|
|
8400
|
+
*
|
|
8401
|
+
* 界面拿它在那一轮上画一句「这一轮没跑完」。不画的话,用户看到的是一段
|
|
8402
|
+
* 说到一半就断的对话,而分不出「模型自己停了」和「程序被关掉了」。
|
|
8403
|
+
*/
|
|
8404
|
+
interruptedTurnFrom?: number;
|
|
8030
8405
|
}
|
|
8031
8406
|
declare const WIRE_MESSAGE_LIST_FIELDS: WireFields<WireMessageListResponse>;
|
|
8032
8407
|
/**
|
|
@@ -8121,6 +8496,31 @@ interface WireSendMessageResponse {
|
|
|
8121
8496
|
accepted: true;
|
|
8122
8497
|
/** 这一条被排到了当前这一轮后面,还没开始跑 */
|
|
8123
8498
|
queued: boolean;
|
|
8499
|
+
/**
|
|
8500
|
+
* `queued: true` 的两种去向里的哪一种(2026-09-16)。
|
|
8501
|
+
*
|
|
8502
|
+
* | 值 | 意思 |
|
|
8503
|
+
* | ------------ | ---------------------------------------------------------- |
|
|
8504
|
+
* | `true` | 正在跑的那一轮会在下一次模型请求前把它拌进去(**插进本轮**)|
|
|
8505
|
+
* | `false` | 排在后面,等这一轮跑完再**开新的一轮** |
|
|
8506
|
+
* | 整个键缺席 | `queued: false`(这条消息立刻开了一轮),问题不成立 |
|
|
8507
|
+
*
|
|
8508
|
+
* ## 为什么必须由服务端说
|
|
8509
|
+
*
|
|
8510
|
+
* 两条判据都在服务端手上:队列里排在它前面的那几条**能不能**折进去,
|
|
8511
|
+
* 以及它自己是不是一条带轮级设置的斜杠命令(`allowed-tools` / `model:`
|
|
8512
|
+
* 那种收窄只对一整轮成立,折进别人的轮里就整个落空)。浏览器两条都看不见。
|
|
8513
|
+
*
|
|
8514
|
+
* ## ⚠️ 它是「会被拌进去」,不是「已经拌进去了」
|
|
8515
|
+
*
|
|
8516
|
+
* 真的进上下文那一刻由事件流上的 `steer` 那一帧宣布。这一轮在那之前撞上轮次
|
|
8517
|
+
* 上限、或者被用户中止时,这条消息会退回队列(开新一轮)或者被
|
|
8518
|
+
* `WireAbortResponse.returned` 交还 —— 那时这一格就说早了。
|
|
8519
|
+
*
|
|
8520
|
+
* 所以界面**据它选措辞**(「插入本轮」/「排队中」)可以,
|
|
8521
|
+
* **据它把消息画成已送达**不行。
|
|
8522
|
+
*/
|
|
8523
|
+
steered?: boolean;
|
|
8124
8524
|
/**
|
|
8125
8525
|
* 这条输入是一条自定义斜杠命令,服务端把它展开了(方案 23 的 web 那一半)。
|
|
8126
8526
|
*
|
|
@@ -8243,6 +8643,30 @@ declare const WIRE_SEND_MESSAGE_RESPONSE_FIELDS: WireFields<WireSendMessageRespo
|
|
|
8243
8643
|
/** `aborted: false` 表示这一轮本来就没在跑,不是失败 */
|
|
8244
8644
|
interface WireAbortResponse {
|
|
8245
8645
|
aborted: boolean;
|
|
8646
|
+
/**
|
|
8647
|
+
* 按下停那一刻**还没被引擎取走**的排队消息,按用户发送的先后(2026-09-16)。
|
|
8648
|
+
*
|
|
8649
|
+
* ## 它答的是「我刚打的那段话去哪了」
|
|
8650
|
+
*
|
|
8651
|
+
* 中止一直会把队列一起清掉(判据在 `SessionHub.abort` 上:按下停要的是现在
|
|
8652
|
+
* 别干了,不是停掉这一轮然后立刻开始下一轮)。「跑到一半插一句话」落地之后,
|
|
8653
|
+
* 那条清空变成一个真会咬人的形状 —— 用户盯着屏幕打了一整段修正意见、发出去、
|
|
8654
|
+
* 看见模型还在往错的方向跑,于是顺手按了停,**那段话就没了**。
|
|
8655
|
+
*
|
|
8656
|
+
* 所以原样交出来,宿主把它放回输入框。codex 的同一处叫
|
|
8657
|
+
* `submit_pending_steers_after_interrupt`。
|
|
8658
|
+
*
|
|
8659
|
+
* ## ⚠️ 已经拌进上下文的那些**不在这里**
|
|
8660
|
+
*
|
|
8661
|
+
* 事件流上已经发过 `steer` 的那几条早就是这段对话的一部分(库里有、屏幕上有),
|
|
8662
|
+
* 交还一次就成了重复。这一条不靠服务端判断保证 —— 两条路(引擎取用和这里的
|
|
8663
|
+
* 清空)动的是同一个数组。
|
|
8664
|
+
*
|
|
8665
|
+
* **一条都没有时整个键缺席**,不是空数组:绝大多数中止发生在没有排队消息的
|
|
8666
|
+
* 时候,写一个 `[]` 会让每个读它的人多判一次「空数组是没有,还是这一版
|
|
8667
|
+
* 服务端不支持」。
|
|
8668
|
+
*/
|
|
8669
|
+
returned?: EpochUserContent[];
|
|
8246
8670
|
}
|
|
8247
8671
|
declare const WIRE_ABORT_FIELDS: WireFields<WireAbortResponse>;
|
|
8248
8672
|
/**
|
|
@@ -10281,6 +10705,83 @@ interface WireRoleAddResponse {
|
|
|
10281
10705
|
path: string;
|
|
10282
10706
|
}
|
|
10283
10707
|
declare const WIRE_ROLE_ADD_RESPONSE_FIELDS: WireFields<WireRoleAddResponse>;
|
|
10708
|
+
/**
|
|
10709
|
+
* 一次删除为什么没成 —— 五档,**每一档对一个不同的下一步**。
|
|
10710
|
+
*
|
|
10711
|
+
* | | 意思 | 用户的下一步 |
|
|
10712
|
+
* | ------------- | -------------------------------------- | ------------------------------------- |
|
|
10713
|
+
* | `missing` | 请求里没给名字 | (界面不该发出这一发,见下) |
|
|
10714
|
+
* | `not-found` | 角色表里没有这个名字 | 刷新这一栏再看,多半已经没了 |
|
|
10715
|
+
* | `not-user` | 它来自内置 / 项目 / 插件 / 宿主 | 那句话里点名了是哪一层,各有各的删法 |
|
|
10716
|
+
* | `ambiguous` | 目录里两份以上都叫这个名字 | 去那个目录里留下要的那一份 |
|
|
10717
|
+
* | `failed` | 真的删不动(权限 / 只读盘) | 去那条路径那儿看一眼 |
|
|
10718
|
+
*
|
|
10719
|
+
* ⚠️ `missing` 那一档**照走 200 + `ok:false`**,不是 400 —— 判据逐字同
|
|
10720
|
+
* {@link WireSkillRemoveFailure}:`{error:{code,message}}` 在界面上会走到
|
|
10721
|
+
* 「请求坏了」那一支去。
|
|
10722
|
+
*/
|
|
10723
|
+
type WireRoleRemoveFailure = 'missing' | 'not-found' | 'not-user' | 'ambiguous' | 'failed';
|
|
10724
|
+
/**
|
|
10725
|
+
* `POST /api/roles/remove` —— 删掉一个**用户级**身份。
|
|
10726
|
+
*
|
|
10727
|
+
* 请求体只有 `{name}`,所以不给它单开一个类型(同 `WireSkillRemoveResponse`
|
|
10728
|
+
* 那条:一个只有一格的请求体类型,读者要多跳一次才知道它就是那一格)。
|
|
10729
|
+
*
|
|
10730
|
+
* ## ⚠️ 没有「预览 → 确认」那一对,而那不是省事
|
|
10731
|
+
*
|
|
10732
|
+
* 判据逐字借技能那条:导入那个 token 防的是「用户看过的那一份」和「真装上的
|
|
10733
|
+
* 那一份」不是一份,而删除这边**要删掉的东西由名字唯一确定**。
|
|
10734
|
+
* 挡住不可撤销的是另外两样:界面上那一步「再按一次确认」,和回执里的
|
|
10735
|
+
* {@link path} —— 用户得看得见自己刚销毁了盘上的哪一处。
|
|
10736
|
+
*
|
|
10737
|
+
* ⚠️ 而身份这条比技能那条多一格 {@link takenOverBy},理由见那一格。
|
|
10738
|
+
*/
|
|
10739
|
+
interface WireRoleRemoveResponse {
|
|
10740
|
+
ok: boolean;
|
|
10741
|
+
/**
|
|
10742
|
+
* 谁真的没了。**没删成时是空串**,不是把请求里那个名字抄回去 ——
|
|
10743
|
+
* 判据逐字同 `WireSkillRemoveResponse.name`:回一个其实还在盘上的名字,
|
|
10744
|
+
* 是这条端点最坏的说谎方式。
|
|
10745
|
+
*/
|
|
10746
|
+
name: string;
|
|
10747
|
+
/**
|
|
10748
|
+
* 刚刚被销毁的是盘上哪一份(服务端的绝对路径)。没删成时 `null`。
|
|
10749
|
+
*
|
|
10750
|
+
* **必须发出来**,判据同技能那条(不可撤销的动作,回执得说得出销毁了哪一处)。
|
|
10751
|
+
* ⚠️ 而这条路上它还多一层意思:**文件名可以和身份名不一样**
|
|
10752
|
+
* (frontmatter 的 `name` 优先于文件名,判据在 core 的 `removeRoleFile`
|
|
10753
|
+
* 文件头第一节)—— 所以这条路径是用户**唯一**能核对「我删掉的是不是那一份」
|
|
10754
|
+
* 的东西,浏览器自己拼不出来。
|
|
10755
|
+
*/
|
|
10756
|
+
path: string | null;
|
|
10757
|
+
/**
|
|
10758
|
+
* 删完之后,**这个名字还在不在合并表里**(`null` = 真的没了)。
|
|
10759
|
+
*
|
|
10760
|
+
* ## ⚠️ 这一格答的是「那一行会不会消失」,而答案常常是「不会」
|
|
10761
|
+
*
|
|
10762
|
+
* 用户级那一份可能是一个**同名覆盖**(手写一份 `general.md` 就是,
|
|
10763
|
+
* `mergeRoles` 的同名语义是「prompt / description 整份替换、工具取交集」)。
|
|
10764
|
+
* 删掉它之后下层那一份**浮上来**:能力页刷新出来,那一行**还在**,只是挪去了
|
|
10765
|
+
* 「内置」那一组、描述和工具都变了。
|
|
10766
|
+
*
|
|
10767
|
+
* 少了这一格,那一屏对用户说的是「已经删掉 x」而列表里 x 还在 —— 一次成功的
|
|
10768
|
+
* 删除长成了一次失败的删除,而屏幕上没有任何地方解释得了。
|
|
10769
|
+
*
|
|
10770
|
+
* 非 `null` 时它是**接管者那一层**(`builtin` / `project` / `plugin` / `host`),
|
|
10771
|
+
* 界面据此多印一句。⚠️ 它**不可能**是 `'user'`:用户级那一份刚被删掉,
|
|
10772
|
+
* 而同一个目录里还有第二份同名的话,上面那道 `ambiguous` 已经把这一发拦掉了。
|
|
10773
|
+
*/
|
|
10774
|
+
takenOverBy: AgentRoleSource | null;
|
|
10775
|
+
/**
|
|
10776
|
+
* 没删成时那一句话(`reason` 各配一句,含「那该去哪儿改」)。删成了是 `null`。
|
|
10777
|
+
*
|
|
10778
|
+
* ⚠️ 它在 **core / runtime** 里现渲染,所以 `lang` 要从服务端一路递下去 ——
|
|
10779
|
+
* 这条病 2026-08-18 在导入那一屏上实测见过一次。
|
|
10780
|
+
*/
|
|
10781
|
+
detail: string | null;
|
|
10782
|
+
reason: WireRoleRemoveFailure | null;
|
|
10783
|
+
}
|
|
10784
|
+
declare const WIRE_ROLE_REMOVE_FIELDS: WireFields<WireRoleRemoveResponse>;
|
|
10284
10785
|
|
|
10285
10786
|
/**
|
|
10286
10787
|
* 插件那一页的契约 —— 十四条端点(方案 59 §六 E2,2026-08-21;本地源与市场管理
|
|
@@ -10547,18 +11048,34 @@ declare const WIRE_PLUGIN_UPLOAD_FORM_FIELDS: readonly ["displayName", "descript
|
|
|
10547
11048
|
/** {@link WIRE_PLUGIN_UPLOAD_FORM_FIELDS} 里的一格 */
|
|
10548
11049
|
type WirePluginUploadFieldName = (typeof WIRE_PLUGIN_UPLOAD_FORM_FIELDS)[number];
|
|
10549
11050
|
/**
|
|
10550
|
-
*
|
|
11051
|
+
* 下拉里的一项(2026-09-15)。**两格都恒有** —— 宿主写 `'office'` 这种裸字符串
|
|
11052
|
+
* 时 `label` 在 runtime 那一侧就补成了同一个值,判据同下面那条「界面要的是结论」。
|
|
11053
|
+
*/
|
|
11054
|
+
interface WirePluginUploadChoice {
|
|
11055
|
+
/** 真发上去的那个值。它要和宿主自己那份目录里的分类名逐字相同 */
|
|
11056
|
+
value: string;
|
|
11057
|
+
/** 屏幕上印的那几个字 */
|
|
11058
|
+
label: string;
|
|
11059
|
+
}
|
|
11060
|
+
/**
|
|
11061
|
+
* 上传表单上一格在浏览器眼里的样子(2026-09-10 两格 → 2026-09-15 四格)。
|
|
10551
11062
|
*
|
|
10552
|
-
* ⚠️
|
|
10553
|
-
* 口径同 `WirePluginsResponse.uploadOptions
|
|
11063
|
+
* ⚠️ **四格都恒有**(`value` 空串 = 宿主没预填,`editable` / `required` 恒是布尔,
|
|
11064
|
+
* `choices` 空数组 = 这一格不是下拉)—— 口径同 `WirePluginsResponse.uploadOptions`:
|
|
11065
|
+
* 界面要的是结论,不是三种等价写法。
|
|
10554
11066
|
*/
|
|
10555
11067
|
interface WirePluginUploadField {
|
|
10556
11068
|
/**
|
|
10557
|
-
*
|
|
11069
|
+
* **宿主**预填的值。没给就是空串。
|
|
10558
11070
|
*
|
|
10559
|
-
* ⚠️
|
|
10560
|
-
*
|
|
10561
|
-
*
|
|
11071
|
+
* ⚠️ **它不是这一格最终从什么值开始** —— 那个答案在
|
|
11072
|
+
* {@link WirePluginUploadPreviewResponse.prefill} 上(2026-09-15 起还有
|
|
11073
|
+
* 第三个来源:包里那份 `epoch-plugin.json` 的同名格)。这一格只说宿主
|
|
11074
|
+
* 那一侧写了什么,而它排在包里那一份**前面**(四档优先级见那一处)。
|
|
11075
|
+
*
|
|
11076
|
+
* ⚠️ 所以界面上**别拿它当输入框的初值**:它只在 {@link editable} 为假那一档
|
|
11077
|
+
* 直接上屏(印着的那段静态文字)。真要画的初值一律走 `prefill`,
|
|
11078
|
+
* 否则「这一格该填什么」在浏览器里就有了第二个答案。
|
|
10562
11079
|
*/
|
|
10563
11080
|
value: string;
|
|
10564
11081
|
/**
|
|
@@ -10573,6 +11090,38 @@ interface WirePluginUploadField {
|
|
|
10573
11090
|
* `PluginUploadForm` 上那两条规矩)。少了那一半,改一次请求体就绕过去了。
|
|
10574
11091
|
*/
|
|
10575
11092
|
editable: boolean;
|
|
11093
|
+
/**
|
|
11094
|
+
* 能挑的就这几个(2026-09-15)。**非空 = 这一格画成 `<select>`。**
|
|
11095
|
+
*
|
|
11096
|
+
* ## ⚠️ 判据是 `choices.length > 0`,不是 `choices` 本身
|
|
11097
|
+
*
|
|
11098
|
+
* 这一格恒有(不画下拉那几格是 `[]`,不是缺席也不是 `null`),而 `[]` 在 JS
|
|
11099
|
+
* 里是**真值** —— 写成 `shape.choices ? … : …` 的那天每一格都会变成一个
|
|
11100
|
+
* 零选项的下拉,而类型上一点问题都没有。这仓库为这件事栽过。
|
|
11101
|
+
*
|
|
11102
|
+
* ## ⚠️ 它是**闭集**,而那一半同样在服务端兑现
|
|
11103
|
+
*
|
|
11104
|
+
* 「下拉是形态,开集是契约」(判据全文在 `shell/model-menu.tsx` 的文件头):
|
|
11105
|
+
* 宿主给了单子就是在说它那份目录只认得这几类。所以 runtime 那一层会把
|
|
11106
|
+
* **单子外的值整个丢掉**,和 {@link editable} 那一条是同一条规矩的第三半。
|
|
11107
|
+
*
|
|
11108
|
+
* ⚠️ 它还管着**包里那一份**:上传者在 `epoch-plugin.json` 里写的同名格
|
|
11109
|
+
* 只有命中这张单子才会进 {@link WirePluginUploadPreviewResponse.prefill}。
|
|
11110
|
+
*
|
|
11111
|
+
* ⚠️ `visibility` 那一格恒是 `[]`(它的值域固定两档、屏幕上画的是两枚单选)。
|
|
11112
|
+
*/
|
|
11113
|
+
choices: readonly WirePluginUploadChoice[];
|
|
11114
|
+
/**
|
|
11115
|
+
* 空着就发不出去吗(2026-09-15)。
|
|
11116
|
+
*
|
|
11117
|
+
* ⚠️ **界面上那一道只是「别让他白按一下」**:标签上一个 `*`、空着时那枚
|
|
11118
|
+
* 「确认上传」是禁用的。真正算数的在服务端 —— 必填格空着时 `POST /api/plugins/upload`
|
|
11119
|
+
* 回一档 `missing-field`,**宿主回调一次都不调**。判据逐字同 {@link editable}。
|
|
11120
|
+
*
|
|
11121
|
+
* ⚠️ `visibility` 上它恒是 `false` 且没有意义:那一格恒有值(认不出的退
|
|
11122
|
+
* `private`),永远不会空着。
|
|
11123
|
+
*/
|
|
11124
|
+
required: boolean;
|
|
10576
11125
|
}
|
|
10577
11126
|
/**
|
|
10578
11127
|
* 七格的形态表 —— **每个键恒有,不画的那几格是 `null`**。
|
|
@@ -11120,7 +11669,15 @@ type WirePluginRefuseReason = 'unknown-ref' | 'not-local'
|
|
|
11120
11669
|
* - `not-plugin` —— 解开了,但里面没有 `epoch-plugin.json`。
|
|
11121
11670
|
* 下一步:确认拖的是插件本身那一层,而不是装着它的那个上级目录。
|
|
11122
11671
|
*/
|
|
11123
|
-
| 'not-archive' | 'unpack-failed' | 'not-plugin' | 'remote-disabled' | 'conflict' | 'stale-token' | 'not-installed' | 'nothing-to-do' | 'upload-unavailable' | 'refresh-unavailable' | 'upload-rejected'
|
|
11672
|
+
| 'not-archive' | 'unpack-failed' | 'not-plugin' | 'remote-disabled' | 'conflict' | 'stale-token' | 'not-installed' | 'nothing-to-do' | 'upload-unavailable' | 'refresh-unavailable' | 'upload-rejected'
|
|
11673
|
+
/**
|
|
11674
|
+
* 宿主标了必填的格子还空着(2026-09-15)。**下一步在用户那儿**(回去把那几格
|
|
11675
|
+
* 填上),所以不并进 `failed`(那一档的下一步是「看 detail 重试」)。
|
|
11676
|
+
*
|
|
11677
|
+
* ⚠️ 界面上正常走不到这一档(那枚按钮在必填格空着时是禁用的)—— 它拦的是
|
|
11678
|
+
* **改了请求体那一发**,判据同 `editable: false` 为什么在服务端兑现。
|
|
11679
|
+
*/
|
|
11680
|
+
| 'missing-field' | 'failed';
|
|
11124
11681
|
/** 一次 hook 盘点:这个事件上有几条 */
|
|
11125
11682
|
interface WirePluginHookTally {
|
|
11126
11683
|
type: string;
|
|
@@ -11426,6 +11983,31 @@ interface WirePluginUploadPreviewResponse {
|
|
|
11426
11983
|
plan: WirePluginUploadPlan | null;
|
|
11427
11984
|
/** 成了才有。原样发回 {@link WirePluginUploadConfirmRequest.token} */
|
|
11428
11985
|
token: string | null;
|
|
11986
|
+
/**
|
|
11987
|
+
* 那张表单**每一格从什么值开始**(2026-09-15)。成了才有。
|
|
11988
|
+
*
|
|
11989
|
+
* ## ⚠️ 它和 `uploadOptions` 各答一半,别拿一格顶两格
|
|
11990
|
+
*
|
|
11991
|
+
* `GET /api/plugins` 那份 {@link WirePluginUploadFields} 答的是
|
|
11992
|
+
* 「**这一屏画成什么样**」(画哪几格、哪几格用户改不动)—— 它是装配期定死的,
|
|
11993
|
+
* 和用户选了哪个目录无关。这一格答的是「**这一次**每一格填什么」,
|
|
11994
|
+
* 而它要等读到包里那份 `epoch-plugin.json` 才知道。
|
|
11995
|
+
*
|
|
11996
|
+
* ⚠️ **界面把它原样塞进表单 state 就行,不用再和 `uploadOptions` 合并一次** ——
|
|
11997
|
+
* 四档优先级(宿主锁死 > 宿主初值 > 包里同名格 > 空)在服务端算完了。
|
|
11998
|
+
* 在浏览器里再算一遍的话,「这一格该填什么」就有了两个答案。
|
|
11999
|
+
*
|
|
12000
|
+
* ⚠️ 只含**宿主真声明了的**那几格,口径同 {@link WirePluginUploadForm}。
|
|
12001
|
+
*/
|
|
12002
|
+
prefill: WirePluginUploadForm | null;
|
|
12003
|
+
/**
|
|
12004
|
+
* {@link prefill} 里**值是从包里读出来的**那几格(2026-09-15)。成了才有。
|
|
12005
|
+
*
|
|
12006
|
+
* ⚠️ 它只管屏幕上那一句提示,一个判断都不参与。没有它的话界面分不出
|
|
12007
|
+
* 「宿主替你填的」和「你自己在包里写的」—— 而用户的下一步不一样:
|
|
12008
|
+
* 前者去问宿主,后者去改自己那份 `epoch-plugin.json`。
|
|
12009
|
+
*/
|
|
12010
|
+
fromManifest: readonly WirePluginUploadFieldName[] | null;
|
|
11429
12011
|
/** 没成才有 */
|
|
11430
12012
|
reason: WirePluginRefuseReason | null;
|
|
11431
12013
|
/** 没成才有。**服务端渲染好的一句人话**,界面原样上屏 */
|
|
@@ -12528,6 +13110,16 @@ declare const WIRE_PERMISSION_SET_RESPONSE_FIELDS: WireFields<WirePermissionSetR
|
|
|
12528
13110
|
* 新值,就是这一屏存在的理由本身要消灭的那种撒谎 —— 而这一条对 `new-session`
|
|
12529
13111
|
* 那一档**同样成立**(正在跑的会话按的仍是旧值),只是它下一次 `GET` 会真的变。
|
|
12530
13112
|
*
|
|
13113
|
+
* ## ✅ 2026-09-16:值照旧不变,但**盘上是什么现在也发出来了**
|
|
13114
|
+
*
|
|
13115
|
+
* 上面那一条一个字都没改,改的是它**只说了一半**这件事:行上的值不动是对的,
|
|
13116
|
+
* 可那句「已写进 X,重启后生效」当时只活在浏览器的一个组件 state 里 —— 切一次分区、
|
|
13117
|
+
* 刷一次页面就没了,屏幕上回到「旧值 + ① 内置默认」,而那个文件里躺着用户刚写的值。
|
|
13118
|
+
* 用户的原话是「改完了保存了,页面也没有展示新的数字,不知道有没有生效」。
|
|
13119
|
+
*
|
|
13120
|
+
* 所以现在每一行可以多带一格 {@link WireSettingRow.pending}:**盘上那一份**。
|
|
13121
|
+
* 两格并排、各答各的问题,没有哪一格需要撒谎。
|
|
13122
|
+
*
|
|
12531
13123
|
* ## 会话作用域,不是进程级的 `/api/settings`
|
|
12532
13124
|
*
|
|
12533
13125
|
* 项目级那两层(③④)跟着**工作区**走,而工作区按会话绑(决定 18)。
|
|
@@ -12706,6 +13298,54 @@ interface WireSettingStep {
|
|
|
12706
13298
|
value?: WireSettingValue;
|
|
12707
13299
|
}
|
|
12708
13300
|
declare const WIRE_SETTING_STEP_FIELDS: WireFields<WireSettingStep>;
|
|
13301
|
+
/**
|
|
13302
|
+
* **已经写进盘、但这个进程还没在用**的那一次改动(2026-09-16)。
|
|
13303
|
+
*
|
|
13304
|
+
* ## 它补的是上面那条「值不变」留下的另一半
|
|
13305
|
+
*
|
|
13306
|
+
* `restart` 那一档写完,行上的 {@link WireSettingRow.value} 一个字都不变 —— 那是对的
|
|
13307
|
+
* (判据在 {@link WireSettingApply},把它乐观换成新值是这一屏存在的理由本身要消灭的
|
|
13308
|
+
* 那种撒谎)。可**只做到这一步,屏幕上就一条线索都不剩了**:用户按完保存、切一次
|
|
13309
|
+
* 分区再回来,看到的是「旧值 + ① 内置默认」,而 `~/.epoch/config.yaml` 里明明躺着他
|
|
13310
|
+
* 刚写的那个值 —— 不但没人告诉他写成了,那枚来源徽标还在说一句会被文件反驳的话。
|
|
13311
|
+
*
|
|
13312
|
+
* 所以这一格发的是**盘上那一份**,和行上那个「进程正在用的」并排摆着。两件事从来
|
|
13313
|
+
* 就不是一件事,这一轮之前只是没地方说出后者。
|
|
13314
|
+
*
|
|
13315
|
+
* ## ⚠️ 它只认「**这个进程**里有人按过保存」
|
|
13316
|
+
*
|
|
13317
|
+
* 服务端记的是一本进程内的账(runtime 的 `SettingsControl.write()` 每成功一次记一笔),
|
|
13318
|
+
* **不是**每次 `GET` 去重读那几个文件。于是 `epoch config set` 和用户自己拿编辑器改
|
|
13319
|
+
* `config.yaml` 这两条路**不会**在这里出现 —— 那是这个取舍写在脸上的代价,别把它
|
|
13320
|
+
* 读成 bug。重读那条路要重走一遍配置解析,还要分辨 `EPOCH_*` 环境变量造成的
|
|
13321
|
+
* 「文件值和进程快照天然不同」,而那种差别报出来全是假的 pending。
|
|
13322
|
+
*
|
|
13323
|
+
* ## 三件让这一格不至于自己变成假话的事
|
|
13324
|
+
*
|
|
13325
|
+
* 1. **和生效值相等时整个不带这个键。** 用户改完又改回原值,盘上和进程就一致了,
|
|
13326
|
+
* 那时还挂着一句「待重启」是自己造出来的焦虑;
|
|
13327
|
+
* 2. **`value` 是落盘后回读验证过的那个**,不是把请求原样回显(同
|
|
13328
|
+
* {@link WireSettingsWriteResponse.value});
|
|
13329
|
+
* 3. **`futile` / `apply` 是现算的**,不是记账那一刻拍下来的 —— 它们必须和这一行此刻
|
|
13330
|
+
* 那个 {@link WireSettingWrite} 说的是同一句话,否则行上的警告和行下的回执会打架。
|
|
13331
|
+
*
|
|
13332
|
+
* 字段和 {@link WireSettingsWriteResponse} 逐字相同,那不是巧合:「刚刚写完的那一次」
|
|
13333
|
+
* 和「写过了还没生效的那一次」是同一件事的两个时刻。所以那一份**继承这一份**,
|
|
13334
|
+
* 界面也只有一个渲染回执的地方。
|
|
13335
|
+
*/
|
|
13336
|
+
interface WireSettingPending {
|
|
13337
|
+
/** 写进了哪一层 */
|
|
13338
|
+
layer: WireSettingWriteLayer;
|
|
13339
|
+
/** 真正写了的那个文件的绝对路径,含义同 {@link WireSettingWrite.path} */
|
|
13340
|
+
path: string;
|
|
13341
|
+
/** **盘上现在**是这个值。行上那个 `value` 是进程正在用的,两者此刻不同 */
|
|
13342
|
+
value: WireSettingValue;
|
|
13343
|
+
/** 写进去了也照样不生效的理由。生效时整个不带,含义同 {@link WireSettingWrite.futile} */
|
|
13344
|
+
futile?: WireSettingFutile;
|
|
13345
|
+
/** 要做什么它才会被用上,见 {@link WireSettingApply} */
|
|
13346
|
+
apply: WireSettingApply;
|
|
13347
|
+
}
|
|
13348
|
+
declare const WIRE_SETTING_PENDING_FIELDS: WireFields<WireSettingPending>;
|
|
12709
13349
|
/** 设置页上的一行 */
|
|
12710
13350
|
interface WireSettingRow {
|
|
12711
13351
|
/**
|
|
@@ -12769,6 +13409,18 @@ interface WireSettingRow {
|
|
|
12769
13409
|
* 的话,schema 哪天多一档,那一档在文档里存在、在界面上点不出来。
|
|
12770
13410
|
*/
|
|
12771
13411
|
choices?: readonly string[];
|
|
13412
|
+
/**
|
|
13413
|
+
* 这一行**已经写进盘、而这个进程还没在用**的那个值(2026-09-16)。
|
|
13414
|
+
* **没有这回事时整个不带这个键**,同 `value` / `futile` 那条规矩。
|
|
13415
|
+
*
|
|
13416
|
+
* ⚠️ **它不是 `value` 的候补,两格必须同时读。** `value` 答的是「这个进程此刻按的
|
|
13417
|
+
* 是什么」,这一格答的是「盘上现在是什么」—— 界面拿前者画那一行、拿后者在行上挂一枚
|
|
13418
|
+
* 芯片加一句回执。把后者显示成前者,就退回成了 {@link WireSettingApply} 里那条判据
|
|
13419
|
+
* 明令禁止的乐观更新。
|
|
13420
|
+
*
|
|
13421
|
+
* 整个契约在 {@link WireSettingPending} 上,包括「为什么它照不到 `epoch config set`」。
|
|
13422
|
+
*/
|
|
13423
|
+
pending?: WireSettingPending;
|
|
12772
13424
|
}
|
|
12773
13425
|
declare const WIRE_SETTING_ROW_FIELDS: WireFields<WireSettingRow>;
|
|
12774
13426
|
interface WireSettingsResponse {
|
|
@@ -12856,29 +13508,19 @@ declare const WIRE_SETTINGS_WRITE_REQUEST_FIELDS: WireFields<WireSettingsWriteRe
|
|
|
12856
13508
|
*
|
|
12857
13509
|
* 值真的变没变由下一次 `GET` 说(`model` 走 `new-session` 那一档时它会跟着动,
|
|
12858
13510
|
* 判据在 {@link WireSettingApply})。
|
|
13511
|
+
*
|
|
13512
|
+
* ## ⚠️ 它**继承** {@link WireSettingPending},多出来的只有一个 `key`(2026-09-16)
|
|
13513
|
+
*
|
|
13514
|
+
* 那一份是「写过了、还没生效」,这一份是「刚刚写完」—— 同一件事的两个时刻,
|
|
13515
|
+
* 于是字段逐字相同。声明成两个独立的接口的话,哪天有人往其中一边加一格,
|
|
13516
|
+
* 界面上「刚保存完看到的」和「刷新之后看到的」就会分叉,而那种分叉在屏幕上
|
|
13517
|
+
* 看起来只是「刷新一下少了点东西」,没人会报这个 bug。
|
|
13518
|
+
*
|
|
13519
|
+
* `key` 只在这一份上,因为那一份是**挂在行上的**({@link WireSettingRow.pending}),
|
|
13520
|
+
* 键已经由那一行说了 —— 再发一遍就是同一个问题的两个答案。
|
|
12859
13521
|
*/
|
|
12860
|
-
interface WireSettingsWriteResponse {
|
|
13522
|
+
interface WireSettingsWriteResponse extends WireSettingPending {
|
|
12861
13523
|
key: string;
|
|
12862
|
-
layer: WireSettingWriteLayer;
|
|
12863
|
-
/** 真正写了的那个文件 */
|
|
12864
|
-
path: string;
|
|
12865
|
-
/**
|
|
12866
|
-
* **校验之后**落盘的那个值,不是把请求原样回显。
|
|
12867
|
-
*
|
|
12868
|
-
* 两者在 zod 修剪过的输入上会不一样,而界面印的必须是文件里现在那个 ——
|
|
12869
|
-
* 回显请求等于让屏幕上出现一个文件里没有的东西。
|
|
12870
|
-
*/
|
|
12871
|
-
value: WireSettingValue;
|
|
12872
|
-
/**
|
|
12873
|
-
* 落盘之后**仍然**不生效的理由,含义同 {@link WireSettingWrite.futile}。
|
|
12874
|
-
*
|
|
12875
|
-
* 写之前那一份已经说过一次了,这里再算一次不是重复:用户是看着警告按下去的
|
|
12876
|
-
* (那是他的权利 —— 把用户级默认值钉住、由项目本地临时压着是件正常事),
|
|
12877
|
-
* 所以回执里必须把同一句话再说一遍,否则「保存成功」四个字会盖掉它。
|
|
12878
|
-
*/
|
|
12879
|
-
futile?: WireSettingFutile;
|
|
12880
|
-
/** 见 {@link WireSettingApply} */
|
|
12881
|
-
apply: WireSettingApply;
|
|
12882
13524
|
}
|
|
12883
13525
|
declare const WIRE_SETTINGS_WRITE_FIELDS: WireFields<WireSettingsWriteResponse>;
|
|
12884
13526
|
|
|
@@ -14024,6 +14666,46 @@ interface WireSchedulePendingResponse {
|
|
|
14024
14666
|
items: readonly SchedulePendingItem[];
|
|
14025
14667
|
}
|
|
14026
14668
|
declare const WIRE_SCHEDULE_PENDING_FIELDS: WireFields<WireSchedulePendingResponse>;
|
|
14669
|
+
/**
|
|
14670
|
+
* 一条正在跑的任务。
|
|
14671
|
+
*
|
|
14672
|
+
* ⚠️ **只有两格,而缺的那两格是刻意的**:底下那张锁表里还有 `pid` 和 `host`,
|
|
14673
|
+
* 它们**不上网线** —— 界面画不了它们(写一个 pid 在卡片上没有任何人用得上),
|
|
14674
|
+
* 而把本机的进程号推给浏览器是白送的信息泄露。要排查「是哪个进程占着」的人
|
|
14675
|
+
* 手上有那张表本身(`sqlite3 ~/.epoch/sessions.db`)。
|
|
14676
|
+
*/
|
|
14677
|
+
interface WireRunningSchedule {
|
|
14678
|
+
scheduleId: string;
|
|
14679
|
+
/** 抢到锁那一刻(epoch 毫秒)。界面上那个走秒的数拿它减出来 */
|
|
14680
|
+
startedAt: number;
|
|
14681
|
+
}
|
|
14682
|
+
declare const WIRE_RUNNING_SCHEDULE_FIELDS: WireFields<WireRunningSchedule>;
|
|
14683
|
+
/**
|
|
14684
|
+
* `GET /api/schedules/running` —— 自动化那一屏上每张卡「在不在跑」的真源。
|
|
14685
|
+
*
|
|
14686
|
+
* ## 它答的是那一发 `GET /api/schedules` 答不了的事
|
|
14687
|
+
*
|
|
14688
|
+
* 那一发里的 `recentRuns` 要等一轮**跑完**才有行(执行器是收尾时才 `recordRun`
|
|
14689
|
+
* 的),所以在它跑着的那几分钟里,整个系统里唯一记着这件事的地方是那张锁表。
|
|
14690
|
+
* 而**手点「先跑一次」和到点自己跑抢的是同一把锁** —— 于是界面上「谁在跑」
|
|
14691
|
+
* 只有一个真源,不用把两种来源缝起来。
|
|
14692
|
+
*
|
|
14693
|
+
* ## ⚠️ 单开一条,理由逐字同 {@link WireSchedulePendingResponse} 那一节
|
|
14694
|
+
*
|
|
14695
|
+
* 那一发回的是「全部任务 + 最近 100 条运行 + 一次平台探测 + 表单缺省」,
|
|
14696
|
+
* 而这一格**要每几秒问一次**。把它塞进那一发等于让每一次轮询都付那份钱。
|
|
14697
|
+
*
|
|
14698
|
+
* ## ⚠️ 这一条**必须**靠客户端轮询,而那不是偷懒
|
|
14699
|
+
*
|
|
14700
|
+
* 到点触发跑在另一个进程里,服务进程不知道它存在 —— SSE 上没有任何一帧能报它
|
|
14701
|
+
* (同上面那一节)。所以第一方界面在**自动化那一屏开着时**每 5 秒拉一次,
|
|
14702
|
+
* 别的屏一发都不发。自己画前端的宿主不拉也不会坏,只是那个转子不会出现。
|
|
14703
|
+
*/
|
|
14704
|
+
interface WireScheduleRunningResponse {
|
|
14705
|
+
/** 顺序不当契约(锁表没有有意义的排序)。一条都没跑时空数组 */
|
|
14706
|
+
items: readonly WireRunningSchedule[];
|
|
14707
|
+
}
|
|
14708
|
+
declare const WIRE_SCHEDULE_RUNNING_FIELDS: WireFields<WireScheduleRunningResponse>;
|
|
14027
14709
|
|
|
14028
14710
|
/**
|
|
14029
14711
|
* 目标那一格的网线形状([方案 52](../../../docs/verify/VERIFY_RECORD-52-goal.md) PR-4 的
|
|
@@ -14617,4 +15299,4 @@ declare const SECRET_SERVICE = "epoch-agent";
|
|
|
14617
15299
|
*/
|
|
14618
15300
|
declare const MCP_DATA_KEY_NAME = "mcp-auth-data-key";
|
|
14619
15301
|
|
|
14620
|
-
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 BlankCard, type BlankGreeting, type BoundKey, type BudgetBreach, COMMAND_NAME_PATTERN, COMPLIANCE_ACTIONS, COMPLIANCE_CATEGORIES, CURRENCIES, type Catalogs, type ComplianceAction, type ComplianceCategory, type ContextBreakdown, type ContextSegment, type Currency, type CurrencyDisplay, 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, type FormatMoneyOptions, 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 HostText, type HostUser, 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, type MoneyDigits, NOOP_TELEMETRY, type NoticeCode, OPERATION_TYPES, OVERSIZE_RATIO, 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_DRAFT_TOOL, 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, SIDEBAR_MENU_MESSAGE_TYPE, SIMULTANEOUS_CONTEXTS, SYSTEM_NOTE_PREFIX, type ScheduleAllowlist, type ScheduleBackendKind, type ScheduleCycle, type ScheduleDefinition, type ScheduleDraft, type ScheduleDraftAllowlist, type ScheduleDraftCreate, type ScheduleDraftPermission, type ScheduleDraftUpdate, type SchedulePendingApproval, type SchedulePendingItem, type ScheduleRun, type ScheduleRunStatus, type ScheduleRunnerSpec, type ScheduleTemplate, type ScheduleTemplateId, type ScheduleTrigger, type ScheduleTriggerKind, type ScheduleWeekday, type ScheduleWideningKind, 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 SidebarMenuItem, type SidebarMenuMessage, 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 TrustExecGrant, type TrustGrant, type TrustLevel, type TrustManager, type TrustRecord, type TrustScope, type TrustTier, UNLIMITED_TURNS, 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_ATTACHMENT_UPLOAD_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_OPEN_RESPONSE_FIELDS, WIRE_FILE_REFERENCE_FIELDS, WIRE_FILE_STAT_MAX, WIRE_FILE_STAT_OK_FIELDS, WIRE_FILE_STAT_REFUSED_FIELDS, WIRE_FILE_STAT_RESPONSE_FIELDS, WIRE_FILE_TEXT_RESPONSE_FIELDS, WIRE_FORGET_WORKSPACE_FIELDS, WIRE_FORGET_WORKSPACE_RESPONSE_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_IMAGE_REFERENCE_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_ARCHIVE_SUFFIXES, WIRE_PLUGIN_INSTALL_FIELDS, WIRE_PLUGIN_LOCAL_FIELDS, WIRE_PLUGIN_LOCAL_INSTALL_FIELDS, WIRE_PLUGIN_MARKETPLACE_FIELDS, WIRE_PLUGIN_MARKETPLACE_RESPONSE_FIELDS, WIRE_PLUGIN_NAME_FIELDS, WIRE_PLUGIN_PREVIEW_FIELDS, WIRE_PLUGIN_PREVIEW_RESPONSE_FIELDS, WIRE_PLUGIN_REFRESH_RESPONSE_FIELDS, WIRE_PLUGIN_STAGE_FIELDS, WIRE_PLUGIN_STAGE_RESPONSE_FIELDS, WIRE_PLUGIN_UPDATE_FIELDS, WIRE_PLUGIN_UPLOAD_CONFIRM_FIELDS, WIRE_PLUGIN_UPLOAD_FIELDS, WIRE_PLUGIN_UPLOAD_FORM_FIELDS, WIRE_PLUGIN_UPLOAD_PREVIEW_RESPONSE_FIELDS, WIRE_PLUGIN_UPLOAD_RESPONSE_FIELDS, WIRE_POLICY_DIR_FIELDS, WIRE_POLICY_STATUS_FIELDS, WIRE_PROJECT_INVENTORY_FIELDS, WIRE_PROVIDERS_FIELDS, WIRE_PROVIDER_KEY_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_DROP_SUFFIXES, WIRE_SKILL_FIELDS, WIRE_SKILL_IMPORT_FIELDS, WIRE_SKILL_IMPORT_PREVIEW_FIELDS, WIRE_SKILL_REMOVE_FIELDS, WIRE_SKILL_STAGE_FIELDS, WIRE_TASKS_FIELDS, WIRE_TOOLS_FIELDS, WIRE_TOOL_GATE_FIELDS, WIRE_TOOL_ROW_FIELDS, WIRE_TRUST_REQUEST_FIELDS, WIRE_TRUST_REQUEST_RESPONSE_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 WireAttachmentUploadResponse, 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 WireFileKind, type WireFileOpenRefusal, type WireFileOpenResponse, type WireFileReference, type WireFileRefusal, type WireFileStat, type WireFileStatOk, type WireFileStatRefused, type WireFileStatResponse, type WireFileTextResponse, type WireFixture, type WireFixtureCase, type WireForgetWorkspaceRequest, type WireForgetWorkspaceResponse, 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 WireImageReference, type WireKnownWorkspace, type WireKnownWorkspaceRef, 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 WirePermissionRememberFailure, type WirePermissionRuleRow, type WirePermissionSetRequest, type WirePermissionSetResponse, type WirePermissionShadow, type WirePermissionState, type WirePermissionStatus, type WirePickDirectoryResponse, type WirePlanAction, type WirePlanModeRequest, type WirePlanModeResponse, type WirePlanModeState, type WirePlanResponse, type WirePluginActionResponse, type WirePluginCounts, type WirePluginEntry, type WirePluginHit, type WirePluginHookTally, type WirePluginInstallRequest, type WirePluginLocalInstallRequest, type WirePluginLocalRequest, type WirePluginMarketplace, type WirePluginMarketplaceRequest, type WirePluginMarketplaceResponse, type WirePluginNameRequest, type WirePluginPreview, type WirePluginPreviewRequest, type WirePluginPreviewResponse, type WirePluginRefreshResponse, type WirePluginRefuseReason, type WirePluginSourceType, type WirePluginStageRequest, type WirePluginStageResponse, type WirePluginStatus, type WirePluginUpdateRequest, type WirePluginUploadConfirmRequest, type WirePluginUploadField, type WirePluginUploadFieldName, type WirePluginUploadFields, type WirePluginUploadFile, type WirePluginUploadForm, type WirePluginUploadPlan, type WirePluginUploadPreviewResponse, type WirePluginUploadRequest, type WirePluginUploadResponse, type WirePluginVisibility, type WirePluginsResponse, type WirePolicyDir, type WirePolicyStatus, type WireProjectInventory, type WireProviderKeyRequest, type WireProviderKeyResponse, 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 WireSettingValueKind, 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 WireSkillRemoveFailure, type WireSkillRemoveResponse, type WireSkillStageFailure, type WireSkillStageResponse, type WireSkillSummary, type WireSummaryWorkspace, type WireTaggedEvent, type WireTasksResponse, type WireToolGate, type WireToolGateAxis, type WireToolRow, type WireToolSummary, type WireToolVerdict, type WireToolsResponse, type WireTrustRequestRequest, type WireTrustRequestResponse, 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, formatMoney, getProviderInfo, hasFailure, imagePartOmittedSummary, isApprovalOutcome, isContextNearlyFull, isCurrency, isHeadlessInputFormat, isHeadlessLifecycleEvent, isHeadlessOutputFormat, isKeyAction, isLang, isMentionableFilePath, isOperationType, isPermissionLevel, isPlanOutcome, isPromptOversize, isProviderType, isQuestionAnswerMap, isSearchProviderType, isValidAgentRoleName, isValidCommandName, isWireGoalPhase, isWireHubEvent, isWirePlanAction, isWirePluginArchiveName, isWireRewindScope, isWireSettingWriteLayer, isWireSkillDropName, makeTranslate, mcpConnectorState, normalizeApproval, packSessionSurface, parseChord, parseModelRef, parseSequence, perAction, placeholdersOf, promptTokens, resolveHostText, sequenceId, sessionPartSummary, stripSystemNote, systemNote, tableOf, totalUsageTokens, translatorFor, truncateToBudget, turnDigest, unfixedRules, utf8ByteLength, wireFields };
|
|
15302
|
+
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 BlankCard, type BlankGreeting, type BoundKey, type BudgetBreach, COMMAND_NAME_PATTERN, COMPLIANCE_ACTIONS, COMPLIANCE_CATEGORIES, CURRENCIES, type Catalogs, type ComplianceAction, type ComplianceCategory, type ContextBreakdown, type ContextSegment, type Currency, type CurrencyDisplay, 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, type FormatMoneyOptions, 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 HostText, type HostUser, 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, type MoneyDigits, NOOP_TELEMETRY, type NoticeCode, OPERATION_TYPES, OVERSIZE_RATIO, 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_DRAFT_TOOL, 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, SIDEBAR_MENU_MESSAGE_TYPE, SIMULTANEOUS_CONTEXTS, SYSTEM_NOTE_PREFIX, type ScheduleAllowlist, type ScheduleBackendKind, type ScheduleCycle, type ScheduleDefinition, type ScheduleDraft, type ScheduleDraftAllowlist, type ScheduleDraftCreate, type ScheduleDraftPermission, type ScheduleDraftUpdate, type SchedulePendingApproval, type SchedulePendingItem, type ScheduleRun, type ScheduleRunStatus, type ScheduleRunnerSpec, type ScheduleTemplate, type ScheduleTemplateId, type ScheduleTrigger, type ScheduleTriggerKind, type ScheduleWeekday, type ScheduleWideningKind, 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 SidebarMenuItem, type SidebarMenuMessage, 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 TrustExecGrant, type TrustGrant, type TrustLevel, type TrustManager, type TrustRecord, type TrustScope, type TrustTier, UNLIMITED_TURNS, 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_ATTACHMENT_UPLOAD_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_OPEN_RESPONSE_FIELDS, WIRE_FILE_REFERENCE_FIELDS, WIRE_FILE_STAT_MAX, WIRE_FILE_STAT_OK_FIELDS, WIRE_FILE_STAT_REFUSED_FIELDS, WIRE_FILE_STAT_RESPONSE_FIELDS, WIRE_FILE_TEXT_RESPONSE_FIELDS, WIRE_FORGET_WORKSPACE_FIELDS, WIRE_FORGET_WORKSPACE_RESPONSE_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_IMAGE_REFERENCE_FIELDS, WIRE_KNOWN_WORKSPACE_FIELDS, WIRE_LANG_PARAM, WIRE_LAST_SESSION_FIELDS, WIRE_LAST_SESSION_RESPONSE_FIELDS, 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_ARCHIVE_SUFFIXES, WIRE_PLUGIN_INSTALL_FIELDS, WIRE_PLUGIN_LOCAL_FIELDS, WIRE_PLUGIN_LOCAL_INSTALL_FIELDS, WIRE_PLUGIN_MARKETPLACE_FIELDS, WIRE_PLUGIN_MARKETPLACE_RESPONSE_FIELDS, WIRE_PLUGIN_NAME_FIELDS, WIRE_PLUGIN_PREVIEW_FIELDS, WIRE_PLUGIN_PREVIEW_RESPONSE_FIELDS, WIRE_PLUGIN_REFRESH_RESPONSE_FIELDS, WIRE_PLUGIN_STAGE_FIELDS, WIRE_PLUGIN_STAGE_RESPONSE_FIELDS, WIRE_PLUGIN_UPDATE_FIELDS, WIRE_PLUGIN_UPLOAD_CONFIRM_FIELDS, WIRE_PLUGIN_UPLOAD_FIELDS, WIRE_PLUGIN_UPLOAD_FORM_FIELDS, WIRE_PLUGIN_UPLOAD_PREVIEW_RESPONSE_FIELDS, WIRE_PLUGIN_UPLOAD_RESPONSE_FIELDS, WIRE_POLICY_DIR_FIELDS, WIRE_POLICY_STATUS_FIELDS, WIRE_PROJECT_INVENTORY_FIELDS, WIRE_PROVIDERS_FIELDS, WIRE_PROVIDER_KEY_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_ROLE_REMOVE_FIELDS, WIRE_RUNNING_SCHEDULE_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_RUNNING_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_PENDING_FIELDS, WIRE_SETTING_ROW_FIELDS, WIRE_SETTING_STEP_FIELDS, WIRE_SETTING_WRITE_FIELDS, WIRE_SETTING_WRITE_LAYERS, WIRE_SKILL_BODY_FIELDS, WIRE_SKILL_DROP_SUFFIXES, WIRE_SKILL_FIELDS, WIRE_SKILL_IMPORT_FIELDS, WIRE_SKILL_IMPORT_PREVIEW_FIELDS, WIRE_SKILL_REMOVE_FIELDS, WIRE_SKILL_STAGE_FIELDS, WIRE_TASKS_FIELDS, WIRE_TOOLS_FIELDS, WIRE_TOOL_GATE_FIELDS, WIRE_TOOL_ROW_FIELDS, WIRE_TRUST_REQUEST_FIELDS, WIRE_TRUST_REQUEST_RESPONSE_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 WireAttachmentUploadResponse, 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 WireFileKind, type WireFileOpenRefusal, type WireFileOpenResponse, type WireFileReference, type WireFileRefusal, type WireFileStat, type WireFileStatOk, type WireFileStatRefused, type WireFileStatResponse, type WireFileTextResponse, type WireFixture, type WireFixtureCase, type WireForgetWorkspaceRequest, type WireForgetWorkspaceResponse, 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 WireImageReference, type WireKnownWorkspace, type WireKnownWorkspaceRef, type WireLastSessionRequest, type WireLastSessionResponse, 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 WirePermissionRememberFailure, type WirePermissionRuleRow, type WirePermissionSetRequest, type WirePermissionSetResponse, type WirePermissionShadow, type WirePermissionState, type WirePermissionStatus, type WirePickDirectoryResponse, type WirePlanAction, type WirePlanModeRequest, type WirePlanModeResponse, type WirePlanModeState, type WirePlanResponse, type WirePluginActionResponse, type WirePluginCounts, type WirePluginEntry, type WirePluginHit, type WirePluginHookTally, type WirePluginInstallRequest, type WirePluginLocalInstallRequest, type WirePluginLocalRequest, type WirePluginMarketplace, type WirePluginMarketplaceRequest, type WirePluginMarketplaceResponse, type WirePluginNameRequest, type WirePluginPreview, type WirePluginPreviewRequest, type WirePluginPreviewResponse, type WirePluginRefreshResponse, type WirePluginRefuseReason, type WirePluginSourceType, type WirePluginStageRequest, type WirePluginStageResponse, type WirePluginStatus, type WirePluginUpdateRequest, type WirePluginUploadChoice, type WirePluginUploadConfirmRequest, type WirePluginUploadField, type WirePluginUploadFieldName, type WirePluginUploadFields, type WirePluginUploadFile, type WirePluginUploadForm, type WirePluginUploadPlan, type WirePluginUploadPreviewResponse, type WirePluginUploadRequest, type WirePluginUploadResponse, type WirePluginVisibility, type WirePluginsResponse, type WirePolicyDir, type WirePolicyStatus, type WireProjectInventory, type WireProviderKeyRequest, type WireProviderKeyResponse, 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 WireRoleRemoveFailure, type WireRoleRemoveResponse, type WireRunningSchedule, 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 WireScheduleRunningResponse, 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 WireSettingPending, type WireSettingRow, type WireSettingStep, type WireSettingValue, type WireSettingValueKind, 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 WireSkillRemoveFailure, type WireSkillRemoveResponse, type WireSkillStageFailure, type WireSkillStageResponse, type WireSkillSummary, type WireSummaryWorkspace, type WireTaggedEvent, type WireTasksResponse, type WireToolGate, type WireToolGateAxis, type WireToolRow, type WireToolSummary, type WireToolVerdict, type WireToolsResponse, type WireTrustRequestRequest, type WireTrustRequestResponse, 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, formatMoney, getProviderInfo, hasFailure, imagePartOmittedSummary, isApprovalOutcome, isContextNearlyFull, isCurrency, isHeadlessInputFormat, isHeadlessLifecycleEvent, isHeadlessOutputFormat, isKeyAction, isLang, isMentionableFilePath, isOperationType, isPermissionLevel, isPlanOutcome, isPromptOversize, isProviderType, isQuestionAnswerMap, isSearchProviderType, isValidAgentRoleName, isValidCommandName, isWireGoalPhase, isWireHubEvent, isWirePlanAction, isWirePluginArchiveName, isWireRewindScope, isWireSettingWriteLayer, isWireSkillDropName, makeTranslate, mcpConnectorState, normalizeApproval, packSessionSurface, parseChord, parseModelRef, parseSequence, perAction, placeholdersOf, promptTokens, resolveHostText, sequenceId, sessionPartSummary, stripSystemNote, systemNote, tableOf, totalUsageTokens, translatorFor, truncateToBudget, turnDigest, unfixedRules, utf8ByteLength, wireFields };
|