@epoch-agent/runtime 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +118 -70
  2. package/dist/index.d.ts +1532 -49
  3. package/dist/index.js +996 -1180
  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,70 @@ 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()`(加一台并落盘)+ `readConfig()` / `writeConfig()` / `applyConfig()`(那份文件的原文读写与生效,2026-08-18)。⚠️ **`McpRegistry` 刻意不交出去**,加能力是在这上面加方法,见下 |
53
+ | `writeMcpServer()` / `McpAddInput` | 往用户手写的 `~/.epoch/mcp.json` 里**插一段**(注释 / 键序 / 缩进一个字节不动)。不带连接,也不带 UI |
54
+ | `readMcpConfigFile()` / `writeMcpConfigFile()` / `mcpConfigRevision()` | 那份文件的**整份**读写(2026-08-18):原文进、原文出,带一个内容指纹防覆盖。⚠️ 和上一条不是一条路 —— 那条写的是**我们生成的**内容,这条写的是**用户敲的**字节,见下 |
55
+ | `applyMcpConfig()` / `McpApplyOutcome` | 把盘上那份**搬进这个进程**(2026-08-18):该断的断、该连的连、没变的一个字节不动。⚠️ 只动 `source === 'user'` 那批,见下 |
56
+ | `EpochRuntime.workspace` / `wireWorkspace()` / `splitDirList()` | **引导会话**的地盘(主根 + `--add-dir`)。要判路径在不在范围内问它 |
57
+ | `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)。⚠️ **收路径不收字节**,见下 |
59
+ | `EpochRuntime.roleWrite` / `RoleWriteControl` | **建一个身份**,落在 `~/.epoch/agents/<name>.md`(2026-08-18)。只有 `add()` —— 没有删也没有改。⚠️ 建完**当场重载这个进程的角色表**;⚠️ 那道「回环绑定才给写」的闸门**不在这一层**,见下 |
60
+ | `buildRuntime({ hostMarketplaces })` / `EpochRuntime.plugins` / `PluginControl` | 宿主预置一份插件市场,用户从那张单子里挑一个装(方案 59 E1):`list` / `search` / `preview`→`install(token)` / `previewUpdate`→`update(token)` / `uninstall` + 一格 `pendingRestart`。**没给 `hostMarketplaces` 就是 `null`**。⚠️ 只收 `<市场>/<插件>` 收不了路径;⚠️ 远程 source 默认关;⚠️ **装完要重建 runtime**,见下 |
61
+ | `EpochRuntime.trusted` | 这次装配**实际**按不按信任在跑。别在宿主侧重查一遍信任表,见下 |
62
+ | `EpochRuntime.trust` | 信任记录的读**与写**:`check` / `record` / `list` / `revoke`,见下 |
63
+ | `EpochRuntime.homeDir` / `.artifactsRoot` | 这次生效的数据目录 + artifact 根目录。要路径读它,别自己 `join` |
64
+ | `buildRuntime({ hostPreset })` / `HostPreset` | 装配前替宿主把 `config.yaml` / `mcp.json` / `workspaces.json` 落到位。**缺失才写**,见下 |
65
+ | `buildRuntime({ hostCapabilities })` / `HostCapabilities` / `HostAgentRole` | 宿主注入自己 ship 的**专家 / 技能 / MCP server / 进程内工具**。前缀由装配层按 `namespace` 拼(⚠️ 后两样是 `__` 不是 `:`),**不落盘**、不进 `epoch plugin list`,见下 |
66
+ | `EpochRuntime.schedules` / `ScheduleControl` | 定时任务的宿主出口:增删改查 + `fire()` + `capability()`。**没给 `scheduleRunner` 就拒绝注册进 OS**,见下 |
67
+ | `fireSchedule()` / `FireResult` | 执行器本体。`epoch schedule fire` 和宿主的 `headless.js` 跑的是**同一段**,见 [AUTOMATION.md](../../docs/AUTOMATION.md) |
68
+ | `AgentRoleError` | `--agent` 给了不认识的角色名。按 `err.name` 认,不用 `instanceof` |
69
+ | `listBackgroundTasks(sessionId)` / `backgroundTaskOutput(sessionId, id, since?)` | 后台任务表(从 plugin-terminal 借道,CLI / TUI / server 都不许直接 import 它)。⚠️ **第一个参数是会话 id**,见下 |
70
+ | `EpochRuntime.diagnosticList` / `.diagnosticsIn(lang)` | 启动诊断。前者是**进程语言**那个快照(CLI / `epoch doctor` 走它),后者按指定语言重渲染 `detail`(方案 58)—— **同一个 sink 的两个投影**,`module` / `code` / `status` 逐项相同 |
71
+ | `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
72
 
65
73
  ## 文件
66
74
 
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 |
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 |
86
99
 
87
100
  ## 设置分层在这儿落地
88
101
 
@@ -220,6 +233,13 @@ rt.sessionFactory.create({ workspace, reuse: false, role: 'explore' });
220
233
  - **一个会话一份**:`AgentSession` + 它的审批桥、`AgentLoop`(`workDir` 与
221
234
  `projectContext` 跟着绑定的工作区走)、`CheckpointManager`、`SessionStore`
222
235
 
236
+ > ⚠️ 「跟着绑定的工作区走」是**每轮现读**,不是装配那一刻的快照(迟绑,
237
+ > 2026-08-20)。`assemble()` 递给 `AgentLoop` 的是 `AgentConfig.site` ——
238
+ > 一个问绑定表的函数。快照那一版的病是:侧栏「新建会话」开出来的会话
239
+ > (`unbound`,方案 55 PR-1)在底栏 picker 里挑完目录之后,界面上那一格换了、
240
+ > 引擎照旧在服务进程的启动目录里干活。判据全文在 `session-factory.ts` 的
241
+ > `assemble` 和 core 的 `AgentSiteRef` 上。
242
+
223
243
  > ⚠️ **「一个会话一份」的东西,取数也得按会话取。** `LiveSession.checkpoints`
224
244
  > (2026-08-15 加)就是这条:`EpochRuntime.checkpoints` 是**引导会话**那一份,
225
245
  > 拿它去答别的会话的表现是列表恒空、回退恒被拒 —— 而这个进程正握着对方的
@@ -235,6 +255,13 @@ rt.sessionFactory.create({ workspace, reuse: false, role: 'explore' });
235
255
  | 权限档(`level` / `setLevel`) | `rt.permissions` | `sessionFactory.get(id).permissions` |
236
256
  | plan 模式(`PlanControl`) | `rt.plan` | `sessionFactory.get(id).plan` |
237
257
 
258
+ ⚠️ **`rt.utility`(一次性补全)刻意不在上面那张表里**:它挂的就是引导会话那一份
259
+ router,**没有「别的会话那一份」** —— 它答的是「这个进程默认用什么」,而不是
260
+ 「窗口 2 现在在用什么」。于是第二个会话换过模型之后,`utility` 跟的仍然是引导
261
+ 会话那个选择。这条代价和 `getUtilityInfo()`(「没配 utility 就跟着主模型走」在
262
+ 多会话下确切的意思是「跟着**引导**会话走」)是同一条,判据在
263
+ [src/utility.ts](src/utility.ts) 的文件头。要打别的就显式给 `model`。
264
+
238
265
  **引导会话那三份就是进程那三份本体,不是副本** —— 所以 TUI 的 `/model` /
239
266
  `/permission`、CLI 的 `--model` 行为一个字节没变,而「两份要同步」这件事在这个
240
267
  形状下说不出口。三样各自的落法和判据在
@@ -416,11 +443,11 @@ PTY 会话 —— 最后这项要先查整棵进程树(`pgrep` / `taskkill`)
416
443
 
417
444
  它**不**预置信任 —— 那是一个决定不是一份配置,走 `rt.trust`(上一段)。
418
445
 
419
- **要把你自己 ship 的专家 / 技能 / MCP server 塞进能力清单,用 `hostCapabilities`,
420
- 别去写用户的 `~/.epoch/agents/` 或 `mcp.json`**(方案 44 §2.1)。它和上面那个
421
- `hostPreset` **不是一件事**:那一个装配前写用户的文件(缺失才写、用户改得动),
422
- 这一个是这一程的运行期事实,**一个字节都不落盘**,用户在自己的目录里找不到它,
423
- 也没有地方能删掉它。
446
+ **要把你自己 ship 的专家 / 技能 / MCP server / 进程内工具塞进能力清单,用
447
+ `hostCapabilities`,别去写用户的 `~/.epoch/agents/` 或 `mcp.json`**
448
+ (方案 44 §2.1、方案 59 §四)。它和上面那个 `hostPreset` **不是一件事**:
449
+ 那一个装配前写用户的文件(缺失才写、用户改得动),这一个是这一程的运行期事实,
450
+ **一个字节都不落盘**,用户在自己的目录里找不到它,也没有地方能删掉它。
424
451
 
425
452
  ```ts
