@epoch-agent/core 2.8.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -5
- package/dist/index.d.ts +909 -163
- package/dist/index.js +1630 -625
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -16,29 +16,29 @@ Agent 引擎。ReAct 循环、Provider 路由、权限、记忆、技能、Hook
|
|
|
16
16
|
|
|
17
17
|
| 目录 | 职责 |
|
|
18
18
|
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
19
|
-
| `agent/` | ReAct 循环(`loop.ts`)、工具执行、实时输出泵、prompt 组装、Nudge / Stall 检测、消息编解码、plan 模式状态、**运行前 token 估算**(`run-estimate.ts
|
|
19
|
+
| `agent/` | ReAct 循环(`loop.ts`)、工具执行、实时输出泵、prompt 组装、Nudge / Stall 检测、消息编解码、plan 模式状态、**运行前 token 估算**(`run-estimate.ts`)、**沙箱升权门的措辞层**(`escalation.ts`:判据在 infra 的 `sandbox/escalation.ts`,入口在 `tool-executor.ts` 的 `resolveEscalation()`,2026-10-27 方案 65 §2.1) |
|
|
20
20
|
| `provider/` | 多 provider 路由 + 降级、模型元数据探测、AI SDK 适配、**token 估算**(`tokenizer.ts`,全仓唯一口径) |
|
|
21
21
|
| `config/` | 10 层配置解析链 + 迁移 + 设置来源分层(`sources.ts`)+ **「为什么是这个值」**(`provenance.ts`,每个键赢在哪一层)+ **「我现在写、写完谁说了算」**(`write.ts`)+ JSON Schema 导出。`schema.ts` 是配置形状的**唯一**真源 |
|
|
22
22
|
| `session/` | SQLite 会话持久化 + FTS5 搜索,落盘完整 `EpochMessage`(工具调用 / artifact 引用)+ 逐轮真实用量(`recordUsage`)+ 已批准的计划 + **那一次工作区决定**(v7 两列,见下)。开给模型那一侧的检索 / 前后文窗口 / 血缘在 `query.ts` + `trace.ts`,开给人那一侧的 `@:` 引用(候选 / 解析 / 当前面)在 `reference.ts`,「哪些会话我能读」**两边共用** `authorization.ts` 那一份,索引体检在 `fts-health.ts`(`epoch doctor` 用) |
|
|
23
23
|
| `memory/` | 文件 + SQLite 双存储。**命中注入模式的条目原文照留、只在渲染给模型时遮罩**(索引块 / `prefetch` / `search` / `read` 四条通道回占位符;文件一个字节不动),写前则直接拒 —— 判据在 `manager.ts` 的 `classifyEntry` / `blockedPlaceholder` 上。配额到九成的提醒走 `takeNotices()` → 那一帧 `notice`(2026-09-28 之前是 `reviewer.ts` 的 5 分钟后台审查,结论没人读,已删) |
|
|
24
24
|
| `skill/` | `SKILL.md` 文件系统 + 自动学习(`learner.ts`)。**读正文有两条路,别混**:`view()` 是模型那条(会 `matchCount++`,而那个数喂着有效性评分、也就是模型下次看得见哪些技能),`body()` 是「人点开看一眼」那条(**一个字都不动统计**)—— 一次浏览不是一次命中,判据写在 `SkillSystem.body` 上。**`create()` 建的目录名取 frontmatter 的 `name`**(不是参数那个,2026-09-28):索引取的也是它,不对齐的话 learner 那条通知会报一个查不到的名字。**写口只有两个导出**(`previewSkillImport` / `importSkills`,方案 42 §六):从本机目录导入技能,收路径不收字节,staging 那一层刻意不导出 —— 判据在 `skill/import.ts` 文件头 |
|
|
25
25
|
| `hook/` | Hook 匹配与执行、命令执行;`sources.ts` 定「读哪几份配置」(两级 `hooks.json` + **Claude Code 的三份 `settings.json`**,方案 50),`config-loader.ts` 只管解析我们自己的格式,`claude-settings.ts` / `claude-matcher.ts` 只管翻 Claude Code 的方言 |
|
|
26
|
-
| `permission/` |
|
|
26
|
+
| `permission/` | 4 级权限(`default` / `acceptEdits` / `plan` / `auto`;老名字 `bypass` 是 `auto` 的别名;**出厂默认是 `acceptEdits`**,见 protocol 的 `DEFAULT_PERMISSION_LEVEL`)+ 审批缓存(`approval-cache.ts`:记弹窗答案;**逐条撤销**走 `revoke(id)` / `PermissionManager.revokeApproval()`,2026-08-27 补 —— 在那之前一次点错的「拒绝」只在内存里、永不过期,除了重启进程收不回来)+ 无头策略 + 紧凑规则 `Tool(content)` + **审计流水**(`audit.ts`:权限层每一次裁决的定长流水,`getAudit()` 取,**只在进程内、永不进遥测**) |
|
|
27
27
|
| `workspace/` | 这次会话的地盘:主根 + `--add-dir` 的额外根、目录体检、「哪些指令文件没被加载」,外加「已知工作区」最近使用清单(`~/.epoch/workspaces.json`,**本机文件不是服务**)。判据本身在 infra 的 `isInWorkspace`,这里是壳 |
|
|
28
28
|
| `compliance/` | **内容合规闸门**:本地词表 + 正则。**缺省只拦用户输入**(送进模型之前,命中 block 时请求不发也不计费);模型输出那一侧同一套东西都在(`agent/turn.ts` 那个唯一出口 + 句子级 holdback),一行配置就能开,但缺省关 —— 它是误伤和流式延迟的大头,判据在 `settings.ts`。变体归一化(全角 / 零宽 / 分隔符 / leet / 繁简,**带 offset 映射**因为 `mask` 要打回原文)、Aho-Corasick 多模式匹配、豁免表(治「`台独` 命中『平**台独**立部署』」那一类)、句子级 holdback 的流式 guard。**缺省跳过代码**(围栏块与行内 code)—— 这是个写代码的 agent,那一格决定这层能不能用。类别 / 动作两个联合住 [protocol](../protocol)(它们同时是配置键名、词库文件名、遥测属性值)。**内置词表是种子不是生产词库**,正式部署挂 `~/.epoch/compliance/<类别>.txt` |
|
|
29
29
|
| `policy/` | 策略文件加载与校验;`sources.ts` 定「扫哪几个 policies 目录」 |
|
|
30
30
|
| `context/` | 上下文压缩(工具结果裁剪是**摘要之前的独立一步**,头中尾保留,裁完够了就不调模型;2026-08-16 起认 `compression.*` 两个配置项,见下)、项目发现、指令文件与它的 `@import` 展开 + 项目外放行闸门、artifact 闸门与清理、图片 token 估算、`@` 提及的候选清单与解析(**文件和 `@:` 会话共享同一本 200KB 的账**,方案 53) |
|
|
31
31
|
| `sandbox/` | `CodeSandbox`(`code_exec` 的执行器)+ 隔离能力的**对外说法**:`describeIsolation()` 那句中文、以及「哪些工具走沙箱」两份清单。**机制那一半 2026-08-16 搬去了 [infra](../infra) 的 `sandbox/`**(消费者不止 core 一个了,`plugin-terminal` 够不着 core);既有 import 路径靠再导出保住 |
|
|
32
|
-
| `delegate/` | 子 agent 任务委托(`delegate_task`
|
|
32
|
+
| `delegate/` | 子 agent 任务委托(`delegate_task` 的后端;`background: true` 时登记成一格 `agent` 作业,见 [infra/jobs.ts](../infra/src/jobs.ts))。⚠️ 2026-10-27 起两件事变了:**子任务跑完把整轮按步落进会话库**(带 `parent_id`,`source` 是 `delegate`,所以它不出现在「接着哪一段聊」的列表里 —— 判据在 `DELEGATE_SESSION_SOURCE` 上),以及**超时来自 `delegate.taskTimeoutSec`**(缺省 15 分钟,`0` = 不限时;在此之前是写死的 5 分钟) |
|
|
33
33
|
| `agent-role/` | agent 角色定义、注册与工具作用域收窄(子 agent 和**顶层会话**两条来路走同一个 `roleScopedProvider`);`createRoleScope` 把它包成**可变槽** —— 身份行和工具表从同一个变量读,好让「这一条消息换个专家」有地方落。⚠️ 2026-08-18 起这里多了一条**写**的路(`create.ts`,全仓第一条建角色的路):它和读口共用同一份 frontmatter schema、同一个解析器,写完**先读回来验一遍**再落盘。⚠️ 2026-09-16 又多了它的反面(`remove.ts`,「能加就得能删」):**落点靠读口解析出来的 `name` 找,不是拿名字拼路径** —— frontmatter 的 `name` 优先于文件名,拼路径要么删不到一份界面上明明列着的身份,要么删掉另一个,而两种错法都不报错;同名两份时**一个字节都不删**。那道「什么样的客户端才准写」的闸门**两条都不在这儿**,在 `@epoch-agent/server`(判据见那两个文件头第三节) |
|
|
34
34
|
| `agent-plugin/` | **插件面板那个可变槽**(2026-10-20):`createPluginScope` —— 「这一条消息是点着哪个插件面板发的」,`<self>` 段从它读。⚠️ **和 `agent-role/` 有同一个形状、少两样东西**:没有 `provider`(插件名不换工具表)、没有身份 —— 它只影响 system prompt 里一句话,判据在那份文件头那张对照表上。别照着 `createRoleScope` 全抄一遍 |
|
|
35
35
|
| `extensions/` | 项目级扩展:自定义斜杠命令的发现 / 加载 / 插值 / 按轮次收窄工具表;三种扩展物共用的 frontmatter 解析 |
|
|
36
36
|
| `plugin/` | 插件:清单校验、四种来源的安装、安装记录(带跨进程锁)、市场、加载成「六类扩展物的来源」(`PLUGIN_LAYOUT` 那七个约定子路径)。见 [docs/PLUGINS.md](../../docs/PLUGINS.md)。⚠️ **2026-10-20 起清单里多了 `panel` 那一格**(输入框上方那块面板,`url` 只收本机回环、判据在 `manifest.ts` 的 `isLoopbackUrl` 上):**写歪了只有这一格不生效,插件照常装得上** —— 那一条口径逐字同 `visibility` / `appearance` |
|
|
37
37
|
| `tools/` | `ToolRegistry` + `builtin/` 下那批内置工具(`todo` / `memory` / `goal` / `code_exec` / `run_code` / `delegate_task` / `ask_user_question` / `skill_view` / **技能写入那四个** / `tool_search` / `schedule_draft` / plan 模式那两个 / 会话检索那**四个**)。**清单与边界的唯一真源是 [docs/TOOLS.md](../../docs/TOOLS.md)** —— 这里不再抄一个会过期的条数(原来写的「13 个」漏了 `tool_search`) |
|
|
38
|
-
| `code-mode/` | `run_code` 的 worker 运行时(方案 51):线程入口、宿主 ↔ worker
|
|
38
|
+
| `code-mode/` | `run_code` 的 worker 运行时(方案 51):线程入口、宿主 ↔ worker 的工具绑定协议、四道上限、只读闸门、**worker 的权限策略**(`policy.ts`)。⚠️ **和 `sandbox/` 是两回事,隔离模型正好相反** —— 那一个的边界是 OS 沙箱,这一个的边界是审批 + Node 权限模型(子调用通道本身在 `agent/tool-executor.ts`)。**默认关**,`tools.mode: both` 才注册那个工具 |
|
|
39
39
|
| `checkpoint/` | 写类工具动手前的文件快照 + 回退(两阶段原子、不覆盖手工改动)。见 [docs/CHECKPOINTS.md](../../docs/CHECKPOINTS.md) |
|
|
40
40
|
| `tracker/` | 待办追踪的 SQLite 表 |
|
|
41
|
-
| `schedule/` | 定时任务(方案 45):store / 单实例锁 / 触发器算术 / 保存期校验 / 录像与留存 / 注册器 / `doctor`,外加 `os/` 下的两个 OS 后端(`schtasks` / `launchd`)和 `draft.ts`(对话里那份提案的解析与判定 —— 它自己一个字节都不写,落库是工具那侧调 `ScheduleControl.create` / `update`)。**不含执行器** —— 跑一次要 `buildRuntime()`,那在 [runtime](../runtime) 的 `fireSchedule()`
|
|
41
|
+
| `schedule/` | 定时任务(方案 45):store / 单实例锁 / 触发器算术 / 保存期校验 / 录像与留存 / 注册器 / `doctor`,外加 `os/` 下的两个 OS 后端(`schtasks` / `launchd`)和 `draft.ts`(对话里那份提案的解析与判定 —— 它自己一个字节都不写,落库是工具那侧调 `ScheduleControl.create` / `update`)。**不含执行器** —— 跑一次要 `buildRuntime()`,那在 [runtime](../runtime) 的 `fireSchedule()`。⚠️ store 的**读写两侧**都过一遍老档位名解析(`permissionOfRow`):库里可能躺着 `bypass` 合并之前写进去的 `"bypass"`,而它是另一条路(云同步)**从别的进程**交过来的,类型在进程边界上什么也不保证 |
|
|
42
42
|
| `cost/` | 计价表、预算守卫、花费状态。缓存命中单独计价,口径见 `pricing.ts` 文件头 |
|
|
43
43
|
| `compat/` | 竞品词汇表。今天只有 `claude-tool-names.ts`(Claude Code 工具名 ↔ 我们的,双向从同一份数据推出来)—— 放在 `hook/` 之外是因为方案 50 §八 点名的第二个消费者(兼容它的权限规则)不住在 hook 里 |
|
|
44
44
|
| `trust/` | 工作区信任判定与闸门 |
|
|
@@ -163,6 +163,22 @@ clone 下来一个仓库就能让我们跑它写的命令。而它还多一层
|
|
|
163
163
|
`UserPromptSubmit` **刻意不加载**:命令型 hook 在 `transform:*` 事件上今天不会被
|
|
164
164
|
执行(`triggerTransform` 只遍历进程内注册的 JS hook),装进去只会让它看起来在跑。
|
|
165
165
|
|
|
166
|
+
**过了闸门之后,那条命令跑在有边界的子进程里(2026-10-26)。**
|
|
167
|
+
`hook/command-runner.ts` 原来是一句裸的 `spawn(command, { shell: true })`,而同一个
|
|
168
|
+
仓库里 `terminal` 是包在 `confine()` 里的 —— 同一能力两套边界(方案 65 §2.3)。
|
|
169
|
+
现在 hook 走同一条路,边界钉死成 `workspace-write`(`hookSandboxPolicy()`):
|
|
170
|
+
**不跟权限档推**,因为 `auto` 那一档推到的是 `danger-full-access`,
|
|
171
|
+
那两档下 `confine()` 直接短路成「没包上」,等于这一条在那两档里一点没做。
|
|
172
|
+
`sandbox.terminal` 那个开关也不管它 —— 它的名字逐字是 terminal,顺带管 hook
|
|
173
|
+
等于把「我关掉终端沙箱是因为 `pnpm install` 被挡了」变成「hook 也一起放开了」。
|
|
174
|
+
Windows 没有后端([infra 的 `sandbox/backend.ts`](../infra/src/sandbox/backend.ts)),
|
|
175
|
+
所以那一支**逐字节保持改之前的样子**。
|
|
176
|
+
|
|
177
|
+
⚠️ **沙箱拒绝不进表决**:`verify:stop` 上「阻止」等于「不许收工」,而被挡住的
|
|
178
|
+
`verify:stop` 下一轮还是被挡住 —— 确定性死循环。它走 `reportIfNotRun()` 那条
|
|
179
|
+
warn,说的是「这条 hook 没跑成」,不是「它通过了」。而「放行」本身沿用的是
|
|
180
|
+
2026-08-08 那条取舍(hook 当前形态不是安全边界),这一笔没动它。
|
|
181
|
+
|
|
166
182
|
**闸门画在 `sources.ts` 里,不在 `HookManager` / `loadPolicyFiles` 里。**
|
|
167
183
|
装配层调 `HookManager` 那一步裹在 `safeInit` 里,闸门判定要是写在里面,
|
|
168
184
|
一次异常就被降级成一条诊断 —— 而「闸门抛异常了」和「闸门放行了」
|
|
@@ -185,6 +201,23 @@ clone 下来一个仓库就能让我们跑它写的命令。而它还多一层
|
|
|
185
201
|
里的 zod 定义比对 `schemas/*.json`),`scripts/gen-settings-schema.mjs` 只是把它写到盘上。
|
|
186
202
|
字段说明表是**必填**的:往 zod 里加字段却没登记,`buildSchemaExports()` 直接抛。
|
|
187
203
|
|
|
204
|
+
2026-10-26 起产物是**三份**:两份设置文件(项目级 / 企业托管)+ `config.schema.json`
|
|
205
|
+
(用户级 `~/.epoch/config.yaml`,48 个顶层键 / 83 个叶子)。第三份按 `io: 'input'`
|
|
206
|
+
生成(它描述「用户可以写什么」,不是产出值的形状),并多补两个 zod 里没有的属性:
|
|
207
|
+
`$schema`(用户自己写的那行)与 `version`(迁移流程读走,`CONFIG_KEYS` 专门补过它)——
|
|
208
|
+
不补的话 `additionalProperties: false` 会把我们自己让人写的键标成错误。
|
|
209
|
+
|
|
210
|
+
**`config/key-table.ts`:配置键的全量表,也从 schema 派生。**
|
|
211
|
+
它把 `z.toJSONSchema()` 的每个叶子折成一行(路径 / 类型 / 枚举 / 边界 / 内置默认 /
|
|
212
|
+
可写性),`epoch config set` / `unset` / `list` 和报错提示都查这一张表 —— 在那之前
|
|
213
|
+
那些名单是**手抄**在 cli 里的三张 `Record`(24 个键),而 schema 有 83 个叶子,
|
|
214
|
+
差的五十多个能写进 yaml 却没有入口认。可写性分三档(`scalar` / `manual` / `blocked`),
|
|
215
|
+
判据写在那个文件的文件头上;`checkConfigValue()` 按 JSON Schema 的
|
|
216
|
+
`type` / `enum` / `minimum` / `exclusiveMinimum` / `maximum` / `minLength` 校验,
|
|
217
|
+
所以值域**永远等于 schema**,不再是一份人肉对齐的近似。
|
|
218
|
+
键表完整性由 `__tests__/config-key-table.test.ts` 盯着,它自己数一遍叶子再和表比 ——
|
|
219
|
+
**判据不和实现共用遍历**,否则遍历逻辑错了两边一起错。
|
|
220
|
+
|
|
188
221
|
**沙箱是隔离,不是安全边界。** 危险命令的判定表在 [infra](../infra) 的
|
|
189
222
|
`command-safety.ts`,那是**护栏不是沙箱**。真沙箱在 infra 的 `sandbox/`。
|
|
190
223
|
|
|
@@ -198,6 +231,13 @@ clone 下来一个仓库就能让我们跑它写的命令。而它还多一层
|
|
|
198
231
|
`code_exec` 那半没有 —— 它的可写目录不是猜出来的(就是自己那个临时目录),
|
|
199
232
|
不存在「一条本来能跑的命令撞上没登记的缓存目录」这种故障,也就没有开关要给。
|
|
200
233
|
|
|
234
|
+
⚠️ **上面那句「写入限于沙箱临时目录、默认禁网」是 macOS / Linux 后端那一档的读法。**
|
|
235
|
+
**Windows 上没有 OS 级后端**(方案 46 §6.2 判过:代价配不上收益),2026-10-27 起
|
|
236
|
+
`code_exec` 的 **JavaScript 那档**挂了 Node 自己的权限模型
|
|
237
|
+
(`sandbox/node-permission.ts`,Node ≥ 22.5 才挂):写不出沙箱目录、起不了子进程、
|
|
238
|
+
读取限于沙箱目录和工作区,**网络仍然通**;Python 那档没有这层限制。判据、没拦住的
|
|
239
|
+
那几样和真机实测读数都在那个文件的文件头。**它防的是跑飞的代码误改盘,不是恶意代码。**
|
|
240
|
+
|
|
201
241
|
**工具的实时输出由引擎定节奏,不由工具自己定。** 工具只管把 chunk 交给
|
|
202
242
|
`ctx.onOutput`,合并窗口、只发完整行、剥 ANSI、`\r` 折叠、总量封顶五件事都在
|
|
203
243
|
`agent/tool-output-pump.ts` 里做完,再变成 `tool-output-delta` 事件。放在这里是因为
|
|
@@ -274,6 +314,37 @@ clone 下来一个仓库就能让我们跑它写的命令。而它还多一层
|
|
|
274
314
|
| 轮间压缩(`maybeCompress`) | provider 报的提示词 | 软线交给 `shouldCompress`;**硬线不认它** |
|
|
275
315
|
| 第一发送出去之前(`turn === 1`) | `estimateRunTokens()` | 只判硬线(估算偏,拿它触发软线会白压) |
|
|
276
316
|
| provider 报 `1030` 之后 | — | 压一次重发一次,**一次 run 只一次** |
|
|
317
|
+
| **图片字节线**(2026-10-28) | `requestImageBytes()` | 摘最老的图重发;还超才压一次 |
|
|
318
|
+
|
|
319
|
+
### 第四条是**字节**,不是 token,别并进上面三行
|
|
320
|
+
|
|
321
|
+
上面三行判的全是 token,而图片这两个量纲会**彻底分家**:一张 2 MB 的截图在
|
|
322
|
+
token 账上只记一千出头(`pendingImageTokens` 按宽高估),在请求体里是 2.7 MB。
|
|
323
|
+
于是「几张截图」在 token 账上还空着九成,请求体已经撞穿网关那条上限了 ——
|
|
324
|
+
表现是 `PROVIDER_BODY_TOO_LARGE`(1051),而上面三条线一条都不会动。
|
|
325
|
+
|
|
326
|
+
判据全文在 [context/request-bytes.ts](./src/context/request-bytes.ts) 的文件头。
|
|
327
|
+
三个落点:
|
|
328
|
+
|
|
329
|
+
| 落在哪 | 做什么 | 代价 |
|
|
330
|
+
| --------------------- | -------------------------------------- | ------------------------ |
|
|
331
|
+
| `agent/loop.ts` 轮初 | 超预算就摘最老的图 | **0**(不发请求) |
|
|
332
|
+
| `agent/loop.ts` catch | 撞 1051 → 摘图重发;还超 → 压一次再发 | 第一步 0,第二步一发摘要 |
|
|
333
|
+
| `runtime` 跑前 | 摘图**并写回持久历史**(治本的那一半) | **0** |
|
|
334
|
+
|
|
335
|
+
⚠️ **压缩那一段的边界 2026-10-28 拆掉了**:原来撞 1051 而 token 那一侧还很宽松时
|
|
336
|
+
压缩器压不动(尾部预算是窗口的比例,窗口大而历史短时中段是空的,它当场返回),
|
|
337
|
+
那一档真正起作用的只有摘图。现在撞墙那一次传 `force: true` —— 尾部预算按 0 算,
|
|
338
|
+
中段就是「除头和最近那几条之外的全部」,于是压得动了。
|
|
339
|
+
|
|
340
|
+
`force` **只松那份按 token 摊开的预算**,头部 `protectFirstN` 和尾部那两条下限
|
|
341
|
+
(`protectLastN` / `minTailUserMessages`)一条都不松 —— 前者是摘要模板 `## Goal`
|
|
342
|
+
的锚点,后者让模型知道自己刚才在干什么。判据全文在 `CompressConfig.force`。
|
|
343
|
+
|
|
344
|
+
另一处 2026-10-28 一起修的:`shrank()` 原来**只数 `content` 字符**。而压缩总会剥掉
|
|
345
|
+
图片(`flattenMessage` 丢 `parts`),图片在 `content` 里却只投影成 `[图片]` 几个字
|
|
346
|
+
—— 于是「几 MB 的图被剥掉了」在它眼里可能等于「一个字没少」,判为没压动而把一次
|
|
347
|
+
已经救下来的压缩丢掉。现在字符或图片字节任一变小就算压动了。
|
|
277
348
|
|
|
278
349
|
三条读代码时必须知道的:
|
|
279
350
|
|
|
@@ -470,6 +541,38 @@ artifacts 的 512MB 从此**出自同一张表**。在这之前两个数字各
|
|
|
470
541
|
的 AST 用例**(`__tests__/provider-response-format-sites.test.ts`)而不是注释:
|
|
471
542
|
这类改动漏的从来不是眼前那两处,是并行分支上新增的第三处。
|
|
472
543
|
|
|
544
|
+
## 工具表的缓存断点 + 顺序即缓存(2026-10-26)
|
|
545
|
+
|
|
546
|
+
工具定义排在 system 和 messages **之前**,所以它是每一条请求里最贵、最稳的那一块
|
|
547
|
+
(实测 33 个内置 ≈ 5.7k tokens)。它在账单上值多少,**取决于打的是哪一家**:
|
|
548
|
+
|
|
549
|
+
| provider | 机制 | 我们要做的 |
|
|
550
|
+
| -------------------------- | ---------------------------------------------- | ------------------------------ |
|
|
551
|
+
| OpenAI / DeepSeek / Gemini | 自动,前缀字节不变就命中 | 什么都别做,**但别把顺序弄乱** |
|
|
552
|
+
| Anthropic | **必须显式打 `cache_control`**,不打就是零命中 | 给工具表最后一个打一个临时断点 |
|
|
553
|
+
|
|
554
|
+
于是两条:
|
|
555
|
+
|
|
556
|
+
- `toSdkTools(tools, { cacheBreakpoint: true })` 在**最后一个**工具上挂
|
|
557
|
+
`{ anthropic: { cacheControl: { type: 'ephemeral' } } }`(断点的语义是「缓存到
|
|
558
|
+
这里为止的前缀」,打末尾才盖住整张表)。不按 provider 分支 —— 路由可能降级换家,
|
|
559
|
+
按起点判的话降级到 Anthropic 那一次就没有断点了;而 `providerOptions` 的契约是
|
|
560
|
+
「SDK 只把匹配当前 provider 名字的那一格递下去」。判据在
|
|
561
|
+
[sdk-adapter.ts](src/provider/sdk-adapter.ts) 的 `ANTHROPIC_CACHE_BREAKPOINT` 上
|
|
562
|
+
- ⚠️ **`ToolRegistry.replaceBySource` 而不是「撤回再注册」**。热更新(MCP 的
|
|
563
|
+
`list_changed` / 重连)原来走的是 `unregisterBySource` + `registerTools`,而注册表
|
|
564
|
+
是 `Map` —— 删了再插等于那个来源的工具全部挪到表尾,前两家那边整段历史按全价重填。
|
|
565
|
+
现在是同名同源就地覆盖、新增的才 append
|
|
566
|
+
|
|
567
|
+
加固在这两处各有一条:`__tests__/provider-cache-breakpoint.test.ts`(含一条扫源码的
|
|
568
|
+
AST 门禁,每个 `toSdkTools(` 调用点都得传 `cacheBreakpoint`)、
|
|
569
|
+
`__tests__/tool-provider.test.ts` 的 `replaceBySource` 那一组(断的是**精确顺序**,
|
|
570
|
+
不是「名字都还在」—— 后者在旧实现下也是绿的)。
|
|
571
|
+
|
|
572
|
+
⚠️ **有一条东西单元测试守不住**:标记要经 AI SDK 转一手才变成 `tools[].cache_control`,
|
|
573
|
+
我们的对象里有、请求体里没有的话,所有用例照样全绿而 Anthropic 一次都不命中。
|
|
574
|
+
那一层由 `packages/runtime/scripts/probe-anthropic-cache.mts` 量(真实出网请求体)。
|
|
575
|
+
|
|
473
576
|
## 「我的模型」:按条目路由(2026-10-15)
|
|
474
577
|
|
|
475
578
|
`<homeDir>/models.json` 里每一条 = 端点 + key + 模型名 + 能力声明,`model` / `models.utility` /
|