@epoch-agent/runtime 0.2.0 → 0.3.2

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 CHANGED
@@ -49,13 +49,15 @@ for await (const event of runtime.session.run('把 README 里过时的命令改
49
49
  | `ApprovalRelay` / `serializeStream()` / `toSerializable()` | 审批 UI 在另一个进程时,把事件里过不去 IPC 的闭包换成 `requestId` |
50
50
  | `registerLocalTools()` / `registerMcpTools()` / `McpServerBatch` | 单独装工具(自定义宿主)。⚠️ `registerMcpTools` 的第四个参数收的是**批次数组**(宿主注入的 / 插件带的),再来一批就加一个元素,**别加第五个参数** |
51
51
  | `listMcpServers()` / `mcpLogin()` / `mcpLogout()` / `probeMcpServer()` | `epoch mcp` 的后端,不带 UI |
52
- | `EpochRuntime.mcp` / `McpControl` | MCP 的**写口**,照 `WorkspaceControl` 收窄过:`reconnect()`(重连一台)+ `add()`(加一台并落盘)+ `readConfig()` / `writeConfig()` / `applyConfig()`(那份文件的原文读写与生效,2026-08-18)。⚠️ **`McpRegistry` 刻意不交出去**,加能力是在这上面加方法,见下 |
52
+ | `EpochRuntime.mcp` / `McpControl` | MCP 的**写口**,照 `WorkspaceControl` 收窄过:`reconnect()`(重连一台)+ `add()`(加一台并落盘)+ `update()` / `remove()`(改一台 / 删一台,就地改写那份文件,2026-09-02)+ `readConfig()` / `writeConfig()` / `applyConfig()`(那份文件的原文读写与生效,2026-08-18)。⚠️ **`McpRegistry` 刻意不交出去**,加能力是在这上面加方法,见下 |
53
53
  | `writeMcpServer()` / `McpAddInput` | 往用户手写的 `~/.epoch/mcp.json` 里**插一段**(注释 / 键序 / 缩进一个字节不动)。不带连接,也不带 UI |
54
+ | `updateMcpServer()` / `removeMcpServer()` | 那份文件的**就地替换 / 删除**(2026-09-02):只碰点名那一台,别的 server 和注释 / 键序 / 缩进一个字节不动。判据见下 |
55
+ | `createMcpTools()` / `MCP_ADD_TOOL` … | 模型自己管 MCP 那四个工具(`mcp_list` / `mcp_add` / `mcp_update` / `mcp_remove`,2026-09-02)。⚠️ 转出来是给**用例**用的,真正的接线在 `buildRuntime` 里;清单与边界见 [docs/TOOLS.md](../../docs/TOOLS.md) |
54
56
  | `readMcpConfigFile()` / `writeMcpConfigFile()` / `mcpConfigRevision()` | 那份文件的**整份**读写(2026-08-18):原文进、原文出,带一个内容指纹防覆盖。⚠️ 和上一条不是一条路 —— 那条写的是**我们生成的**内容,这条写的是**用户敲的**字节,见下 |
55
57
  | `applyMcpConfig()` / `McpApplyOutcome` | 把盘上那份**搬进这个进程**(2026-08-18):该断的断、该连的连、没变的一个字节不动。⚠️ 只动 `source === 'user'` 那批,见下 |
56
58
  | `EpochRuntime.workspace` / `wireWorkspace()` / `splitDirList()` | **引导会话**的地盘(主根 + `--add-dir`)。要判路径在不在范围内问它 |
57
59
  | `EpochRuntime.workspaces` / `WorkspaceControl` / `SessionWorkspaces` | 每会话绑一个工作区(决定 18):绑 / 查 / 解绑 + 「已知工作区」清单。`bind()` 收一个可选的 `lang`(方案 58 PR-2)—— 只影响失败那句 `detail`,`reason` 是契约 |
58
- | `EpochRuntime.skillImport` / `SkillControl` | 从**本机目录**导入技能到 `~/.epoch/skills/`(方案 42 §六):`preview()` 看一眼、`import(source, token)` 真写。两者都收一个可选的 `lang`(方案 58 PR-2)。⚠️ **收路径不收字节**,见下 |
60
+ | `EpochRuntime.skillWrite` / `SkillControl` | 技能那一栏的**写口**,落点 `~/.epoch/skills/`:`preview()` 看一眼、`import(source, token)` 真写(方案 42 §六)、`remove(name)` 删掉一份(2026-09-01,「能加就得能删」)。三个都收一个可选的 `lang`(方案 58 PR-2)。⚠️ 导入**收路径不收字节**,见下;⚠️ `remove` 只删得动用户级,且回执带**刚被销毁的那个目录** |
59
61
  | `EpochRuntime.roleWrite` / `RoleWriteControl` | **建一个身份**,落在 `~/.epoch/agents/<name>.md`(2026-08-18)。只有 `add()` —— 没有删也没有改。⚠️ 建完**当场重载这个进程的角色表**;⚠️ 那道「回环绑定才给写」的闸门**不在这一层**,见下 |
60
62
  | `buildRuntime({ hostMarketplaces })` / `EpochRuntime.plugins` / `PluginControl` | 宿主预置一份插件市场,用户从那张单子里挑一个装(方案 59 E1):`list` / `search` / `preview`→`install(token)` / `previewUpdate`→`update(token)` / `uninstall` + 一格 `pendingRestart`。**没给 `hostMarketplaces` 就是 `null`**。⚠️ 只收 `<市场>/<插件>` 收不了路径;⚠️ 远程 source 默认关;⚠️ **装完要重建 runtime**,见下 |
61
63
  | `EpochRuntime.trusted` | 这次装配**实际**按不按信任在跑。别在宿主侧重查一遍信任表,见下 |
@@ -72,30 +74,34 @@ for await (const event of runtime.session.run('把 README 里过时的命令改
72
74
 
73
75
  ## 文件
74
76
 
75
- | 文件 | 职责 |
76
- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
77
- | `build.ts` | `buildRuntime()`。显式手写装配,不用 DI 容器 |
78
- | `host-preset.ts` | 宿主预置(方案 54 §六):装配前落文件。四条语义写在它的文件头,别在别处再判一次 |
79
- | `host-capabilities.ts` | 宿主注入的能力清单(方案 44 §2.1):校验 + 拼前缀。**和上一行不是一件事** —— 那个写用户的文件,这个一个字节都不落盘 |
80
- | `plugin-mcp-servers.ts` | 已装插件根下那份 `mcp.json`(方案 44 PR-3):读路径 → 拼 `<插件名>__` 前缀。core 只交路径(它不许 import `plugin-mcp`),解析在这儿 |
81
- | `host-marketplaces.ts` | 宿主预置市场(方案 59 E1)**装配时**那一半:接上 / 重拉 / 远程默认拒 + 诊断。⚠️ **它会落盘**(和 `hostCapabilities` 相反),理由在它的文件头第一节 |
82
- | `plugin-control.ts` | `EpochRuntime.plugins` 那六个动词。**一条安装逻辑都没有**(那在 core)——它守的是三条判据:不热加载 / 只收 `<市场>/<插件>` / 「这一程加载了谁」和「盘上装了谁」的差 |
83
- | `plugin-install.ts` | 「装一次」那个动作的**结局形状** + preview → install 两段。中间那条缝用 token 缝上,判据同 core 的 `skill/import.ts` |
84
- | `session-factory.ts` | 多会话工厂:**全仓唯一一处** `new AgentLoop` + `new AgentSession`,外加审批/提问的路由 |
85
- | `utility.ts` | `EpochRuntime.utility` 那**一个动词**。它有一整套自己的判据要落点:挂哪个 router、桶的钥匙(`sessionId ?? utility:<label>`)、以及那两条记账后果 |
86
- | `services.ts` | provider / session / memory / skill / hook / 权限 / 上下文 / 沙箱;设置分层的合并点也在这儿 |
87
- | `tools.ts` | **编译期**插件(`EpochPlugin`)+ 内置工具 + MCP 全部进同一个 `ToolRegistry` |
88
- | `commands.ts` | 自定义斜杠命令的装配:加载命令表、包一层可按轮次收窄的工具 provider |
89
- | `workspace.ts` | `--add-dir` 落地(目录体检 告知权限层 进启动诊断)+ 每会话的工作区绑定与信任判定 |
90
- | `agent-session.ts` | 审批回调流事件、历史累积、逐轮持久化、**划检查点的「一轮」** |
91
- | `session-store.ts` | `SessionManager` 的薄封装,落盘的是完整形状 |
92
- | `serialize.ts` | 跨 IPC 的事件序列化 + `ApprovalRelay` |
93
- | `events.ts` | 进程级事件观测口(方案 61):**总线本体 + 唯一那个单例**。发布点不在这儿,在 `agent-session.ts` 的两处 `yield` 之前。⚠️ 它**不做投影** —— 和 `serialize.ts` 的关系写在它的文件头 |
94
- | `mcp-admin.ts` | MCP 运维门面(`cli` 不许直接依赖 `plugin-mcp`,所以从这儿转) |
95
- | `mcp-config-write.ts` | 往用户手写的 `~/.epoch/mcp.json` 里**插一段**(`POST /api/mcp` 走它):注释 / 键序 / 缩进一个字节不动。判据同 `infra/src/config-yaml.ts` |
96
- | `mcp-config-file.ts` | 那份文件的**整份**读写(2026-08-18):原文进、原文出 + 内容指纹 + 换行符跟着盘上那份走。⚠️ 和上一条**不冲突**,主语不同,见下 |
97
- | `mcp-config-apply.ts` | 把盘上那份搬进这个进程:四档动作(新增 / 重连 / 没动 / 断开)+ 「只动用户那批」 |
98
- | `telemetry/` | OTel / 文件两种 sink,`@opentelemetry/*` 是**可选** peer |
77
+ | 文件 | 职责 |
78
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
79
+ | `build.ts` | `buildRuntime()`。显式手写装配,不用 DI 容器 |
80
+ | `host-preset.ts` | 宿主预置(方案 54 §六):装配前落文件。四条语义写在它的文件头,别在别处再判一次 |
81
+ | `host-capabilities.ts` | 宿主注入的能力清单(方案 44 §2.1):校验 + 拼前缀。**和上一行不是一件事** —— 那个写用户的文件,这个一个字节都不落盘 |
82
+ | `plugin-mcp-servers.ts` | 已装插件根下那份 `mcp.json`(方案 44 PR-3):读路径 → 拼 `<插件名>__` 前缀。core 只交路径(它不许 import `plugin-mcp`),解析在这儿 |
83
+ | `host-marketplaces.ts` | 宿主预置市场(方案 59 E1)**装配时**那一半:接上 / 重拉 / 远程默认拒 + 诊断。⚠️ **它会落盘**(和 `hostCapabilities` 相反),理由在它的文件头第一节 |
84
+ | `plugin-control.ts` | `EpochRuntime.plugins` 那六个动词。**一条安装逻辑都没有**(那在 core)——它守的是三条判据:不热加载 / 只收 `<市场>/<插件>` / 「这一程加载了谁」和「盘上装了谁」的差 |
85
+ | `plugin-install.ts` | 「装一次」那个动作的**结局形状** + preview → install 两段。中间那条缝用 token 缝上,判据同 core 的 `skill/import.ts` |
86
+ | `session-factory.ts` | 多会话工厂:**全仓唯一一处** `new AgentLoop` + `new AgentSession`,外加审批/提问的路由 |
87
+ | `utility.ts` | `EpochRuntime.utility` 那**一个动词**。它有一整套自己的判据要落点:挂哪个 router、桶的钥匙(`sessionId ?? utility:<label>`)、以及那两条记账后果 |
88
+ | `services.ts` | provider / session / memory / skill / hook / 权限 / 上下文 / 沙箱;设置分层的合并点也在这儿 |
89
+ | `tools.ts` | **编译期**插件(`EpochPlugin`)+ 内置工具 + MCP 全部进同一个 `ToolRegistry` |
90
+ | `commands.ts` | 自定义斜杠命令的装配:加载命令表、包一层可按轮次收窄的工具 provider |
91
+ | `workspace-files.ts` | `@文件` 补全的收窄面(方案 63),2026-09-02 起也是 Web UI「路径可点」的底座:列候选 / 读正文 / 看一眼是什么 / 算「用什么命令交给系统应用」。**四个方法都过同一道 `file_read` 权限闸** —— 少一个,浏览器就能看到模型看不到的东西 |
92
+ | `workspace.ts` | `--add-dir` 落地(目录体检 告知权限层 → 进启动诊断)+ 每会话的工作区绑定与信任判定 |
93
+ | `agent-session.ts` | 审批回调 → 流事件、历史累积、逐轮持久化、**划检查点的「一轮」** |
94
+ | `session-store.ts` | `SessionManager` 的薄封装,落盘的是完整形状 |
95
+ | `serialize.ts` | IPC 的事件序列化 + `ApprovalRelay` |
96
+ | `events.ts` | 进程级事件观测口(方案 61):**总线本体 + 唯一那个单例**。发布点不在这儿,在 `agent-session.ts` 的两处 `yield` 之前。⚠️ 它**不做投影** —— 和 `serialize.ts` 的关系写在它的文件头 |
97
+ | `mcp-admin.ts` | MCP 运维门面(`cli` 不许直接依赖 `plugin-mcp`,所以从这儿转) |
98
+ | `mcp-config-write.ts` | 往用户手写的 `~/.epoch/mcp.json` 里**插一段**(`POST /api/mcp` 走它):注释 / 键序 / 缩进一个字节不动。判据同 `infra/src/config-yaml.ts` |
99
+ | `mcp-config-file.ts` | 那份文件的**整份**读写(2026-08-18):原文进、原文出 + 内容指纹 + 换行符跟着盘上那份走。⚠️ 和上一条**不冲突**,主语不同,见下 |
100
+ | `mcp-config-apply.ts` | 把盘上那份搬进这个进程:四档动作(新增 / 重连 / 没动 / 断开)+ 「只动用户那批」 |
101
+ | `mcp-config-edit.ts` | 那份文件的**就地替换 / 删除**(2026-09-02):`mcp_update` / `mcp_remove` 走它。**删除是这一族里唯一会动到用户已写下字节的动作**(那个逗号),见下 |
102
+ | `mcp-json-scan.ts` | 上面三条写 MCP 配置的路**共用**的那份文本扫描器(字符串 / 两种注释 / 定位 `servers` 那个 `{`)。各写一份的下场是同一份 `mcp.json` 在两个动作下一个能做一个说「看不懂」 |
103
+ | `mcp-tools.ts` | 模型自己管 MCP 那四个工具。住在 runtime 而不是 core,因为它们要的 `McpControl` 是装配层的产物 |
104
+ | `telemetry/` | OTel / 文件两种 sink,`@opentelemetry/*` 是**可选** peer |
99
105
 
100
106
  ## 设置分层在这儿落地
101
107
 
@@ -284,12 +290,17 @@ router,**没有「别的会话那一份」** —— 它答的是「这个进
284
290
 
285
291
  ## 导入技能:**收路径,不收字节**(方案 42 §六)
286
292
 
287
- `EpochRuntime.skillImport` 转出去的是**两个函数**,不是 `SkillSystem` ——
293
+ `EpochRuntime.skillWrite` 转出去的是**三个函数**,不是 `SkillSystem` ——
288
294
  后者上有六个写方法、一个会 `matchCount++` 的 `view()`、还有一个会**自动删**
289
- 低效技能的 `maintain()`。交出去等于让网线那一侧够得着「删掉用户的技能」
290
- 和「污染那本喂给模型的评分」。收窄之后 `@epoch-agent/server` 那个包**写不出**
295
+ 低效技能的 `maintain()`。交出去等于让网线那一侧够得着「污染那本喂给模型的评分」
296
+ 和「一口气自动淘汰一批」。收窄之后 `@epoch-agent/server` 那个包**写不出**
291
297
  别的动作 —— 那不是靠 review 盯住的,是编译期的事(同 `McpControl` 只露 reconnect)。
292
298
 
299
+ ⚠️ **2026-09-01 这一格从 `skillImport` 改名成 `skillWrite`**,因为那天它多了
300
+ 一个 `remove`:一个叫 `skillImport` 的字段上挂着「递归删目录」是这一层最不该有的
301
+ 假话。而多出来的那一个是**逐条点名删**(`remove(name)`),和上面说的 `maintain()`
302
+ 那种「自动淘汰一批」仍然是两件事 —— 后者照旧不出这个包。
303
+
293
304
  ⚠️ **两个动作都只收一条服务端本机路径。** 判据全文在 core 的
294
305
  `skill/import.ts` 文件头,一句话:用户级技能目录**连信任闸门都没有**,
295
306
  往它里面落一份文件 = 让那段文字无条件进往后每一轮的 system prompt;
@@ -301,14 +312,14 @@ router,**没有「别的会话那一份」** —— 它答的是「这个进
301
312
  嵌入宿主要注意:这条路的落点**永远是 `SkillSystem` 自己那个用户级目录**,
302
313
  调用方给不了。让宿主传一个目录进来,它就成了「往任意目录写文件」的口子。
303
314
 
304
- ## 改 `mcp.json`:**两条路,主语不同**(2026-08-18
315
+ ## 改 `mcp.json`:**两条路,主语不同**(2026-08-18;2026-09-02 加了两个动作)
305
316
 
306
317
  这一层有两条写 `~/.epoch/mcp.json` 的路,而它们**不许互相取代**:
307
318
 
308
- | 路 | 写下去的是 | 做法 |
309
- | ------------------------------------------- | ------------------ | ------------------------ |
310
- | `writeMcpServer()`(`mcp-config-write`) | **我们生成的**一段 | 只插入,一个已有字节不碰 |
311
- | `writeMcpConfigFile()`(`mcp-config-file`) | **用户敲的**字节 | 整份覆盖 |
319
+ | 路 | 写下去的是 | 做法 |
320
+ | -------------------------------------------------------------- | ------------------ | ---------------------- |
321
+ | `writeMcpServer()` / `updateMcpServer()` / `removeMcpServer()` | **我们生成的**一段 | 就地改,只碰点名那一台 |
322
+ | `writeMcpConfigFile()`(`mcp-config-file`) | **用户敲的**字节 | 整份覆盖 |
312
323
 
313
324
  `mcp-config-write.ts` 的文件头逐字判掉了「读进来 → 改 → 整份写回」:我们读进来的
314
325
  不是原文(`parseMcpConfig` 吐的是归一化之后的结构),整份写回会把 `connect_timeout`
@@ -319,6 +330,21 @@ router,**没有「别的会话那一份」** —— 它答的是「这个进
319
330
  所以注释 / 空行 / 键序 / snake_case 键名 / 未知键全都原样活着。⚠️ 别把它读成
320
331
  「那条判据过期了」,也别拿它去简化第一行。
321
332
 
333
+ ### 📮 2026-09-02:第一行从「只插入」变成三个动作
334
+
335
+ `mcp-config-write.ts` 的文件头原来有一句总纲:「**我们对这个文件只有一次插入的
336
+ 权利**」。这一轮把它推翻了,而理由不是「插入不够用」,是**模型手上那条路没有第二
337
+ 个选项**:`mcp_update` / `mcp_remove` 那两个工具([docs/TOOLS.md](../../docs/TOOLS.md#管-mcp-自己2026-09-02))
338
+ 要么走就地改写,要么走第二行那条「读原文 → 整份写回」,而后者意味着把整份
339
+ `mcp.json` 交进模型的上下文 —— 里面装着他每一台 server 的 `env` 密钥。
340
+ 一条为了保住排版而存在的判据,不该以「顺带把密钥全交出去」为代价来维持。
341
+
342
+ ⚠️ **变的是「几次」,不是「什么」。** 替换和删除照旧是定位 + 拼接文本,上面那段
343
+ 「整份写回会改写他配置的语义」一个字都没过期。**删除是这一族里唯一会动到用户已经
344
+ 写下的字节的动作** —— 被删那台前面或后面那个逗号是他写的,而留着它就是一份坏 JSON。
345
+ 边界(只动那一个逗号、只动只剩空白的那一行、行尾注释一律留着不猜归属)逐条写在
346
+ `mcp-config-edit.ts` 文件头第一节。
347
+
322
348
  三件这一层管、上面那层管不了的事:
323
349
 
324
350
  1. **指纹防覆盖**。这份文件另一个编辑器一直开着(用户的 vim、还有 `writeMcpServer`