@epoch-agent/runtime 0.1.0 → 0.3.1

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.
Files changed (4) hide show
  1. package/README.md +152 -78
  2. package/dist/index.d.ts +2143 -78
  3. package/dist/index.js +1754 -1240
  4. package/package.json +9 -9
package/README.md CHANGED
@@ -4,9 +4,14 @@
4
4
  可跑的 agent。**嵌入方要装的就是这个包**,不是 core。
5
5
 
6
6
  - ✅ **做**:`buildRuntime()` 装配、`AgentSession`(审批桥 / 历史累积 / 持久化)、
7
- 会话回放、MCP 运维门面、telemetry 接线
7
+ 会话回放、MCP 运维门面、**插件的宿主控制面**(宿主预置市场 → 列 / 搜 / 装 / 卸)、
8
+ telemetry 接线、**进程级事件观测口**(`EpochRuntime.events`:所有会话产出的每一帧
9
+ `AgentEvent`,判据在 [events.ts](./src/events.ts) 文件头)
8
10
  - ❌ **不做**:领域逻辑(在 [core](../core))、命令解析(在 [cli](../cli))、
9
- 渲染(在 [tui](../tui) / [web](../web)
11
+ 渲染(在 [tui](../tui) / [web](../web))、**插件的热加载**(装完要重建 runtime,
12
+ 判据在 [plugin-control.ts](./src/plugin-control.ts) 文件头第二节)、
13
+ **事件的缓冲 / 补帧 / 跨进程**(观测口只在本进程内、不排队不发号;
14
+ wire 那一层在 [server](../server) 的 hub)
10
15
  - **依赖**:[protocol](../protocol) + [infra](../infra) + [core](../core) +
11
16
  [plugin-file](../plugins/plugin-file) / [plugin-terminal](../plugins/plugin-terminal) /
12
17
  [plugin-web](../plugins/plugin-web) / [plugin-mcp](../plugins/plugin-mcp) /
@@ -27,62 +32,76 @@ for await (const event of runtime.session.run('把 README 里过时的命令改
27
32
 
28
33
  ## 导出
29
34
 
30
- | 导出 | 用途 |
31
- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
32
- | `buildRuntime()` / `EpochRuntime` | 装配入口 |
33
- | `ensureSecretsReady()` | 不建整个 runtime 时单独把凭据存储备好 |
34
- | `AgentSession` | 宿主友好的会话:审批转事件 + 多轮历史 + 持久化 |
35
- | `EpochRuntime.sessionFactory` / `SessionFactory` / `LiveSession` | 一个进程里再开一个会话(方案 30 §6.3)。作用域那张表见下 |
36
- | `SessionStore` / `StoredMessage` / `TurnDetail` | 回放旧会话(刷新页面 / 会话详情),给的是完整 `EpochMessage` |
37
- | `EpochRuntime.checkpoints` / `RewindScope` | 列出检查点、预览回退、回退(文件 / 对话 / 两者)。快照本身由引擎做。**这一份是引导会话的**,别的会话问 `LiveSession.checkpoints` |
38
- | `buildServices()` / `Services` | 只要服务层、不要 AgentLoop 时用 |
39
- | `wireCommands()` / `CommandControl` / `withToolScope()` | 自定义斜杠命令:命令表 + 展开 + 按轮次收窄工具表 |
40
- | `withModelScope()` / `ModelTurnScope` | 命令 frontmatter 的 `model:` 只在这一轮生效,收尾无条件还原 |
41
- | `withRoleScope()` / `RoleControl` / `LiveSession.roles` | **这一条消息**用哪个专家(方案 57)。名字认不认得问 `has()`,换人问 `enterTurn()`,收尾无条件还原 |
42
- | `ApprovalRelay` / `serializeStream()` / `toSerializable()` | 审批 UI 在另一个进程时,把事件里过不去 IPC 的闭包换成 `requestId` |
43
- | `registerLocalTools()` / `registerMcpTools()` / `McpServerBatch` | 单独装工具(自定义宿主)。⚠️ `registerMcpTools` 的第四个参数收的是**批次数组**(宿主注入的 / 插件带的),再来一批就加一个元素,**别加第五个参数** |
44
- | `listMcpServers()` / `mcpLogin()` / `mcpLogout()` / `probeMcpServer()` | `epoch mcp` 的后端,不带 UI |
45
- | `EpochRuntime.mcp` / `McpControl` | MCP 的**写口**,照 `WorkspaceControl` 收窄过:`reconnect()`(重连一台)+ `add()`(加一台并落盘)+ `readConfig()` / `writeConfig()` / `applyConfig()`(那份文件的原文读写与生效,2026-08-18)。⚠️ **`McpRegistry` 刻意不交出去**,加能力是在这上面加方法,见下 |
46
- | `writeMcpServer()` / `McpAddInput` | 往用户手写的 `~/.epoch/mcp.json` 里**插一段**(注释 / 键序 / 缩进一个字节不动)。不带连接,也不带 UI |
47
- | `readMcpConfigFile()` / `writeMcpConfigFile()` / `mcpConfigRevision()` | 那份文件的**整份**读写(2026-08-18):原文进、原文出,带一个内容指纹防覆盖。⚠️ 和上一条不是一条路 —— 那条写的是**我们生成的**内容,这条写的是**用户敲的**字节,见下 |
48
- | `applyMcpConfig()` / `McpApplyOutcome` | 把盘上那份**搬进这个进程**(2026-08-18):该断的断、该连的连、没变的一个字节不动。⚠️ 只动 `source === 'user'` 那批,见下 |
49
- | `EpochRuntime.workspace` / `wireWorkspace()` / `splitDirList()` | **引导会话**的地盘(主根 + `--add-dir`)。要判路径在不在范围内问它 |
50
- | `EpochRuntime.workspaces` / `WorkspaceControl` / `SessionWorkspaces` | 每会话绑一个工作区(决定 18):绑 / / 解绑 + 「已知工作区」清单。`bind()` 收一个可选的 `lang`(方案 58 PR-2)—— 只影响失败那句 `detail`,`reason` 是契约 |
51
- | `EpochRuntime.skillImport` / `SkillControl` | 从**本机目录**导入技能到 `~/.epoch/skills/`(方案 42 §六):`preview()` 看一眼、`import(source, token)` 真写。两者都收一个可选的 `lang`(方案 58 PR-2)。⚠️ **收路径不收字节**,见下 |
52
- | `EpochRuntime.roleWrite` / `RoleWriteControl` | **建一个身份**,落在 `~/.epoch/agents/<name>.md`(2026-08-18)。只有 `add()` —— 没有删也没有改。⚠️ 建完**当场重载这个进程的角色表**;⚠️ 那道「回环绑定才给写」的闸门**不在这一层**,见下 |
53
- | `EpochRuntime.trusted` | 这次装配**实际**按不按信任在跑。别在宿主侧重查一遍信任表,见下 |
54
- | `EpochRuntime.trust` | 信任记录的读**与写**:`check` / `record` / `list` / `revoke`,见下 |
55
- | `EpochRuntime.homeDir` / `.artifactsRoot` | 这次生效的数据目录 + artifact 根目录。要路径读它,别自己 `join` |
56
- | `buildRuntime({ hostPreset })` / `HostPreset` | 装配前替宿主把 `config.yaml` / `mcp.json` / `workspaces.json` 落到位。**缺失才写**,见下 |
57
- | `buildRuntime({ hostCapabilities })` / `HostCapabilities` / `HostAgentRole` | 宿主注入自己 ship 的**专家 / 技能 / MCP server**。前缀由装配层按 `namespace` 拼(⚠️ server 那一样是 `__` 不是 `:`),**不落盘**、不进 `epoch plugin list`,见下 |
58
- | `EpochRuntime.schedules` / `ScheduleControl` | 定时任务的宿主出口:增删改查 + `fire()` + `capability()`。**没给 `scheduleRunner` 就拒绝注册进 OS**,见下 |
59
- | `fireSchedule()` / `FireResult` | 执行器本体。`epoch schedule fire` 和宿主的 `headless.js` 跑的是**同一段**,见 [AUTOMATION.md](../../docs/AUTOMATION.md) |
60
- | `AgentRoleError` | `--agent` 给了不认识的角色名。按 `err.name` 认,不用 `instanceof` |
61
- | `listBackgroundTasks(sessionId)` / `backgroundTaskOutput(sessionId, id, since?)` | 后台任务表(从 plugin-terminal 借道,CLI / TUI / server 都不许直接 import 它)。⚠️ **第一个参数是会话 id**,见下 |
62
- | `EpochRuntime.diagnosticList` / `.diagnosticsIn(lang)` | 启动诊断。前者是**进程语言**那个快照(CLI / `epoch doctor` 走它),后者按指定语言重渲染 `detail`(方案 58)—— **同一个 sink 的两个投影**,`module` / `code` / `status` 逐项相同 |
63
- | `SESSION_SEARCH_MODULE` | 会话检索那两条启动诊断的 `module`。**是契约不是文案**,所以是常量不是 `t()`(方案 58 §1.5 1 条修的就是它) |
35
+ | 导出 | 用途 |
36
+ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37
+ | `buildRuntime()` / `EpochRuntime` | 装配入口 |
38
+ | `ensureSecretsReady()` | 不建整个 runtime 时单独把凭据存储备好 |
39
+ | `AgentSession` | 宿主友好的会话:审批转事件 + 多轮历史 + 持久化 |
40
+ | `EpochRuntime.sessionFactory` / `SessionFactory` / `LiveSession` | 一个进程里再开一个会话(方案 30 §6.3)。作用域那张表见下 |
41
+ | `SessionStore` / `StoredMessage` / `TurnDetail` | 回放旧会话(刷新页面 / 会话详情),给的是完整 `EpochMessage` |
42
+ | `EpochRuntime.checkpoints` / `RewindScope` | 列出检查点、预览回退、回退(文件 / 对话 / 两者)。快照本身由引擎做。**这一份是引导会话的**,别的会话问 `LiveSession.checkpoints` |
43
+ | `buildServices()` / `Services` | 只要服务层、不要 AgentLoop 时用 |
44
+ | `wireCommands()` / `CommandControl` / `withToolScope()` | 自定义斜杠命令:命令表 + 展开 + 按轮次收窄工具表 |
45
+ | `withModelScope()` / `ModelTurnScope` | 命令 frontmatter 的 `model:` 只在这一轮生效,收尾无条件还原 |
46
+ | `withRoleScope()` / `RoleControl` / `LiveSession.roles` | **这一条消息**用哪个专家(方案 57)。名字认不认得问 `has()`,换人问 `enterTurn()`,收尾无条件还原 |
47
+ | `EpochRuntime.utility` / `UtilityControl` / `utilityBudgetKey()` | 一次性补全(方案 59 需求 B):不建会话、不落历史、不装 system prompt、**入参上压根没有 `tools`**。不给 `model` 就跟随 `models.utility`。⚠️ 它挂的是**引导会话**那一份 router;⚠️ 给了 `sessionId` 那笔钱**会拦住那个会话的下一轮**,不给则落 `utility:<label>` 保留桶而**那个桶今天没有上界** —— 两条见下 |
48
+ | `EpochRuntime.events` / `AgentEventBus` / `AgentEventSubscriber` | **进程里所有会话**产出的每一帧 `AgentEvent`(方案 61):`subscribe(fn)` `(event, sessionId)`,回一个幂等的退订。投递发生在 `run()` 的 `yield` **之前**,零订阅者时是一次 `size === 0` 的检查。⚠️ **进程级不是 runtime 级**(两份 runtime 同一个总线);⚠️ 回调同步执行、不许干重活;⚠️ 审批 / 提问帧上的 `respond` **不许调**,见 [EMBEDDING §13.5](../../docs/EMBEDDING.md) |
49
+ | `ApprovalRelay` / `serializeStream()` / `toSerializable()` | 审批 UI 在另一个进程时,把事件里过不去 IPC 的闭包换成 `requestId` |
50
+ | `registerLocalTools()` / `registerMcpTools()` / `McpServerBatch` | 单独装工具(自定义宿主)。⚠️ `registerMcpTools` 的第四个参数收的是**批次数组**(宿主注入的 / 插件带的),再来一批就加一个元素,**别加第五个参数** |
51
+ | `listMcpServers()` / `mcpLogin()` / `mcpLogout()` / `probeMcpServer()` | `epoch mcp` 的后端,不带 UI |
52
+ | `EpochRuntime.mcp` / `McpControl` | MCP 的**写口**,照 `WorkspaceControl` 收窄过:`reconnect()`(重连一台)+ `add()`(加一台并落盘)+ `update()` / `remove()`(改一台 / 删一台,就地改写那份文件,2026-09-02)+ `readConfig()` / `writeConfig()` / `applyConfig()`(那份文件的原文读写与生效,2026-08-18)。⚠️ **`McpRegistry` 刻意不交出去**,加能力是在这上面加方法,见下 |
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) |
56
+ | `readMcpConfigFile()` / `writeMcpConfigFile()` / `mcpConfigRevision()` | 那份文件的**整份**读写(2026-08-18):原文进、原文出,带一个内容指纹防覆盖。⚠️ 和上一条不是一条路 —— 那条写的是**我们生成的**内容,这条写的是**用户敲的**字节,见下 |
57
+ | `applyMcpConfig()` / `McpApplyOutcome` | 把盘上那份**搬进这个进程**(2026-08-18):该断的断、该连的连、没变的一个字节不动。⚠️ 只动 `source === 'user'` 那批,见下 |
58
+ | `EpochRuntime.workspace` / `wireWorkspace()` / `splitDirList()` | **引导会话**的地盘(主根 + `--add-dir`)。要判路径在不在范围内问它 |
59
+ | `EpochRuntime.workspaces` / `WorkspaceControl` / `SessionWorkspaces` | 每会话绑一个工作区(决定 18):绑 / 查 / 解绑 + 「已知工作区」清单。`bind()` 收一个可选的 `lang`(方案 58 PR-2)—— 只影响失败那句 `detail`,`reason` 是契约 |
60
+ | `EpochRuntime.skillWrite` / `SkillControl` | 技能那一栏的**写口**,落点 `~/.epoch/skills/`:`preview()` 看一眼、`import(source, token)` 真写(方案 42 §六)、`remove(name)` 删掉一份(2026-09-01,「能加就得能删」)。三个都收一个可选的 `lang`(方案 58 PR-2)。⚠️ 导入**收路径不收字节**,见下;⚠️ `remove` 只删得动用户级,且回执带**刚被销毁的那个目录** |
61
+ | `EpochRuntime.roleWrite` / `RoleWriteControl` | **建一个身份**,落在 `~/.epoch/agents/<name>.md`(2026-08-18)。只有 `add()` —— 没有删也没有改。⚠️ 建完**当场重载这个进程的角色表**;⚠️ 那道「回环绑定才给写」的闸门**不在这一层**,见下 |
62
+ | `buildRuntime({ hostMarketplaces })` / `EpochRuntime.plugins` / `PluginControl` | 宿主预置一份插件市场,用户从那张单子里挑一个装(方案 59 E1):`list` / `search` / `preview`→`install(token)` / `previewUpdate`→`update(token)` / `uninstall` + 一格 `pendingRestart`。**没给 `hostMarketplaces` 就是 `null`**。⚠️ 只收 `<市场>/<插件>` 收不了路径;⚠️ 远程 source 默认关;⚠️ **装完要重建 runtime**,见下 |
63
+ | `EpochRuntime.trusted` | 这次装配**实际**按不按信任在跑。别在宿主侧重查一遍信任表,见下 |
64
+ | `EpochRuntime.trust` | 信任记录的读**与写**:`check` / `record` / `list` / `revoke`,见下 |
65
+ | `EpochRuntime.homeDir` / `.artifactsRoot` | 这次生效的数据目录 + artifact 根目录。要路径读它,别自己 `join` |
66
+ | `buildRuntime({ hostPreset })` / `HostPreset` | 装配前替宿主把 `config.yaml` / `mcp.json` / `workspaces.json` 落到位。**缺失才写**,见下 |
67
+ | `buildRuntime({ hostCapabilities })` / `HostCapabilities` / `HostAgentRole` | 宿主注入自己 ship 的**专家 / 技能 / MCP server / 进程内工具**。前缀由装配层按 `namespace` 拼(⚠️ 后两样是 `__` 不是 `:`),**不落盘**、不进 `epoch plugin list`,见下 |
68
+ | `EpochRuntime.schedules` / `ScheduleControl` | 定时任务的宿主出口:增删改查 + `fire()` + `capability()`。**没给 `scheduleRunner` 就拒绝注册进 OS**,见下 |
69
+ | `fireSchedule()` / `FireResult` | 执行器本体。`epoch schedule fire` 和宿主的 `headless.js` 跑的是**同一段**,见 [AUTOMATION.md](../../docs/AUTOMATION.md) |
70
+ | `AgentRoleError` | `--agent` 给了不认识的角色名。按 `err.name` 认,不用 `instanceof` |
71
+ | `listBackgroundTasks(sessionId)` / `backgroundTaskOutput(sessionId, id, since?)` | 后台任务表(从 plugin-terminal 借道,CLI / TUI / server 都不许直接 import 它)。⚠️ **第一个参数是会话 id**,见下 |
72
+ | `EpochRuntime.diagnosticList` / `.diagnosticsIn(lang)` | 启动诊断。前者是**进程语言**那个快照(CLI / `epoch doctor` 走它),后者按指定语言重渲染 `detail`(方案 58)—— **同一个 sink 的两个投影**,`module` / `code` / `status` 逐项相同 |
73
+ | `SESSION_SEARCH_MODULE` / `WORKSPACE_MODULE` / `HOST_*_MODULE` | 几族启动诊断的 `module` 值。**是契约不是文案**,所以是常量不是 `t()`(方案 58 §1.5 第 1 条修的就是它)。`module` **一律英文 PascalCase**,2026-08-23 改过一批名,迁移表在 [EMBEDDING.md §1](../../docs/EMBEDDING.md) |
64
74
 
65
75
  ## 文件
66
76
 
67
- | 文件 | 职责 |
68
- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
69
- | `build.ts` | `buildRuntime()`。显式手写装配,不用 DI 容器 |
70
- | `host-preset.ts` | 宿主预置(方案 54 §六):装配前落文件。四条语义写在它的文件头,别在别处再判一次 |
71
- | `host-capabilities.ts` | 宿主注入的能力清单(方案 44 §2.1):校验 + 拼前缀。**和上一行不是一件事** —— 那个写用户的文件,这个一个字节都不落盘 |
72
- | `plugin-mcp-servers.ts` | 已装插件根下那份 `mcp.json`(方案 44 PR-3):读路径 → 拼 `<插件名>__` 前缀。core 只交路径(它不许 import `plugin-mcp`),解析在这儿 |
73
- | `session-factory.ts` | 多会话工厂:**全仓唯一一处** `new AgentLoop` + `new AgentSession`,外加审批/提问的路由 |
74
- | `services.ts` | provider / session / memory / skill / hook / 权限 / 上下文 / 沙箱;设置分层的合并点也在这儿 |
75
- | `tools.ts` | **编译期**插件(`EpochPlugin`)+ 内置工具 + MCP 全部进同一个 `ToolRegistry` |
76
- | `commands.ts` | 自定义斜杠命令的装配:加载命令表、包一层可按轮次收窄的工具 provider |
77
- | `workspace.ts` | `--add-dir` 落地(目录体检 告知权限层 → 进启动诊断)+ 每会话的工作区绑定与信任判定 |
78
- | `agent-session.ts` | 审批回调 流事件、历史累积、逐轮持久化、**划检查点的「一轮」** |
79
- | `session-store.ts` | `SessionManager` 的薄封装,落盘的是完整形状 |
80
- | `serialize.ts` | IPC 的事件序列化 + `ApprovalRelay` |
81
- | `mcp-admin.ts` | MCP 运维门面(`cli` 不许直接依赖 `plugin-mcp`,所以从这儿转) |
82
- | `mcp-config-write.ts` | 往用户手写的 `~/.epoch/mcp.json` 里**插一段**(`POST /api/mcp` 走它):注释 / 键序 / 缩进一个字节不动。判据同 `infra/src/config-yaml.ts` |
83
- | `mcp-config-file.ts` | 那份文件的**整份**读写(2026-08-18):原文进、原文出 + 内容指纹 + 换行符跟着盘上那份走。⚠️ 和上一条**不冲突**,主语不同,见下 |
84
- | `mcp-config-apply.ts` | 把盘上那份搬进这个进程:四档动作(新增 / 重连 / 没动 / 断开)+ 「只动用户那批」 |
85
- | `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 |
86
105
 
87
106
  ## 设置分层在这儿落地
88
107
 
@@ -220,6 +239,13 @@ rt.sessionFactory.create({ workspace, reuse: false, role: 'explore' });
220
239
  - **一个会话一份**:`AgentSession` + 它的审批桥、`AgentLoop`(`workDir` 与
221
240
  `projectContext` 跟着绑定的工作区走)、`CheckpointManager`、`SessionStore`
222
241
 
242
+ > ⚠️ 「跟着绑定的工作区走」是**每轮现读**,不是装配那一刻的快照(迟绑,
243
+ > 2026-08-20)。`assemble()` 递给 `AgentLoop` 的是 `AgentConfig.site` ——
244
+ > 一个问绑定表的函数。快照那一版的病是:侧栏「新建会话」开出来的会话
245
+ > (`unbound`,方案 55 PR-1)在底栏 picker 里挑完目录之后,界面上那一格换了、
246
+ > 引擎照旧在服务进程的启动目录里干活。判据全文在 `session-factory.ts` 的
247
+ > `assemble` 和 core 的 `AgentSiteRef` 上。
248
+
223
249
  > ⚠️ **「一个会话一份」的东西,取数也得按会话取。** `LiveSession.checkpoints`
224
250
  > (2026-08-15 加)就是这条:`EpochRuntime.checkpoints` 是**引导会话**那一份,
225
251
  > 拿它去答别的会话的表现是列表恒空、回退恒被拒 —— 而这个进程正握着对方的
@@ -235,6 +261,13 @@ rt.sessionFactory.create({ workspace, reuse: false, role: 'explore' });
235
261
  | 权限档(`level` / `setLevel`) | `rt.permissions` | `sessionFactory.get(id).permissions` |
236
262
  | plan 模式(`PlanControl`) | `rt.plan` | `sessionFactory.get(id).plan` |
237
263
 
264
+ ⚠️ **`rt.utility`(一次性补全)刻意不在上面那张表里**:它挂的就是引导会话那一份
265
+ router,**没有「别的会话那一份」** —— 它答的是「这个进程默认用什么」,而不是
266
+ 「窗口 2 现在在用什么」。于是第二个会话换过模型之后,`utility` 跟的仍然是引导
267
+ 会话那个选择。这条代价和 `getUtilityInfo()`(「没配 utility 就跟着主模型走」在
268
+ 多会话下确切的意思是「跟着**引导**会话走」)是同一条,判据在
269
+ [src/utility.ts](src/utility.ts) 的文件头。要打别的就显式给 `model`。
270
+
238
271
  **引导会话那三份就是进程那三份本体,不是副本** —— 所以 TUI 的 `/model` /
239
272
  `/permission`、CLI 的 `--model` 行为一个字节没变,而「两份要同步」这件事在这个
240
273
  形状下说不出口。三样各自的落法和判据在
@@ -257,12 +290,17 @@ rt.sessionFactory.create({ workspace, reuse: false, role: 'explore' });
257
290
 
258
291
  ## 导入技能:**收路径,不收字节**(方案 42 §六)
259
292
 
260
- `EpochRuntime.skillImport` 转出去的是**两个函数**,不是 `SkillSystem` ——
293
+ `EpochRuntime.skillWrite` 转出去的是**三个函数**,不是 `SkillSystem` ——
261
294
  后者上有六个写方法、一个会 `matchCount++` 的 `view()`、还有一个会**自动删**
262
- 低效技能的 `maintain()`。交出去等于让网线那一侧够得着「删掉用户的技能」
263
- 和「污染那本喂给模型的评分」。收窄之后 `@epoch-agent/server` 那个包**写不出**
295
+ 低效技能的 `maintain()`。交出去等于让网线那一侧够得着「污染那本喂给模型的评分」
296
+ 和「一口气自动淘汰一批」。收窄之后 `@epoch-agent/server` 那个包**写不出**
264
297
  别的动作 —— 那不是靠 review 盯住的,是编译期的事(同 `McpControl` 只露 reconnect)。
265
298
 
299
+ ⚠️ **2026-09-01 这一格从 `skillImport` 改名成 `skillWrite`**,因为那天它多了
300
+ 一个 `remove`:一个叫 `skillImport` 的字段上挂着「递归删目录」是这一层最不该有的
301
+ 假话。而多出来的那一个是**逐条点名删**(`remove(name)`),和上面说的 `maintain()`
302
+ 那种「自动淘汰一批」仍然是两件事 —— 后者照旧不出这个包。
303
+
266
304
  ⚠️ **两个动作都只收一条服务端本机路径。** 判据全文在 core 的
267
305
  `skill/import.ts` 文件头,一句话:用户级技能目录**连信任闸门都没有**,
268
306
  往它里面落一份文件 = 让那段文字无条件进往后每一轮的 system prompt;
@@ -274,14 +312,14 @@ rt.sessionFactory.create({ workspace, reuse: false, role: 'explore' });
274
312
  嵌入宿主要注意:这条路的落点**永远是 `SkillSystem` 自己那个用户级目录**,
275
313
  调用方给不了。让宿主传一个目录进来,它就成了「往任意目录写文件」的口子。
276
314
 
277
- ## 改 `mcp.json`:**两条路,主语不同**(2026-08-18
315
+ ## 改 `mcp.json`:**两条路,主语不同**(2026-08-18;2026-09-02 加了两个动作)
278
316
 
279
317
  这一层有两条写 `~/.epoch/mcp.json` 的路,而它们**不许互相取代**:
280
318
 
281
- | 路 | 写下去的是 | 做法 |
282
- | ------------------------------------------- | ------------------ | ------------------------ |
283
- | `writeMcpServer()`(`mcp-config-write`) | **我们生成的**一段 | 只插入,一个已有字节不碰 |
284
- | `writeMcpConfigFile()`(`mcp-config-file`) | **用户敲的**字节 | 整份覆盖 |
319
+ | 路 | 写下去的是 | 做法 |
320
+ | -------------------------------------------------------------- | ------------------ | ---------------------- |
321
+ | `writeMcpServer()` / `updateMcpServer()` / `removeMcpServer()` | **我们生成的**一段 | 就地改,只碰点名那一台 |
322
+ | `writeMcpConfigFile()`(`mcp-config-file`) | **用户敲的**字节 | 整份覆盖 |
285
323
 
286
324
  `mcp-config-write.ts` 的文件头逐字判掉了「读进来 → 改 → 整份写回」:我们读进来的
287
325
  不是原文(`parseMcpConfig` 吐的是归一化之后的结构),整份写回会把 `connect_timeout`
@@ -292,6 +330,21 @@ rt.sessionFactory.create({ workspace, reuse: false, role: 'explore' });
292
330
  所以注释 / 空行 / 键序 / snake_case 键名 / 未知键全都原样活着。⚠️ 别把它读成
293
331
  「那条判据过期了」,也别拿它去简化第一行。
294
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
+
295
348
  三件这一层管、上面那层管不了的事:
296
349
 
297
350
  1. **指纹防覆盖**。这份文件另一个编辑器一直开着(用户的 vim、还有 `writeMcpServer`
@@ -416,11 +469,11 @@ PTY 会话 —— 最后这项要先查整棵进程树(`pgrep` / `taskkill`)
416
469
 
417
470
  它**不**预置信任 —— 那是一个决定不是一份配置,走 `rt.trust`(上一段)。
418
471
 
419
- **要把你自己 ship 的专家 / 技能 / MCP server 塞进能力清单,用 `hostCapabilities`,
420
- 别去写用户的 `~/.epoch/agents/` 或 `mcp.json`**(方案 44 §2.1)。它和上面那个
421
- `hostPreset` **不是一件事**:那一个装配前写用户的文件(缺失才写、用户改得动),
422
- 这一个是这一程的运行期事实,**一个字节都不落盘**,用户在自己的目录里找不到它,
423
- 也没有地方能删掉它。
472
+ **要把你自己 ship 的专家 / 技能 / MCP server / 进程内工具塞进能力清单,用
473
+ `hostCapabilities`,别去写用户的 `~/.epoch/agents/` 或 `mcp.json`**
474
+ (方案 44 §2.1、方案 59 §四)。它和上面那个 `hostPreset` **不是一件事**:
475
+ 那一个装配前写用户的文件(缺失才写、用户改得动),这一个是这一程的运行期事实,
476
+ **一个字节都不落盘**,用户在自己的目录里找不到它,也没有地方能删掉它。
424
477
 
425
478
  ```ts
426
479
  buildRuntime({
@@ -429,37 +482,58 @@ buildRuntime({
429
482
  roles: [{ name: 'triage', description: '分拣工单,只读', tools: ['file_read'] }],
430
483
  skillDirs: [join(app.getAppPath(), 'resources', 'skills')], // 必须绝对路径
431
484
  mcpServers: [{ name: 'deploy', transport: 'http', url: 'http://127.0.0.1:3456/mcp' }],
485
+ tools: [listJobsTool], // `EpochTool` 本体,name: 'list_jobs'
432
486
  },
433
487
  });
434
488
  // → 专家 `acme:triage`、技能 `acme:<分类>/<名字>`、server `acme__deploy`
435
- // (工具是 `mcp__acme__deploy__<工具>`),能力页上各自单独一组,标着「宿主」
489
+ // (工具是 `mcp__acme__deploy__<工具>`)、进程内工具 `acme__list_jobs`,
490
+ // 四样都标着「宿主」来源
436
491
  ```
437
492
 
438
- 五条性质(全文在 [host-capabilities.ts](src/host-capabilities.ts) 的文件头):
493
+ 六条性质(全文在 [host-capabilities.ts](src/host-capabilities.ts) 的文件头):
439
494
 
440
495
  - **命名空间是强制的**:前缀由装配层按 `namespace` 拼,宿主在 `name` 里自己写前缀
441
496
  会被判成非法名字。不这么做的话,一个叫 `general` 的注入会顶掉内置那条,
442
497
  而 `delegate_task` 不带 `role` 时取的就是它
443
- - ⚠️ **三样的分隔符不一样**:角色 / 技能是 `<namespace>:`,MCP server
444
- `<namespace>__` —— server 名会原样变成工具名的一部分,而 provider 硬拒带 `:`
445
- 的工具名(实测)。判据在 plugin-mcp 的 `McpServerStatus.source`
498
+ - ⚠️ **分隔符分两种**:角色 / 技能是 `<namespace>:`,MCP server 和工具是
499
+ `<namespace>__` —— 后两样都会原样进工具名,而 provider 硬拒带 `:` 的工具名
500
+ (实测)。判据在 plugin-mcp 的 `McpServerStatus.source` 上。
501
+ ⚠️ 字符集也分两种:**工具名允许 `_`**(`list_jobs` 是对的),server 名不允许 ——
502
+ 前者是名字的末段,后者夹在两个 `__` 中间
446
503
  - **`source` 宿主写不出来**:`HostAgentRole` 上没有那个字段,`McpServerConfig`
447
- 上也没有,装配层填死 `'host'`
504
+ 和 `EpochTool` 上也没有,装配层填死 `'host'`
448
505
  - **坏的那一条只跳过它自己**,各留一条诊断(`module === 'HostCapabilities'`)。
449
506
  唯一整份作废的是 `namespace` 本身坏掉 —— 那时没有前缀
450
507
  - **注入是装配时一次性的**,但注入进去的东西生命周期照旧:技能照样被 `SkillLearner`
451
- 影响(只是那个目录只读),server 照样掉线重连、照样能在能力页上按「重连」
508
+ 影响(只是那个目录只读),server 照样掉线重连、照样能在能力页上按「重连」。
509
+ ⚠️ 工具那一格上「一次性」最硬:注册表没有「换掉某一个工具」的路
510
+ - ⚠️ **注入的工具照样过审批闸门**,而这不是我们答应的一件事:闸门在
511
+ `ToolExecutor` 上,它不看来源。免不免确认由 `annotations` / `operation` 决定,
512
+ 和内置 / 插件 / MCP 工具走同一套。连带一件白拿的:`execute(args, ctx)` 的
513
+ `ctx.sessionId` 是**真正在跑的那个会话**(绕一圈 MCP 的话这个值到不了宿主手上)
452
514
 
453
515
  注入的 server **装配时就真连**,并和用户 `mcp.json` 那批**共用同一份**
454
- `MCP_CONNECT_BUDGET_MS`(不是两份相加)。真连是因为 `## 可用工具` 是按第一轮的
455
- 工具表拼进 system prompt 的;共用一份是因为那个预算存在的全部理由就是
456
- 「启动不能被一台连不上的 server 拖住」。和用户那批重名时**用户赢**。
516
+ `MCP_CONNECT_BUDGET_MS`(不是两份相加)。真连是因为**第一轮发给 provider 的那份
517
+ 工具 schema 数组是按那一刻的注册表拼的**(2026-08-26 之前这句话的凭据是
518
+ `## 可用工具` 那段散文清单,那一段删了,结论没变);共用一份是因为那个预算存在的
519
+ 全部理由就是「启动不能被一台连不上的 server 拖住」。和用户那批重名时**用户赢**。
457
520
 
458
521
  **要 artifact 的路径就读 `rt.artifactsRoot`。** 它和 `rt.homeDir` 是这次装配的事实,
459
522
  不是配置里的一个字段。2026-08-15 之前 artifact 的**写**路径不认 `homeDir`
460
523
  (读那侧一直认),后果是宿主传了 `homeDir` 就产物 404 而全链路 200 ——
461
524
  在宿主侧 `join(homeDir, 'artifacts')` 拼一遍,就是给同一个目录第二个说法。
462
525
 
526
+ **`rt.sessions.delete()` 现在连带删产物**(2026-08-20,方案 47 PR-4)。在这之前它
527
+ 只删检查点,而 core 的 `SessionManager.delete()` 上写着「产物由装配层跟着一起清」
528
+ —— 一句从头到尾没兑现过的话。宿主这边要知道两件:
529
+
530
+ - `SessionDeletion` 多了一格 `artifactsRemoved`,和 `checkpointsRemoved` 一样
531
+ **与 `deleted` 分开报**:附属物删不掉不会让整次删除变成失败(对话已经没了,
532
+ 回一个错只会让用户再点一次然后收到 404)。**别把三格 and 起来当成功判据** ——
533
+ 该说的是「删了,但那个目录还在」,而用户下一步要知道是哪个目录
534
+ - 那一格**还没进 `WireDeleteSessionResponse`**(`epoch web` 那条 REST 路只透传,
535
+ 所以它会随 JSON 出门,但契约上没有它)。走 REST 的宿主要读它,得先加那一格
536
+
463
537
  ## 开发
464
538
 
465
539
  ```bash