426
453
  buildRuntime({
@@ -429,37 +456,58 @@ buildRuntime({
429
456
  roles: [{ name: 'triage', description: '分拣工单,只读', tools: ['file_read'] }],
430
457
  skillDirs: [join(app.getAppPath(), 'resources', 'skills')], // 必须绝对路径
431
458
  mcpServers: [{ name: 'deploy', transport: 'http', url: 'http://127.0.0.1:3456/mcp' }],
459
+ tools: [listJobsTool], // `EpochTool` 本体,name: 'list_jobs'
432
460
  },
433
461
  });
434
462
  // → 专家 `acme:triage`、技能 `acme:<分类>/<名字>`、server `acme__deploy`
435
- // (工具是 `mcp__acme__deploy__<工具>`),能力页上各自单独一组,标着「宿主」
463
+ // (工具是 `mcp__acme__deploy__<工具>`)、进程内工具 `acme__list_jobs`,
464
+ // 四样都标着「宿主」来源
436
465
  ```
437
466
 
438
- 五条性质(全文在 [host-capabilities.ts](src/host-capabilities.ts) 的文件头):
467
+ 六条性质(全文在 [host-capabilities.ts](src/host-capabilities.ts) 的文件头):
439
468
 
440
469
  - **命名空间是强制的**:前缀由装配层按 `namespace` 拼,宿主在 `name` 里自己写前缀
441
470
  会被判成非法名字。不这么做的话,一个叫 `general` 的注入会顶掉内置那条,
442
471
  而 `delegate_task` 不带 `role` 时取的就是它
443
- - ⚠️ **三样的分隔符不一样**:角色 / 技能是 `<namespace>:`,MCP server
444
- `<namespace>__` —— server 名会原样变成工具名的一部分,而 provider 硬拒带 `:`
445
- 的工具名(实测)。判据在 plugin-mcp 的 `McpServerStatus.source`
472
+ - ⚠️ **分隔符分两种**:角色 / 技能是 `<namespace>:`,MCP server 和工具是
473
+ `<namespace>__` —— 后两样都会原样进工具名,而 provider 硬拒带 `:` 的工具名
474
+ (实测)。判据在 plugin-mcp 的 `McpServerStatus.source` 上。
475
+ ⚠️ 字符集也分两种:**工具名允许 `_`**(`list_jobs` 是对的),server 名不允许 ——
476
+ 前者是名字的末段,后者夹在两个 `__` 中间
446
477
  - **`source` 宿主写不出来**:`HostAgentRole` 上没有那个字段,`McpServerConfig`
447
- 上也没有,装配层填死 `'host'`
478
+ 和 `EpochTool` 上也没有,装配层填死 `'host'`
448
479
  - **坏的那一条只跳过它自己**,各留一条诊断(`module === 'HostCapabilities'`)。
449
480
  唯一整份作废的是 `namespace` 本身坏掉 —— 那时没有前缀
450
481
  - **注入是装配时一次性的**,但注入进去的东西生命周期照旧:技能照样被 `SkillLearner`
451
- 影响(只是那个目录只读),server 照样掉线重连、照样能在能力页上按「重连」
482
+ 影响(只是那个目录只读),server 照样掉线重连、照样能在能力页上按「重连」。
483
+ ⚠️ 工具那一格上「一次性」最硬:注册表没有「换掉某一个工具」的路
484
+ - ⚠️ **注入的工具照样过审批闸门**,而这不是我们答应的一件事:闸门在
485
+ `ToolExecutor` 上,它不看来源。免不免确认由 `annotations` / `operation` 决定,
486
+ 和内置 / 插件 / MCP 工具走同一套。连带一件白拿的:`execute(args, ctx)` 的
487
+ `ctx.sessionId` 是**真正在跑的那个会话**(绕一圈 MCP 的话这个值到不了宿主手上)
452
488
 
453
489
  注入的 server **装配时就真连**,并和用户 `mcp.json` 那批**共用同一份**
454
- `MCP_CONNECT_BUDGET_MS`(不是两份相加)。真连是因为 `## 可用工具` 是按第一轮的
455
- 工具表拼进 system prompt 的;共用一份是因为那个预算存在的全部理由就是
456
- 「启动不能被一台连不上的 server 拖住」。和用户那批重名时**用户赢**。
490
+ `MCP_CONNECT_BUDGET_MS`(不是两份相加)。真连是因为**第一轮发给 provider 的那份
491
+ 工具 schema 数组是按那一刻的注册表拼的**(2026-08-26 之前这句话的凭据是
492
+ `## 可用工具` 那段散文清单,那一段删了,结论没变);共用一份是因为那个预算存在的
493
+ 全部理由就是「启动不能被一台连不上的 server 拖住」。和用户那批重名时**用户赢**。
457
494
 
458
495
  **要 artifact 的路径就读 `rt.artifactsRoot`。** 它和 `rt.homeDir` 是这次装配的事实,
459
496
  不是配置里的一个字段。2026-08-15 之前 artifact 的**写**路径不认 `homeDir`
460
497
  (读那侧一直认),后果是宿主传了 `homeDir` 就产物 404 而全链路 200 ——
461
498
  在宿主侧 `join(homeDir, 'artifacts')` 拼一遍,就是给同一个目录第二个说法。
462
499
 
500
+ **`rt.sessions.delete()` 现在连带删产物**(2026-08-20,方案 47 PR-4)。在这之前它
501
+ 只删检查点,而 core 的 `SessionManager.delete()` 上写着「产物由装配层跟着一起清」
502
+ —— 一句从头到尾没兑现过的话。宿主这边要知道两件:
503
+
504
+ - `SessionDeletion` 多了一格 `artifactsRemoved`,和 `checkpointsRemoved` 一样
505
+ **与 `deleted` 分开报**:附属物删不掉不会让整次删除变成失败(对话已经没了,
506
+ 回一个错只会让用户再点一次然后收到 404)。**别把三格 and 起来当成功判据** ——
507
+ 该说的是「删了,但那个目录还在」,而用户下一步要知道是哪个目录
508
+ - 那一格**还没进 `WireDeleteSessionResponse`**(`epoch web` 那条 REST 路只透传,
509
+ 所以它会随 JSON 出门,但契约上没有它)。走 REST 的宿主要读它,得先加那一格
510
+
463
511
  ## 开发
464
512
 
465
513
  ```bash