@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.
- package/README.md +118 -70
- package/dist/index.d.ts +1532 -49
- package/dist/index.js +996 -1180
- package/package.json +9 -9
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { GoalRoundResult, Goal, GoalWrite, GoalClear, Workspace, TrustDecision, ProjectContext, RecentWorkspace, TrustGate, RecentWorkspaces, PermissionManager, SessionManager, AgentLoop, PluginCommandDir, ScheduleRegistrar, HeadlessPolicy, CreateScheduleInput, ScheduleIssue, RegisterOutcome, UpdateScheduleInput, needsAllowlist, ConfigOrigins, SkillDirSpec, ProviderRouter, AgentProvider, GoalService, MemoryManager, SkillSystem, SkillLearner, HookManager, ContextCompressor, TrackerDB, TrustManager, ModelPricing, LayerOutcome, ManagedPolicy, PluginArtifacts, CheckpointSummary, RewindPreview, RewindOutcome, CodeSandbox, ToolRegistry, QuestionAskFn, SettingValue, SettingLayer, SettingStep, SettingWriteTarget, IsolationReport, RoleMergeNotice, SkillMeta, SkillImportResult, SkillImportPreview, SkillImportDone, ShadowFinding, BaseOrigin, PermissionAuditScope, PermissionAuditSnapshot, ToolGate, SettingWriteLayer, SettingApply, SettingWriteFailure, PluginPathRef } from '@epoch-agent/core';
|
|
2
|
-
export { SANDBOX_COVERS, SANDBOX_EXCLUDES, SCHEDULE_DEFAULTS, SCHEDULE_DEFAULT_ALLOWLIST, nextRunAt, readRecording, skillIndexDescription } from '@epoch-agent/core';
|
|
1
|
+
import { GoalRoundResult, Goal, GoalWrite, GoalClear, Workspace, TrustDecision, ProjectContext, RecentWorkspace, TrustGate, RecentWorkspaces, PermissionManager, SessionManager, FtsHealth, FtsReclaim, AgentLoop, PluginCommandDir, InstallPreview, PluginRecord, PluginSourceType, PluginCounts, MarketplaceEntry, ScheduleRegistrar, HeadlessPolicy, CreateScheduleInput, ScheduleIssue, RegisterOutcome, UpdateScheduleInput, needsAllowlist, HookSourceStatus, ConfigOrigins, SkillDirSpec, ProviderRouter, AgentProvider, GoalService, MemoryManager, SkillSystem, SkillLearner, HookManager, ContextCompressor, TrackerDB, TrustManager, ModelPricing, LayerOutcome, ManagedPolicy, PluginArtifacts, CheckpointSummary, RewindPreview, RewindOutcome, ToolRevealState, CodeSandbox, ISkillSystem, ToolRegistry, QuestionAskFn, ResponseFormat, ProviderWarning, SettingValue, SettingLayer, SettingStep, SettingWriteTarget, SessionReferences, IsolationReport, RoleMergeNotice, SkillMeta, SkillIndexResidency, SkillImportResult, SkillImportPreview, SkillImportDone, ShadowFinding, BaseOrigin, PermissionAuditScope, PermissionAuditSnapshot, CachedApproval, ToolGate, SettingWriteLayer, SettingApply, SettingWriteFailure, PluginPathRef } from '@epoch-agent/core';
|
|
2
|
+
export { DEFAULT_GOAL_MAX_ROUNDS, MAX_GOAL_MAX_ROUNDS, MAX_OBJECTIVE_CHARS, MAX_REASON_CHARS, SANDBOX_COVERS, SANDBOX_EXCLUDES, SCHEDULE_DEFAULTS, SCHEDULE_DEFAULT_ALLOWLIST, nextRunAt, readRecording, skillIndexDescription } from '@epoch-agent/core';
|
|
3
3
|
import { McpServerConfig, McpServerStatus, McpServerSource, McpTransport, McpAuthStatus } from '@epoch-agent/plugin-mcp';
|
|
4
4
|
export { McpAuthState, McpAuthStatus, McpServerConfig } from '@epoch-agent/plugin-mcp';
|
|
5
|
-
import { Lang, DiagnosticSink, EpochContentPart, EpochToolCall, EpochToolResult, EpochTurnFacts, TokenUsage, EpochMessage, ApprovalRequest, ApprovalReply, QuestionRequest, QuestionAnswer, EpochUserContent, AgentEvent, CustomCommandDef, ExpandedCommand, ToolProvider, ModelRef, SetSelectionResult, AgentRole, ProviderType, SecretStore, WireModelSource, WireModelProbeStatus, PermissionLevel, ModelSelection, SelectionContext, ScheduleRunStatus, ScheduleRun, ScheduleDefinition, ScheduleBackendKind, ScheduleRunnerSpec, LogFn, EpochConfig, ActiveAgentRole, ApprovalOutcome, PermissionManager as PermissionManager$1, TrustManager as TrustManager$1, ContextBreakdown, Diagnostic } from '@epoch-agent/protocol';
|
|
5
|
+
import { Lang, DiagnosticSink, EpochContentPart, EpochToolCall, EpochToolResult, EpochTurnFacts, TokenUsage, EpochMessage, ApprovalRequest, ApprovalReply, QuestionRequest, QuestionAnswer, EpochUserContent, AgentEvent, CustomCommandDef, ExpandedCommand, ToolProvider, ModelRef, SetSelectionResult, EpochTool, AgentRole, ProviderType, SecretStore, WireModelSource, WireModelProbeStatus, PermissionLevel, ModelSelection, SelectionContext, ScheduleRunStatus, ScheduleRun, ScheduleDefinition, ScheduleBackendKind, ScheduleRunnerSpec, LogFn, EpochConfig, ActiveAgentRole, ApprovalOutcome, PermissionManager as PermissionManager$1, TrustManager as TrustManager$1, ContextBreakdown, Diagnostic } from '@epoch-agent/protocol';
|
|
6
6
|
export { BackgroundTaskInfo, BackgroundTaskStatus } from '@epoch-agent/protocol';
|
|
7
7
|
export { t } from '@epoch-agent/infra';
|
|
8
8
|
export { taskOutput as backgroundTaskOutput, listTasks as listBackgroundTasks } from '@epoch-agent/plugin-terminal';
|
|
@@ -75,6 +75,50 @@ interface GoalControl {
|
|
|
75
75
|
interface GoalRounds {
|
|
76
76
|
beginRound(sessionId: string): GoalRoundResult;
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* 「**任意一段**会话的目标」—— 按 sessionId 现查的那一份(方案 52 PR-4)。
|
|
80
|
+
*
|
|
81
|
+
* ## 它和上面那个 {@link GoalControl} 是两件事,而且都要
|
|
82
|
+
*
|
|
83
|
+
* ```
|
|
84
|
+
* GoalControl 「**我这一段**的目标怎么样」 —— TUI / 一个会话一个宿主
|
|
85
|
+
* GoalCatalog 「**那一段**的目标怎么样」 —— 多会话宿主(web 服务端)
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* 一个 HTTP 服务进程手里同时有 N 段会话,而每一条请求的 sessionId 来自 URL ——
|
|
89
|
+
* 它压根没有「我这一段」这个东西。收窄口在那种宿主里反过来变成障碍:要么给每段
|
|
90
|
+
* 会话各包一个(那是工厂已经干的事,但**它只装得下这个进程正握着的那些**),
|
|
91
|
+
* 要么让服务端拿一个 id 去问一个和它无关的控制面。
|
|
92
|
+
*
|
|
93
|
+
* ⚠️ **这一份能答冷却掉的会话,而工厂那一份不能** —— 这是它存在的正题。
|
|
94
|
+
* 目标**没有内存态**:`GoalService` 每次都现读 SQLite(`GoalStore.current`),
|
|
95
|
+
* 所以「这段会话此刻活不活着」和「它的目标是什么」是两个无关的问题。
|
|
96
|
+
* 计划那一格必须分两个数据源(内存里那份可能比盘上新,判据在
|
|
97
|
+
* `server/src/plan.ts` 的文件头),目标这一格**只有一个真源**,因此不分支。
|
|
98
|
+
*
|
|
99
|
+
* ## ⚠️ 刻意**没有** `block()`,也不许有
|
|
100
|
+
*
|
|
101
|
+
* 判据逐字同 {@link GoalControl}(那儿也没有):`blocked` 是模型 / 策略层的态,
|
|
102
|
+
* 模型走的是它手里那个 `goal` 工具,策略层走的是 `AgentSession.run()` 里那一步。
|
|
103
|
+
* 从宿主这一侧露出一个 `block()`,等于给下一个人一条「原来这儿能报卡住」的路 ——
|
|
104
|
+
* 而那个 `code` 是**给程序路由用的键**(dock 第四档按它决定给哪个动作),
|
|
105
|
+
* 让宿主编一个出来的方向是提权(52 §5.1 结论 3)。
|
|
106
|
+
*
|
|
107
|
+
* `promptView` / `beginRound` 同样不在这儿:前者是 prompt 的事(`goal/provider.ts`),
|
|
108
|
+
* 后者是划轮的事({@link GoalRounds})。**逐个方法转发而不是把 `GoalService`
|
|
109
|
+
* 原样递出去**,就是为了让上面这三条在类型上成立,而不是靠一句叮嘱。
|
|
110
|
+
*/
|
|
111
|
+
interface GoalCatalog {
|
|
112
|
+
current: (sessionId: string) => Goal | null;
|
|
113
|
+
/** 建一个(**人专用**,判据同 `GoalControl.create`) */
|
|
114
|
+
create: (sessionId: string, objective: string, maxRounds?: number) => GoalWrite;
|
|
115
|
+
edit: (sessionId: string, objective: string) => GoalWrite;
|
|
116
|
+
setBudget: (sessionId: string, maxRounds: number) => GoalWrite;
|
|
117
|
+
pause: (sessionId: string) => GoalWrite;
|
|
118
|
+
resume: (sessionId: string) => GoalWrite;
|
|
119
|
+
complete: (sessionId: string, evidence: string) => GoalWrite;
|
|
120
|
+
clear: (sessionId: string) => GoalClear;
|
|
121
|
+
}
|
|
78
122
|
|
|
79
123
|
/**
|
|
80
124
|
* 「这个会话在工作区里改过哪些文件」(方案 42 PR-4)。
|
|
@@ -736,9 +780,15 @@ declare class SessionStore {
|
|
|
736
780
|
*
|
|
737
781
|
* ⚠️ **它不是 `cwd`。** 上面那个 `cwd` 记的是「这段会话属于哪个项目」
|
|
738
782
|
* (`epoch -c` 照它找、`core/session/authorization.ts` 拿它当鉴权边界),
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
783
|
+
* 这一格记的是「用户为这段会话做过什么**决定**」(含「明确不使用工作区」——
|
|
784
|
+
* 那一档压根没有项目根可记)。两者仍然是两件事。
|
|
785
|
+
*
|
|
786
|
+
* ⚠️ 上面那句话 2026-08-20 前半段过期了一次:原文说 `cwd`「在三条路上记的是
|
|
787
|
+
* **服务进程的启动目录**而不是用户选的地盘」,并把那个偏差当成这一格必须另开的
|
|
788
|
+
* 理由之一。**那个偏差是迟绑那一轮修掉的 bug 本身**(判据在 `SessionFactory`
|
|
789
|
+
* 的 `assemble` 上),`cwd` 现在记的就是这段会话真正在跑的那个项目根。
|
|
790
|
+
* **两格照旧都要在**,但理由收窄成上一段那一条:一个是「哪个项目」、
|
|
791
|
+
* 一个是「什么决定」,而后者有一档没有前者。
|
|
742
792
|
*
|
|
743
793
|
* ⚠️ **收 sessionId 而不是闭包一个**:`AgentSession.resume()` 会就地换掉
|
|
744
794
|
* `sessionId`,而这个 store 实例跟着那个 `AgentSession` 走(上面 `cwd` /
|
|
@@ -760,8 +810,13 @@ declare class SessionStore {
|
|
|
760
810
|
* 传项目根而不是 `process.cwd()`:在子目录里敲 `epoch -c` 要接上同一个仓库的
|
|
761
811
|
* 会话。读取侧(`SessionManager.getLatestByCwd`)用的是同一个口径。
|
|
762
812
|
* 不给就仍然写 NULL —— 那段会话只是不参与 `-c` 匹配,别的一切照旧。
|
|
813
|
+
*
|
|
814
|
+
* ⚠️ **收函数不收字符串**(迟绑,2026-08-20),判据同下面那个 `decision`:
|
|
815
|
+
* 建行发生在**第一条消息**那一刻,而这段会话的地盘可能是装配之后才选的
|
|
816
|
+
* (`deferWorkspace` 那一档)。快照的表现是一段在用户挑的目录里干了一整轮活的
|
|
817
|
+
* 会话,在库里记着服务进程的启动目录 —— 于是 `epoch -c` 在那个项目里接不到它。
|
|
763
818
|
*/
|
|
764
|
-
constructor(sessions: SessionManager, artifactsRoot?: string | undefined, cwd?: string | undefined,
|
|
819
|
+
constructor(sessions: SessionManager, artifactsRoot?: string | undefined, cwd?: (() => string | undefined) | undefined,
|
|
765
820
|
/**
|
|
766
821
|
* 这段会话是谁开的(`sessions.source`)。缺省 `'cli'`。
|
|
767
822
|
*
|
|
@@ -785,9 +840,15 @@ declare class SessionStore {
|
|
|
785
840
|
*
|
|
786
841
|
* ⚠️ **它不是 `cwd`。** 上面那个 `cwd` 记的是「这段会话属于哪个项目」
|
|
787
842
|
* (`epoch -c` 照它找、`core/session/authorization.ts` 拿它当鉴权边界),
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
843
|
+
* 这一格记的是「用户为这段会话做过什么**决定**」(含「明确不使用工作区」——
|
|
844
|
+
* 那一档压根没有项目根可记)。两者仍然是两件事。
|
|
845
|
+
*
|
|
846
|
+
* ⚠️ 上面那句话 2026-08-20 前半段过期了一次:原文说 `cwd`「在三条路上记的是
|
|
847
|
+
* **服务进程的启动目录**而不是用户选的地盘」,并把那个偏差当成这一格必须另开的
|
|
848
|
+
* 理由之一。**那个偏差是迟绑那一轮修掉的 bug 本身**(判据在 `SessionFactory`
|
|
849
|
+
* 的 `assemble` 上),`cwd` 现在记的就是这段会话真正在跑的那个项目根。
|
|
850
|
+
* **两格照旧都要在**,但理由收窄成上一段那一条:一个是「哪个项目」、
|
|
851
|
+
* 一个是「什么决定」,而后者有一档没有前者。
|
|
791
852
|
*
|
|
792
853
|
* ⚠️ **收 sessionId 而不是闭包一个**:`AgentSession.resume()` 会就地换掉
|
|
793
854
|
* `sessionId`,而这个 store 实例跟着那个 `AgentSession` 走(上面 `cwd` /
|
|
@@ -919,6 +980,19 @@ declare class SessionStore {
|
|
|
919
980
|
* `assistantText += event.text`),合完是空就整条不发(那边也是
|
|
920
981
|
* `if (assistantText)`)。判据是 resume 回来的上下文必须和刚才在内存里的那份
|
|
921
982
|
* **逐字相同** —— 差一个换行,模型看到的就是另一段历史。
|
|
983
|
+
*
|
|
984
|
+
* ## ⚠️ 2026-08-28:合并之后还要挂上那一行「本轮调用过的工具」
|
|
985
|
+
*
|
|
986
|
+
* tool 行仍然不进历史(上面第一段那条理由没变),但它们身上的
|
|
987
|
+
* `toolResults` 现在要被**读一眼**:那一轮调过哪些工具,折成一行挂在合并后的
|
|
988
|
+
* assistant 消息末尾。判据全文在 protocol 的 `assistantTurnText` 上,一句话是
|
|
989
|
+
* 「光留最终正文的话,早期那几条用户指令在模型眼里像是没做完」。
|
|
990
|
+
*
|
|
991
|
+
* 所以这个循环从「逐条过滤」改成了**按轮分组**:user 行是轮边界,一轮里的
|
|
992
|
+
* assistant 正文和 tool 结果各自攒起来,边界上一次性折出一条。
|
|
993
|
+
* 攒的那一份和直播那边喂给 `assistantTurnText` 的**是同一批东西**
|
|
994
|
+
* (`steps.flatMap(s => s.toolResults)`),所以两条路产出逐字节相同 ——
|
|
995
|
+
* 门禁在 `packages/runtime/__tests__/context-recency.test.ts`。
|
|
922
996
|
*/
|
|
923
997
|
loadHistory(sessionId: string): Array<{
|
|
924
998
|
role: 'user' | 'assistant';
|
|
@@ -948,6 +1022,29 @@ declare class SessionStore {
|
|
|
948
1022
|
* 汇总,它自己坏了不该让整屏 500。
|
|
949
1023
|
*/
|
|
950
1024
|
fileChanges(sessionId: string, workDir: string): SessionFileChange[];
|
|
1025
|
+
/**
|
|
1026
|
+
* FTS 索引体检(方案 48 PR-4)—— `epoch doctor` 的「会话检索」一节。
|
|
1027
|
+
*
|
|
1028
|
+
* 这个方法在这里纯粹是**够得着**的问题:doctor 手上有 `rt.sessionStore`,
|
|
1029
|
+
* 没有 `SessionManager`(`EpochRuntime` 刻意不交出那个 —— 交出去等于给宿主
|
|
1030
|
+
* 一条对会话库执行任意 SQL 的路)。所以照这个类既有的形状再加一层薄封装。
|
|
1031
|
+
*
|
|
1032
|
+
* ⚠️ **只给 doctor 用。** 它要扫全表 + 跑完整性检查(外部内容表上还有一道
|
|
1033
|
+
* `rank = 1` 的逐行比对,要重分词),会话库大起来之后是秒级的。
|
|
1034
|
+
* 别接到启动路径或任何 REST 端点上,
|
|
1035
|
+
* 判据全文在 `core/src/session/fts-health.ts` 的文件头。
|
|
1036
|
+
*/
|
|
1037
|
+
ftsHealth(): FtsHealth;
|
|
1038
|
+
/**
|
|
1039
|
+
* 把删掉的会话在索引里留下的字节收回来 —— `epoch doctor --reclaim` 的落点。
|
|
1040
|
+
*
|
|
1041
|
+
* 在这里的理由同上面那一条(doctor 手上只有 `rt.sessionStore`)。
|
|
1042
|
+
*
|
|
1043
|
+
* ⚠️ 这一个**会写**,而且 `VACUUM` 要独占库文件:**只许由用户显式要求触发**,
|
|
1044
|
+
* 别接到任何自动路径上(三条理由在 `core/src/session/fts-health.ts` 的
|
|
1045
|
+
* `reclaimFtsSpace()` 上)。
|
|
1046
|
+
*/
|
|
1047
|
+
reclaimFts(vacuum?: boolean): FtsReclaim;
|
|
951
1048
|
}
|
|
952
1049
|
|
|
953
1050
|
/**
|
|
@@ -1054,6 +1151,18 @@ interface AgentSessionOptions {
|
|
|
1054
1151
|
* 里 `lastRole` 那一格):从「有」变成「没有」同样要留痕。
|
|
1055
1152
|
*/
|
|
1056
1153
|
currentRole?: () => string | undefined;
|
|
1154
|
+
/**
|
|
1155
|
+
* 这一轮在哪个目录里跑(迟绑,2026-08-20,可选)。没给就一条留痕都不插。
|
|
1156
|
+
*
|
|
1157
|
+
* **给函数不给路径**,判据逐字同上面 `currentRole`:`unbound` 那一档的会话
|
|
1158
|
+
* (侧栏「新建会话」)建出来时还没有地盘、跑在服务进程的启动目录上,
|
|
1159
|
+
* 地盘是用户之后在输入框底栏那张 picker 里挑的。快照永远等于启动目录。
|
|
1160
|
+
*
|
|
1161
|
+
* ⚠️ **它是「引擎现在按哪个目录干活」,不是「绑定表里写着什么」**:后者在
|
|
1162
|
+
* 「不使用工作区」和「还没选」两档都是 null,而引擎其实在启动目录里跑着 ——
|
|
1163
|
+
* 拿它留痕会说出一句和真跑的不是同一件事的话。真源见 `AgentSiteRef`。
|
|
1164
|
+
*/
|
|
1165
|
+
currentWorkDir?: () => string;
|
|
1057
1166
|
}
|
|
1058
1167
|
declare class AgentSession {
|
|
1059
1168
|
/**
|
|
@@ -1084,6 +1193,17 @@ declare class AgentSession {
|
|
|
1084
1193
|
* 那个解析器就静默失灵。
|
|
1085
1194
|
*/
|
|
1086
1195
|
private lastRole;
|
|
1196
|
+
private readonly currentWorkDir;
|
|
1197
|
+
/**
|
|
1198
|
+
* 上一轮在哪个目录里跑(迟绑,2026-08-20)。
|
|
1199
|
+
*
|
|
1200
|
+
* ⚠️ **和 `lastRole` 有一处刻意的差别:它的起点不是构造那一刻的值,而是
|
|
1201
|
+
* `null`**。理由是这条留痕只在**已经聊过**的会话上才有话要说(见
|
|
1202
|
+
* {@link markWorkspaceHandoff}),而构造那一刻历史可能是空的也可能不是
|
|
1203
|
+
* (`revive()` 接回来的那一段),两种情形下起点取哪个都会有一半是错的。
|
|
1204
|
+
* 取 null + 由那个方法自己判「历史空不空」,两种情形就只有一套判据。
|
|
1205
|
+
*/
|
|
1206
|
+
private lastWorkDir;
|
|
1087
1207
|
/** 不是 `readonly` 的数组内容 —— `compact()` 和 `resume()` 会整体换掉它 */
|
|
1088
1208
|
private history;
|
|
1089
1209
|
/** 待交给宿主的请求队列(审批 + 提问)+ 「有新请求」的唤醒信号 */
|
|
@@ -1208,6 +1328,11 @@ declare class AgentSession {
|
|
|
1208
1328
|
* 宿主只要 for-await 这个流,把 approval-request 交给用户点选即可 ——
|
|
1209
1329
|
* 不用关心阻塞回调、不用自己拼历史、不用自己写持久化。
|
|
1210
1330
|
*
|
|
1331
|
+
* ⚠️ **每一帧在 `yield` 之前还会投一份到进程级事件观测口**(方案 61,
|
|
1332
|
+
* `EpochRuntime.events`)。这不改变这条流的任何语义 —— 它仍然是**单消费者**、
|
|
1333
|
+
* 仍然是拉式(没人迭代就一帧都不产生),观测口只是「多看了一眼」。
|
|
1334
|
+
* 零订阅者时那一眼是一次 `size === 0` 的检查,不分配。
|
|
1335
|
+
*
|
|
1211
1336
|
* @param opts.signal 中止信号。`AgentLoop` 一直支持 `input.signal`(轮次之间
|
|
1212
1337
|
* 和工具执行处都会检查),但这个签名以前不透传,于是宿主唯一的中止手段
|
|
1213
1338
|
* 是硬杀进程 —— `dispose()` 跑不到,SQLite 连接和子进程都留着。
|
|
@@ -1245,6 +1370,34 @@ declare class AgentSession {
|
|
|
1245
1370
|
* 「assistantText 为空时它恰好不写第二行」——那是一个实现细节,不是一条承诺。
|
|
1246
1371
|
*/
|
|
1247
1372
|
private markRoleHandoff;
|
|
1373
|
+
/**
|
|
1374
|
+
* 地盘换了的那一刻,往历史里插一条 `[系统]` 消息(迟绑,2026-08-20)。
|
|
1375
|
+
*
|
|
1376
|
+
* ## 它答的是决定 18 反对中途换地盘的那一条理由
|
|
1377
|
+
*
|
|
1378
|
+
* 那条原话是:「中途换一个,前面那些工具调用里的 `./src/a.ts` 指的就是另一个
|
|
1379
|
+
* 目录了,而**记录里没有任何地方写着「从这条开始换地盘了」**」。
|
|
1380
|
+
* 换地盘这件事本身照旧只许发生一次(`unbound → bound`,服务端那道 409 守着),
|
|
1381
|
+
* 这个方法补的是那句话的后半截 —— 于是那一次换成了一件**记录里说得出**的事。
|
|
1382
|
+
*
|
|
1383
|
+
* 形状三条判据逐字照 {@link markRoleHandoff}(`role: 'user'` + `[系统]` 前缀 /
|
|
1384
|
+
* 两条连续 user 消息是安全的 / 只在真的变了那一条上插),**不在这儿抄第二遍**。
|
|
1385
|
+
*
|
|
1386
|
+
* ## ⚠️ 两道闸,各拦一件事
|
|
1387
|
+
*
|
|
1388
|
+
* 1. **`previous === null` —— 这是这段会话的第一轮。** 主路径(新建会话 →
|
|
1389
|
+
* 挑目录 → 发第一句话)走的就是它:用户从头到尾只挑过一次,插一条
|
|
1390
|
+
* 「地盘换了」是纯噪音,还要占压缩预算。这一轮只记起点;
|
|
1391
|
+
* 2. **`history.length === 0` —— 历史被整体换过。** `compact()` / `resume()`
|
|
1392
|
+
* 会换掉这个数组,而 `lastWorkDir` 活在实例上、换不掉。**这一条不是第 1 条
|
|
1393
|
+
* 的重复**:第一轮之后再撞上空历史,说明前面那几轮已经不在记录里了,
|
|
1394
|
+
* 那时这句话指着一段不存在的对话。
|
|
1395
|
+
*
|
|
1396
|
+
* 旧目录**刻意不印**:`lastWorkDir` 只是这个进程记的一笔(起点是 null,见它的
|
|
1397
|
+
* ⚠️),而模型真正需要的是「此后按哪个目录算」。印一个我们其实没记准的旧路径,
|
|
1398
|
+
* 比不印更糟。
|
|
1399
|
+
*/
|
|
1400
|
+
private markWorkspaceHandoff;
|
|
1248
1401
|
/** 一次性执行并拿最终文本(CLI 单次问答用) */
|
|
1249
1402
|
once(userMessage: EpochUserContent, opts?: RunOptions): Promise<{
|
|
1250
1403
|
text: string;
|
|
@@ -1358,8 +1511,131 @@ interface CommandWiring {
|
|
|
1358
1511
|
declare function wireCommands(opts: WireCommandsOptions): CommandWiring;
|
|
1359
1512
|
|
|
1360
1513
|
/**
|
|
1361
|
-
*
|
|
1362
|
-
*
|
|
1514
|
+
* 进程级事件观测口(方案 61 PR-1)—— 让「一帧都没发」这句话**说得出口**。
|
|
1515
|
+
*
|
|
1516
|
+
* ## 这一格在补什么
|
|
1517
|
+
*
|
|
1518
|
+
* 在它之前,`AgentEvent` 没有「存在」这个概念,只有「被某个 for-await 吃掉了」:
|
|
1519
|
+
* `AgentSession.run()` 是**单消费者** async generator(`server/src/hub.ts` 明写),
|
|
1520
|
+
* 三条消费路(TUI / web 的 hub / `once()`)各自独占一个 for-await。于是
|
|
1521
|
+
* 「这条路一帧 `AgentEvent` 都没发」这种断言**做不出真断言** —— 只能退而断
|
|
1522
|
+
* 「事件可见的副作用没变」(跑过一轮的话历史会长、落盘会多)。那条代理断言挡得住
|
|
1523
|
+
* 「utility 偷偷建了个会话」,挡不住「发了一帧没人收的事件」,
|
|
1524
|
+
* 原文在 [VERIFY_RECORD-59 §十 #1](../../../docs/verify/VERIFY_RECORD-59-host-outlets.md)。
|
|
1525
|
+
*
|
|
1526
|
+
* 这个文件就是那个「存在」:`run()` 每产出一帧、**yield 之前**投递一份到这里。
|
|
1527
|
+
*
|
|
1528
|
+
* ## 它**不是**推式化
|
|
1529
|
+
*
|
|
1530
|
+
* `run()` 是拉式的,事件只在有人迭代时产生。这里不改变这一点 —— 发布点写在
|
|
1531
|
+
* yield 之前,语义是「**产生了一帧**就投递」,不是「**主动广播**」。
|
|
1532
|
+
* 没有人跑 `run()`,总线上一帧都不会有,这和今天的直觉一致。
|
|
1533
|
+
*
|
|
1534
|
+
* ## 四条形状判据
|
|
1535
|
+
*
|
|
1536
|
+
* 1. **进程级,不是 runtime 级。** 这是一个模块级单例:同一个进程里
|
|
1537
|
+
* `buildRuntime()` 建了几份,`EpochRuntime.events` 拿到的都是**同一个**总线。
|
|
1538
|
+
* 这不是省事,是这一格存在的理由 —— 要断的那句话是「**任何**会话一帧都没有」,
|
|
1539
|
+
* 而 runtime 级的总线答不出「别人那个 runtime 偷偷跑了一轮」。
|
|
1540
|
+
* 2. **投递语义:同步 + 订阅者自律。** 回调同步执行,跑慢了会拖慢 `run()`。
|
|
1541
|
+
* **不许在这里加队列** —— 加队列等于引入「掉队订阅者的积压」这个新问题,
|
|
1542
|
+
* 而今天的三个订阅者(测试 / 诊断 / 宿主)都不是重活。要做重活的宿主自己
|
|
1543
|
+
* 在回调里往自己的队列里塞一条就走。
|
|
1544
|
+
* 3. **零订阅零开销。** {@link ProcessEventBus.publish} 的**第一条语句**是
|
|
1545
|
+
* `size === 0` 的一次检查,之后再无分配 —— 投递给订阅者时也不构造信封
|
|
1546
|
+
* (回调签名是两个入参,见 {@link AgentEventSubscriber})。这条由
|
|
1547
|
+
* `__tests__/event-bus.test.ts` 里那道扫源码的门禁钉住,不是靠这段注释。
|
|
1548
|
+
* 4. **订阅者抛错不许炸 `run()`。** catch 住、{@link AgentEventBus.deliveryFailures}
|
|
1549
|
+
* 上记一笔、下一个订阅者照常收。
|
|
1550
|
+
*
|
|
1551
|
+
* ## 为什么这条路上一个投影都没有(核过 `serialize.ts`,2026-08-25)
|
|
1552
|
+
*
|
|
1553
|
+
* [serialize.ts](./serialize.js) 做的是「摘闭包 → 换 `requestId`」,它的前提是
|
|
1554
|
+
* **要过进程边界**(`structuredClone` 对函数直接抛 `DataCloneError`)。本总线整条
|
|
1555
|
+
* 只在进程内(方案 61 §七:跨进程不在范围里),闭包原样递过去才是对的。
|
|
1556
|
+
* 于是这条路上的投影是**零个**,不是第二个 —— 方案 §八 第 3 条那句「不许抄两份
|
|
1557
|
+
* 投影」在这里是以「一份都不做」满足的。
|
|
1558
|
+
*
|
|
1559
|
+
* 同理**不复用 hub 的 `WireEnvelope`**:那上面 `seq` 是断线续传的游标、`ts` 上钉着
|
|
1560
|
+
* 「每产出一个信封恰好读一次挂钟」的不变量,两条本总线一条都答不上
|
|
1561
|
+
* (它不缓冲、不发号、不读钟)。
|
|
1562
|
+
*
|
|
1563
|
+
* **要跨进程旁听的宿主,自己在订阅回调里调一次 `toSerializable()`** ——
|
|
1564
|
+
* 那才是复用的正确姿势:一份投影,两个调用点。
|
|
1565
|
+
*
|
|
1566
|
+
* ## 和 server hub 的 `observe()` 是两条不同的支流,别合并
|
|
1567
|
+
*
|
|
1568
|
+
* `observe()`(方案 54 §三)挂在 hub 的**广播层** —— wire 帧、序列化之后,
|
|
1569
|
+
* 因此只有「由 hub 驱动」的会话上得了它。本总线挂在 `run()` 的产出处,
|
|
1570
|
+
* TUI 驱动的会话、宿主自己驱动 `run()` 的会话都在。两者的取舍写在
|
|
1571
|
+
* [docs/EMBEDDING.md](../../../docs/EMBEDDING.md) §13.5 那张表上。
|
|
1572
|
+
*/
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* 一个旁听者。
|
|
1576
|
+
*
|
|
1577
|
+
* ## 为什么是两个入参,不是一个 `{ sessionId, event }` 信封
|
|
1578
|
+
*
|
|
1579
|
+
* 因为信封是一次**分配**。这条路上每一帧都会走一次,而它唯一的职责是把
|
|
1580
|
+
* 「哪个会话」捎上 —— 为此在每帧上造一个对象,等于让「有人在听」这件事
|
|
1581
|
+
* 变成 `run()` 的一笔固定成本。两个入参之后,**有没有订阅者都不分配**,
|
|
1582
|
+
* 于是判据 3(零订阅零开销)不需要在「零」和「非零」之间讲两套话。
|
|
1583
|
+
*
|
|
1584
|
+
* ⚠️ **`event` 是原对象,不是副本。** `approval-request` / `question` 两种帧上
|
|
1585
|
+
* 带着 `respond` 闭包,旁听者**够得着它**:
|
|
1586
|
+
*
|
|
1587
|
+
* > **不许调。** 那是消费者(跑 for-await 的那一位)的事。旁听者调了,
|
|
1588
|
+
* > 用户面前那个审批框就会在没人点的情况下自己有了答案 —— 而那正是
|
|
1589
|
+
* > `QuestionRelay` 拆成独立一个类要防的那件事(「没人答」的唯一合法表达是
|
|
1590
|
+
* > `skipped`,不是一个编出来的答案)。
|
|
1591
|
+
*
|
|
1592
|
+
* 想改事件内容同理:改的是消费者手里那一份。旁听是**只读**的。
|
|
1593
|
+
*/
|
|
1594
|
+
type AgentEventSubscriber = (event: AgentEvent, sessionId: string) => void;
|
|
1595
|
+
/**
|
|
1596
|
+
* 进程级事件观测口的对外面 —— 宿主拿到的是 `EpochRuntime.events`。
|
|
1597
|
+
*
|
|
1598
|
+
* **刻意只有读和订阅,没有 `publish`。** 发布点只有一个(`AgentSession.run()`),
|
|
1599
|
+
* 把它开给宿主等于让「总线上有一帧」不再蕴含「真的产生了一帧」,
|
|
1600
|
+
* 而那正是这一格全部的价值。
|
|
1601
|
+
*/
|
|
1602
|
+
interface AgentEventBus {
|
|
1603
|
+
/**
|
|
1604
|
+
* 订阅**本进程所有会话**的事件。
|
|
1605
|
+
*
|
|
1606
|
+
* 不带过滤参数是拍过的(方案 61 §八 第 2 条,2026-08-25 拍板「不做」):
|
|
1607
|
+
*
|
|
1608
|
+
* - 判据一:**过滤一行就能自己写**(`if (sessionId !== mine) return`),
|
|
1609
|
+
* 而总线替你做等于多出一套「传了一个不存在的 sessionId 会怎样」的语义;
|
|
1610
|
+
* - 判据二(更硬的那条):**第一批订阅者要的恰恰是不过滤。**
|
|
1611
|
+
* 59 §十 #1 那条真断言问的是「**任何**会话一帧都不能有」——
|
|
1612
|
+
* 有了过滤参数,那条用例就会被写成「过滤引导会话 → 零帧」,
|
|
1613
|
+
* 于是它又变回一条代理断言(挡不住「utility 偷偷开了第二个会话」)。
|
|
1614
|
+
*
|
|
1615
|
+
* @returns 退订。**幂等** —— 多调几次不会误删后来注册的同一个函数(同 `observe()`)
|
|
1616
|
+
*/
|
|
1617
|
+
subscribe(fn: AgentEventSubscriber): () => void;
|
|
1618
|
+
/** 现在有几个旁听者。宿主诊断面板用;也是「退订真的退了」的锚 */
|
|
1619
|
+
readonly subscriberCount: number;
|
|
1620
|
+
/**
|
|
1621
|
+
* 订阅者抛过几次错(累计,进程级)。
|
|
1622
|
+
*
|
|
1623
|
+
* 有这一格是因为**吞掉的错必须留个数**:订阅者抛错不许炸 `run()`(判据 4),
|
|
1624
|
+
* 但「不炸」和「什么都没发生」是两件事 —— 没有这个数的话,一个每帧都抛的
|
|
1625
|
+
* 旁听者在任何一处都看不出来。
|
|
1626
|
+
*
|
|
1627
|
+
* ⚠️ **只有数,没有日志。** 总线是模块级单例,它够不着 `buildRuntime({ logger })`
|
|
1628
|
+
* 注入的那一路(一个进程里可能建过好几份 runtime,各带各的 logger),
|
|
1629
|
+
* 而库代码不许自己往 stderr 写(EMBEDDING §3「`logger` —— 库不写 stderr」)。
|
|
1630
|
+
* 想知道是哪一次抛的,宿主在自己的回调里包一层 try/catch —— 那儿才有上下文。
|
|
1631
|
+
*/
|
|
1632
|
+
readonly deliveryFailures: number;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
* `hostCapabilities` —— 嵌入宿主往能力清单里塞自己的专家 / 技能 / 连接器 / 工具
|
|
1637
|
+
* ([方案 44 §2.1](../../../docs/verify/VERIFY_RECORD-44-host-capability-list.md)、
|
|
1638
|
+
* 方案 59 §四)。
|
|
1363
1639
|
*
|
|
1364
1640
|
* ## 为什么另开一路,而不是扩 `PluginArtifacts`
|
|
1365
1641
|
*
|
|
@@ -1416,31 +1692,73 @@ declare function wireCommands(opts: WireCommandsOptions): CommandWiring;
|
|
|
1416
1692
|
*
|
|
1417
1693
|
* **别把这句话和「注入进去的东西是死的」混起来读。** `EpochRuntime.skills` 是个
|
|
1418
1694
|
* getter,理由是**引擎自己会改它**(`SkillLearner` 在会话里新学)—— 和「谁能往里
|
|
1419
|
-
* 塞」无关。宿主注入的技能照样进索引、照样被 `skill_view`
|
|
1420
|
-
*
|
|
1421
|
-
* CRUD,不是生命周期。
|
|
1695
|
+
* 塞」无关。宿主注入的技能照样进索引、照样被 `skill_view` 读、被读到时照样
|
|
1696
|
+
* `matchCount++`。它只是**只读**(`SkillSystem.requireWritable` 的第三档),
|
|
1697
|
+
* 而那说的是 CRUD,不是生命周期。
|
|
1698
|
+
*
|
|
1699
|
+
* ⚠️ **这里原来写的是「照样被 `SkillLearner` 记有效性」,那一句是假的**,而它是
|
|
1700
|
+
* 对外契约上的假话(宿主会照它做产品决定)。两处都不成立:`SkillLearner` 只写
|
|
1701
|
+
* `SKILL.md`,它压根不碰统计;而 `effectiveness` 只由 `SkillSystem.feedback()` 改,
|
|
1702
|
+
* 那个方法从引入那天起就没有生产调用者,读它的 `maintain()` 同样没有 ——
|
|
1703
|
+
* 于是每条技能的评分从加载到进程结束恒为初值 1,宿主拿它排序 / 筛选拿到的是常量。
|
|
1422
1704
|
*
|
|
1423
|
-
*
|
|
1705
|
+
* **2026-08-25 那一整套删了**(`feedback()` / `maintain()` / `effectiveness` /
|
|
1706
|
+
* 三个反馈计数),所以宿主今天不会再看到那个字段 —— 它不是「变成常量」,
|
|
1707
|
+
* 是不存在。`SkillStats` 只剩 `matchCount` / `lastUsedAt` 两格,
|
|
1708
|
+
* 由 `skill_view` 那条路写。判据在那个接口的 JSDoc 上,
|
|
1709
|
+
* 出处 `docs/verify/VERIFY_RECORD-skill-index-not-wired.md`。
|
|
1424
1710
|
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1711
|
+
* ## 连接器与工具那两样:**前缀用 `__` 不是 `:`**
|
|
1712
|
+
*
|
|
1713
|
+
* 四样里只有这两样拼出来的名字会**上网线进 provider** —— server 名是工具名的
|
|
1714
|
+
* 一部分(`mcp__<server>__<tool>`),而宿主注入的工具**它自己就是工具名**;
|
|
1715
|
+
* 带 `:` 的工具名被 provider 硬拒(实测,判据全文在 `McpServerStatus.source`
|
|
1716
|
+
* 的 JSDoc 上)。所以四样分两种拼法:
|
|
1428
1717
|
*
|
|
1429
1718
|
* | 注入的 | 拼出来的名字 | 为什么 |
|
|
1430
1719
|
* | ------ | -------------------- | --------------------------------------------- |
|
|
1431
1720
|
* | 专家 | `acme:triage` | 角色名只在我们进程里当键用,不进 provider |
|
|
1432
1721
|
* | 技能 | `acme:<分类>/<名字>` | 同上 |
|
|
1433
1722
|
* | server | `acme__github` | **会**变成 `mcp__acme__github__<工具>` 发出去 |
|
|
1723
|
+
* | 工具 | `acme__list_jobs` | **它自己就是工具名** |
|
|
1434
1724
|
*
|
|
1435
|
-
* ⚠️
|
|
1436
|
-
*
|
|
1725
|
+
* ⚠️ 别拿前两行去类推后两行 —— 那条路今天就是红的。反过来也别为了长得一致把
|
|
1726
|
+
* 四样统一成 `__`:`:` 在角色 / 技能那边是**已经在跑**的形状(插件那条路早就这么
|
|
1437
1727
|
* 拼),改它等于改一批用户手上已经生效的名字。
|
|
1438
1728
|
*
|
|
1439
|
-
*
|
|
1729
|
+
* ## ⚠️ 工具名这一格收得比 server 名**松**:它允许 `_`(方案 59 §四)
|
|
1730
|
+
*
|
|
1731
|
+
* 另外三样卡的都是 `AGENT_ROLE_NAME_PATTERN`(`[a-z0-9][a-z0-9-]*`),工具卡的是
|
|
1732
|
+
* {@link HOST_TOOL_NAME_PATTERN}(`[a-z0-9][a-z0-9_-]*`)。差的就是一个下划线,
|
|
1733
|
+
* 而这一格不是放宽标准,是**那条禁令在这一格上没有理由**:
|
|
1734
|
+
*
|
|
1735
|
+
* | 这一段在名字的哪儿 | 后面还有分隔符吗 | 于是 `_` |
|
|
1736
|
+
* | ------------------ | ------------------------------------ | -------- |
|
|
1737
|
+
* | server 名 | 有 —— `mcp__<server>__<工具>` 还要再切一刀 | **禁** —— 放过去 `__` 就有第二种读法 |
|
|
1738
|
+
* | 宿主注入的工具名 | **没有** —— 它是整个名字的末段 | 允许 |
|
|
1739
|
+
*
|
|
1740
|
+
* `namespace` 那一格照旧禁 `_`,所以 `<namespace>__<工具名>` 按**第一个** `__`
|
|
1741
|
+
* 切开永远只有一种读法 —— 这条不变式是 namespace 那张正则守着的,不是工具名这张。
|
|
1742
|
+
*
|
|
1743
|
+
* 反过来禁掉 `_` 的代价很具体:全仓每一个工具都是 snake_case(`file_read` /
|
|
1744
|
+
* `session_search` / `ask_user_question`),宿主照着写必然写 `list_jobs`,
|
|
1745
|
+
* 然后拿到一条「名字不合法」——**宿主注入的工具会成为全 product 里唯一
|
|
1746
|
+
* 不能用 snake_case 的一批**,而挡住它换不来任何一条不变式。
|
|
1747
|
+
*
|
|
1748
|
+
* 📮 **工具名限长 128 那两格,2026-08-24 已治**(判据全文在
|
|
1749
|
+
* VERIFY_RECORD-59 §十 第 5 条):provider 那条正则连着限长 128
|
|
1440
1750
|
* (`^[a-zA-Z0-9_-]{1,128}$`),而宿主注入让工具名多出一段 `<namespace>__`。
|
|
1441
|
-
*
|
|
1442
|
-
*
|
|
1443
|
-
*
|
|
1751
|
+
* 超长撞上的表现是 provider 拒掉**整份**工具表,不是只拒那一个。
|
|
1752
|
+
*
|
|
1753
|
+
* | 哪一格 | 量的是什么 | 在哪量(**同一份实现**) |
|
|
1754
|
+
* | ------ | ----------------------------- | --------------------------------------------------------------- |
|
|
1755
|
+
* | server | `mcp__<ns>__<server>__<工具>` | `registerMcpTools` —— 名字要连上 server 之后才齐,这一层量不出来 |
|
|
1756
|
+
* | 工具 | `<ns>__<工具名>` | `build.ts` 的 3.4c(注册那一刻) |
|
|
1757
|
+
*
|
|
1758
|
+
* 两格共用 {@link dropOverlongToolNames}:超长的那一条**只跳过它自己**并报一条
|
|
1759
|
+
* 指名道姓的诊断 —— **早报而不是等 provider 拒**,口径同「坏的那一条只跳过它
|
|
1760
|
+
* 自己」(留着它等于拿一条注定不能用的工具换整份工具表陪葬)。分开做等于让
|
|
1761
|
+
* 同一条不变式有两份实现,而那正是这个文件头到处在防的事。
|
|
1444
1762
|
*/
|
|
1445
1763
|
|
|
1446
1764
|
/**
|
|
@@ -1500,13 +1818,15 @@ interface HostAgentRole {
|
|
|
1500
1818
|
* roles: [{ name: 'triage', description: '分拣工单,只读' , tools: ['file_read'] }],
|
|
1501
1819
|
* skillDirs: [join(app.getAppPath(), 'resources', 'skills')],
|
|
1502
1820
|
* mcpServers: [{ name: 'github', transport: 'http', url: 'http://127.0.0.1:3456/mcp' }],
|
|
1821
|
+
* tools: [listJobsTool], // name: 'list_jobs' → `acme__list_jobs`
|
|
1503
1822
|
* },
|
|
1504
1823
|
* });
|
|
1505
1824
|
* // → 专家叫 `acme:triage`,技能叫 `acme:<分类>/<名字>`,server 叫 `acme__github`
|
|
1506
|
-
* // (工具因此是 `mcp__acme__github__
|
|
1825
|
+
* // (工具因此是 `mcp__acme__github__<工具>`),进程内工具叫 `acme__list_jobs`,
|
|
1826
|
+
* // 四样都标着 host 来源
|
|
1507
1827
|
* ```
|
|
1508
1828
|
*
|
|
1509
|
-
*
|
|
1829
|
+
* 四格给不给互相独立;四格都不给(或者整个选项不给)时**一行都不跑**,
|
|
1510
1830
|
* 连一条诊断都不多 —— 老调用方逐字节不变。
|
|
1511
1831
|
*/
|
|
1512
1832
|
interface HostCapabilities {
|
|
@@ -1559,13 +1879,49 @@ interface HostCapabilities {
|
|
|
1559
1879
|
*
|
|
1560
1880
|
* 这一批会在**装配时真连**,和用户 `~/.epoch/mcp.json` 里那批共用同一份
|
|
1561
1881
|
* `MCP_CONNECT_BUDGET_MS`(不是两份相加)。**不给宿主一个「要不要连」的
|
|
1562
|
-
*
|
|
1563
|
-
*
|
|
1882
|
+
* 开关**:第一轮发给 provider 的那份工具 schema 数组是按**那一刻的注册表**
|
|
1883
|
+
* 拼的,懒连意味着第一轮模型压根不知道这些工具存在,而「装完第一句话就用得上」
|
|
1564
1884
|
* 正是宿主注入它们的理由。
|
|
1885
|
+
*
|
|
1886
|
+
* ⚠️ 2026-08-26 换了一次措辞:这里原来说的是「`## 可用工具` 那一段是按第一轮的
|
|
1887
|
+
* 工具表拼进 system prompt 的」。那一段删了(判据在 core 的
|
|
1888
|
+
* `SystemPromptSegments` 上),但**结论一个字没变** —— 顶上来的是 schema 数组,
|
|
1889
|
+
* 它同样是第一轮那一刻取的快照。
|
|
1565
1890
|
*/
|
|
1566
1891
|
mcpServers?: readonly McpServerConfig[];
|
|
1892
|
+
/**
|
|
1893
|
+
* 宿主**进程内**的工具(方案 59 §四)。收的就是 `EpochTool` 本体 ——
|
|
1894
|
+
* 一个函数,不是一台 server、不是一条路径。
|
|
1895
|
+
*
|
|
1896
|
+
* 它替掉的是「宿主为了给引擎几个工具,先在自己进程里起一台 MCP server,
|
|
1897
|
+
* 再让引擎绕一圈 HTTP 连回来」那条路。绕掉的不只是那一圈开销:
|
|
1898
|
+
* `ToolContext` 停在我们进程里,JSON-RPC 请求上没有它,于是宿主的工具
|
|
1899
|
+
* **拿不到 `sessionId`**(方案 59 §五)。进程内这条路上它天然就在。
|
|
1900
|
+
*
|
|
1901
|
+
* ⚠️ **`name` 写不带前缀的那半截**,前缀由这一层拼成 `<namespace>__<name>`。
|
|
1902
|
+
* 名字卡 {@link HOST_TOOL_NAME_PATTERN}(`[a-z0-9][a-z0-9_-]*`)——
|
|
1903
|
+
* 比另外三格多一个 `_`,判据(以及为什么这一格上放开 `_` 不破坏任何不变式)
|
|
1904
|
+
* 在文件头那张表上。不合法的那一条**只跳过它自己**,其余照常注入。
|
|
1905
|
+
*
|
|
1906
|
+
* ⚠️ **在权限层它和别人一档,而这不是我们答应的一件事** ——
|
|
1907
|
+
* 闸门在 `ToolExecutor.execute` 的第 2 步上,它对 `toolProvider.get()` 交回来
|
|
1908
|
+
* 的**任何**工具过同一道 `checkPermission`,压根不知道工具是谁给的。
|
|
1909
|
+
* 也就是说:宿主不会因为工具跑在同一个进程里就获得一条绕过审批的路。
|
|
1910
|
+
* 想让自己那个工具免确认,走的是和别人一样的路(`annotations.readOnlyHint`
|
|
1911
|
+
* / `operation` / 用户的权限规则),不是「我是宿主」。
|
|
1912
|
+
*
|
|
1913
|
+
* ⚠️ **装配时一次性**,同 `roles` / `skillDirs`:`ToolRegistry` 只有
|
|
1914
|
+
* `registerTools`,没有「换掉某一个」的路(`unregisterBySource` 是按**来源
|
|
1915
|
+
* 整批**撤,给 MCP 那条全量刷新用的,不是单点替换)。宿主 ship 了哪些工具是
|
|
1916
|
+
* 它的构建期属性 —— 要多一个就是下一次 `buildRuntime()`。
|
|
1917
|
+
*
|
|
1918
|
+
* ⚠️ **撞名保住我们自己的**:内置和插件工具先注册,这一批后注册,
|
|
1919
|
+
* 于是撞上的时候赢的是我们那个(判据同 MCP 那批,`ToolRegistry.addTool`
|
|
1920
|
+
* 先注册的赢)。拼了 `<namespace>__` 之后实际撞不着,但规则不能留空。
|
|
1921
|
+
*/
|
|
1922
|
+
tools?: readonly EpochTool[];
|
|
1567
1923
|
}
|
|
1568
|
-
/**
|
|
1924
|
+
/** 一次注入的产物。四样都可能是空数组,那时装配层什么都不用做 */
|
|
1569
1925
|
interface HostCapabilityWiring {
|
|
1570
1926
|
/** 已经拼好前缀、`source` 填死 `'host'` 的角色表 */
|
|
1571
1927
|
roles: AgentRole[];
|
|
@@ -1580,6 +1936,14 @@ interface HostCapabilityWiring {
|
|
|
1580
1936
|
* 字段,用户就能在自己的 `mcp.json` 里给自己那台贴上宿主的标。
|
|
1581
1937
|
*/
|
|
1582
1938
|
mcpServers: McpServerConfig[];
|
|
1939
|
+
/**
|
|
1940
|
+
* 已经拼好 `<namespace>__` 前缀的工具。
|
|
1941
|
+
*
|
|
1942
|
+
* **这里也不带来源** —— 同 {@link mcpServers}:来源是注册那一步填的
|
|
1943
|
+
* (`registry.registerTools(tools, 'host')`),而 `EpochTool` 上压根没有那一格,
|
|
1944
|
+
* 于是「宿主自己写 source」这件事在类型上说不出口。
|
|
1945
|
+
*/
|
|
1946
|
+
tools: EpochTool[];
|
|
1583
1947
|
}
|
|
1584
1948
|
/**
|
|
1585
1949
|
* 校验 + 拼前缀。**不抛** —— 坏的那一条只跳过它自己,其余照常进来。
|
|
@@ -1589,6 +1953,79 @@ interface HostCapabilityWiring {
|
|
|
1589
1953
|
*/
|
|
1590
1954
|
declare function wireHostCapabilities(caps: HostCapabilities | undefined, diags: DiagnosticSink): HostCapabilityWiring;
|
|
1591
1955
|
|
|
1956
|
+
/**
|
|
1957
|
+
* 宿主预置的插件市场 —— **装配时**那一半(方案 59 §六 E1)。
|
|
1958
|
+
*
|
|
1959
|
+
* ```ts
|
|
1960
|
+
* buildRuntime({ hostMarketplaces: { sources: ['/app/resources/market'] } })
|
|
1961
|
+
* → 把那份 `epoch-marketplace.json` 接进 `marketplaces.json`
|
|
1962
|
+
* → 之后 `runtime.plugins.search()` 才有东西可搜
|
|
1963
|
+
* ```
|
|
1964
|
+
*
|
|
1965
|
+
* 运行期那六个动词(列 / 搜 / 预览 / 装 / 更新 / 卸)在
|
|
1966
|
+
* [plugin-control.ts](./plugin-control.ts)。**刀口按时机下**:这个文件回答
|
|
1967
|
+
* 「装配那一刻要不要把这份清单接上、接不上说什么」,那个文件回答
|
|
1968
|
+
* 「用户点了一下之后发生什么」—— 两者的失败形态和呈现通道都不同
|
|
1969
|
+
* (这边进启动诊断,那边是返回值)。
|
|
1970
|
+
*
|
|
1971
|
+
* ## 一、⚠️ 这一条**会落盘**,而 `hostCapabilities` 不会 —— 两者不是同一件事
|
|
1972
|
+
*
|
|
1973
|
+
* `hostCapabilities` 的判据是「宿主 ship 的东西是这一程的运行期事实,一个字节
|
|
1974
|
+
* 都不落盘」。这一条反过来,理由很具体:**市场记录是「装插件时去哪儿查」的索引**,
|
|
1975
|
+
* 它必须和用户自己 `epoch plugin marketplace add` 加的那些躺在同一张表里 ——
|
|
1976
|
+
* 否则 `<市场>/<插件>` 这个引用形式会有两个答案。
|
|
1977
|
+
*
|
|
1978
|
+
* 落点是宿主自己的 `homeDir`(Electron 宿主给的是 `userData` 下的子目录),
|
|
1979
|
+
* 不是用户的 `~/.epoch` —— 这一点让它和 `hostPreset`「替宿主把文件写到位」同档,
|
|
1980
|
+
* 而不是「往用户的目录里塞东西」。
|
|
1981
|
+
*
|
|
1982
|
+
* ## 二、每次装配**重拉一遍**
|
|
1983
|
+
*
|
|
1984
|
+
* 已经加过同一个地址的走 `updateMarketplace`(重拉 + 整份替换),没加过的走
|
|
1985
|
+
* `addMarketplace`。判据:宿主 ship 了什么是它的**构建期属性** —— 应用升级之后
|
|
1986
|
+
* 那份清单跟着变,而一份留在上一版的快照是假话。
|
|
1987
|
+
*
|
|
1988
|
+
* ## 三、⚠️ 远程市场地址**默认拒**,而且是拒 + 说一句
|
|
1989
|
+
*
|
|
1990
|
+
* 取一份远端目录要发一次 HTTP(`github:` 走 raw.githubusercontent、`https://`
|
|
1991
|
+
* 直接下),本地目录那档只是读一个文件。这条和插件本体那个开关是**同一个**
|
|
1992
|
+
* {@link HostMarketplaces.allowRemote},因为它们是同一个风险的两处出口 ——
|
|
1993
|
+
* 完整判据(为什么本地和远程本来就该是两个开关)在
|
|
1994
|
+
* [plugin-control.ts](./plugin-control.ts) 的文件头第三节。
|
|
1995
|
+
*
|
|
1996
|
+
* **坏的那一条只跳过它自己**,口径同 `wireHostCapabilities`:一条接不上不该让
|
|
1997
|
+
* 其余的清单跟着消失。抢不到写锁那一档也收成诊断 —— 少一个预置市场比装配起不来
|
|
1998
|
+
* 轻一个量级。
|
|
1999
|
+
*/
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* 诊断里的模块名。**稳定标识,宿主拿它做分支**(见 `Diagnostic.module`)——
|
|
2003
|
+
* 改它等于改契约。同 `HOST_CAPABILITIES_MODULE`。
|
|
2004
|
+
*/
|
|
2005
|
+
declare const HOST_MARKETPLACE_MODULE = "HostMarketplaces";
|
|
2006
|
+
/** 宿主预置的市场(方案 59 §6.4) */
|
|
2007
|
+
interface HostMarketplaces {
|
|
2008
|
+
/**
|
|
2009
|
+
* 市场地址,形状同 `epoch plugin marketplace add` 收的那一串:
|
|
2010
|
+
* 本地目录(或那份 `epoch-marketplace.json` 本身)、`github:owner/repo`、
|
|
2011
|
+
* `https://…/epoch-marketplace.json`。
|
|
2012
|
+
*
|
|
2013
|
+
* ⚠️ **后两种默认被 {@link allowRemote} 挡着**(取目录要发一次 HTTP)。
|
|
2014
|
+
* 宿主 ship 的市场绝大多数是第一种 —— 一个躺在 `resources/` 里的 JSON。
|
|
2015
|
+
*
|
|
2016
|
+
* 每次装配都**重拉一遍**,判据见文件头第二节。
|
|
2017
|
+
*/
|
|
2018
|
+
sources: readonly string[];
|
|
2019
|
+
/**
|
|
2020
|
+
* 放开远程 source(`github:` / `git+` / `npm:` / `https://`),**默认 false**。
|
|
2021
|
+
*
|
|
2022
|
+
* 它同时管两处,因为两处是同一个风险:{@link sources} 里的远程市场地址,
|
|
2023
|
+
* 以及清单条目里解析出来是远程的那些插件。判据在
|
|
2024
|
+
* [plugin-control.ts](./plugin-control.ts) 的文件头第三节。
|
|
2025
|
+
*/
|
|
2026
|
+
allowRemote?: boolean;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1592
2029
|
/**
|
|
1593
2030
|
* `hostPreset` —— 装配前替宿主把那几个文件落到位([方案 54 §六](../../../docs/verify/VERIFY_RECORD-54-embedding-host-gaps.md))。
|
|
1594
2031
|
*
|
|
@@ -1732,6 +2169,33 @@ declare function applyHostPreset(opts: ApplyHostPresetOptions): Promise<void>;
|
|
|
1732
2169
|
* 封闭集),模型那一层不许闭合。判据全文在 protocol 的 `wire-provider.ts` 上。
|
|
1733
2170
|
*/
|
|
1734
2171
|
|
|
2172
|
+
/**
|
|
2173
|
+
* 一家 provider 的**显示名**。
|
|
2174
|
+
*
|
|
2175
|
+
* 十三家里十一家原样返回 `PROVIDER_INFOS` 里那个品牌名 —— 品牌名不翻。剩下两家的
|
|
2176
|
+
* 显示名带一句注解(「本地」/「通用适配」),那半句该跟界面语言走,所以走 catalog。
|
|
2177
|
+
*
|
|
2178
|
+
* ## 为什么在 runtime 而不是 protocol 里
|
|
2179
|
+
*
|
|
2180
|
+
* 注解原来就挂在 `PROVIDER_INFOS[].label` 上,而 protocol 是零依赖层、够不着 `t()`
|
|
2181
|
+
* —— 结果是一个把界面切成 English 的人在模型菜单上看到「Ollama (本地)」。
|
|
2182
|
+
* runtime 是**两个消费方都够得着的最低那一层**:`epoch model` 那个向导(cli)和
|
|
2183
|
+
* `GET /api/providers`(server)。放 core 也行,但 provider 目录这件事整个在这个
|
|
2184
|
+
* 文件里,没必要再跨一层。
|
|
2185
|
+
*
|
|
2186
|
+
* ## 为什么是 `switch` 而不是查一张表
|
|
2187
|
+
*
|
|
2188
|
+
* `t()` 的 key 必须是**源码里看得见的字面量**:根 `__tests__/i18n-catalog.test.ts`
|
|
2189
|
+
* 靠扫 `t('...')` 认「哪些 key 是活的」,key 一旦拼出来(`t(`provider.label_${type}`)`)
|
|
2190
|
+
* 那两条会被判成死 key,门禁当场红 —— 而它红得对,因为那种写法下**catalog 里少一条
|
|
2191
|
+
* 没人会发现**。同 `command-safety.ts` 那 66 条 `desc: () => t('danger.x')`。
|
|
2192
|
+
*
|
|
2193
|
+
* @param lang 这一次渲染用哪个语言。server 要按请求给(方案 58),cli 不给 = 进程语言
|
|
2194
|
+
*/
|
|
2195
|
+
declare function providerLabel(info: {
|
|
2196
|
+
readonly type: ProviderType;
|
|
2197
|
+
readonly label: string;
|
|
2198
|
+
}, lang?: Lang): string;
|
|
1735
2199
|
/** 一个可挑的 provider。和网线那一份 `WireProviderOption` 逐字同形 */
|
|
1736
2200
|
interface ProviderOption {
|
|
1737
2201
|
type: ProviderType;
|
|
@@ -1807,11 +2271,285 @@ interface ModelCatalogOptions {
|
|
|
1807
2271
|
*/
|
|
1808
2272
|
declare function buildModelCatalogControl(opts: ModelCatalogOptions): ModelCatalogControl;
|
|
1809
2273
|
|
|
2274
|
+
/**
|
|
2275
|
+
* 「装一个插件」这个动作的**结局形状**,以及 preview → install 那两段的实现
|
|
2276
|
+
* (方案 59 §六 E1)。
|
|
2277
|
+
*
|
|
2278
|
+
* 六个动词的控制面在 [plugin-control.ts](./plugin-control.ts),装配时把宿主预置
|
|
2279
|
+
* 的市场接上在 [host-marketplaces.ts](./host-marketplaces.ts)。**刀口按题目下**:
|
|
2280
|
+
* 这个文件从头到尾只答一个问题 —— 「这一次安装到底成没成,没成是哪一档」。
|
|
2281
|
+
*
|
|
2282
|
+
* ## ⚠️ 两段之间那条缝,用 token 缝上
|
|
2283
|
+
*
|
|
2284
|
+
* 判据逐字同 core 的 [skill/import.ts](../../core/src/skill/import.ts) 文件头:
|
|
2285
|
+
* `preview()` 和 `install()` 是两次调用,中间目录可能被改过 —— 于是
|
|
2286
|
+
* 「你确认的」和「真装上的」是两份。
|
|
2287
|
+
*
|
|
2288
|
+
* 落法**借的是 core 自己那个 `confirm` 回调**,而不是在这一层重新扫一遍目录:
|
|
2289
|
+
*
|
|
2290
|
+
* - `preview()` = 跑一发 `installPlugin`,`confirm` 里把那份预览接走然后回 `false`
|
|
2291
|
+
* —— core 那条路上 `confirm` 回 false 就整件事作废、staging 清干净,
|
|
2292
|
+
* 所以它天然是「一个字节都不写」;
|
|
2293
|
+
* - `install()` = 再跑一发,`confirm` 里对着**这一次真正要装的那份预览**重算摘要,
|
|
2294
|
+
* 对不上就回 `false`。也就是说比对发生在**要落地的那一刻**,不是比对两份快照。
|
|
2295
|
+
*
|
|
2296
|
+
* ⚠️ 摘要算的是**那一屏上的东西**(清单名 / 版本 / 来源 / 扩展物盘点),
|
|
2297
|
+
* **不是文件内容的哈希**:本地那一档装的是软链,它的内容本来就是活的 ——
|
|
2298
|
+
* 对一份随时会变的东西做完整性检查,只会让一条正常的开发路径随机失败。
|
|
2299
|
+
*/
|
|
2300
|
+
|
|
2301
|
+
/**
|
|
2302
|
+
* 一次拒绝的档位。**每一档对一个不同的下一步**,合并任意两档都会让上层拿同一句话
|
|
2303
|
+
* 去答两个问题 —— 判据同 `RoleAddOutcome` 那五档。
|
|
2304
|
+
*
|
|
2305
|
+
* | 档 | 说的是 | 下一步 |
|
|
2306
|
+
* | ----------------- | -------------------------------------------------- | -------------- |
|
|
2307
|
+
* | `unknown-ref` | 不是 `<市场>/<插件>`,或者那个市场 / 插件不存在 | 换个引用 |
|
|
2308
|
+
* | `remote-disabled` | 它是远程 source,而 `allowRemote` 关着 | 宿主去开那个开关 |
|
|
2309
|
+
* | `conflict` | 已经装过同名的 | 先卸载 |
|
|
2310
|
+
* | `stale-token` | 你确认的和现在要装的对不上,**一个字节都没写** | 重看一次预览 |
|
|
2311
|
+
* | `not-installed` | `update` / `uninstall` 收到一个没装过的名字 | 先装上 |
|
|
2312
|
+
* | `nothing-to-do` | **不是出错**:软链装的插件一直是最新的,没得更新 | 什么都不用做 |
|
|
2313
|
+
* | `failed` | 其余(拉不到 / 清单坏了 / 写记录时被别的进程抢先) | 看 `detail` |
|
|
2314
|
+
*/
|
|
2315
|
+
type PluginRefuseReason = 'unknown-ref' | 'remote-disabled' | 'conflict' | 'stale-token' | 'not-installed' | 'nothing-to-do' | 'failed';
|
|
2316
|
+
/** 拒绝的共同形状。`preview` 有值时说明失败发生在读完清单之后,界面可以照样画那一屏 */
|
|
2317
|
+
interface PluginRefusal {
|
|
2318
|
+
ok: false;
|
|
2319
|
+
reason: PluginRefuseReason;
|
|
2320
|
+
detail: string;
|
|
2321
|
+
preview?: InstallPreview;
|
|
2322
|
+
}
|
|
2323
|
+
type PluginPreviewOutcome = {
|
|
2324
|
+
ok: true;
|
|
2325
|
+
preview: InstallPreview;
|
|
2326
|
+
/** 拿去 `PluginControl.install`。判据见文件头 */
|
|
2327
|
+
token: string;
|
|
2328
|
+
} | PluginRefusal;
|
|
2329
|
+
type PluginInstallOutcome = {
|
|
2330
|
+
ok: true;
|
|
2331
|
+
record: PluginRecord;
|
|
2332
|
+
preview: InstallPreview;
|
|
2333
|
+
} | PluginRefusal;
|
|
2334
|
+
|
|
2335
|
+
/**
|
|
2336
|
+
* 插件的**宿主控制面** —— 「宿主替用户装一个扩展物」那条路(方案 59 §六 E1)。
|
|
2337
|
+
*
|
|
2338
|
+
* ```
|
|
2339
|
+
* buildRuntime({ hostMarketplaces: { sources: ['/app/resources/market'] } })
|
|
2340
|
+
* → runtime.plugins.search('office') // 从宿主预置的那份清单里挑
|
|
2341
|
+
* → runtime.plugins.preview('acme/office') // 「将安装什么」,一个字节都不写
|
|
2342
|
+
* → runtime.plugins.install('acme/office', token)
|
|
2343
|
+
* → runtime.plugins.pendingRestart === true // ⚠️ 还没生效,见第二节
|
|
2344
|
+
* ```
|
|
2345
|
+
*
|
|
2346
|
+
* core 那一层([marketplace.ts](../../core/src/plugin/marketplace.ts) /
|
|
2347
|
+
* [install.ts](../../core/src/plugin/install.ts) / `lifecycle.ts` / `store.ts`)
|
|
2348
|
+
* 早就完整了,这个文件**一条安装逻辑都没有** —— 它只做三件只有装配层做得了的事:
|
|
2349
|
+
* 把「这一程加载了谁」和「盘上装了谁」对起来、守住下面第三 / 第四节那两道闸、
|
|
2350
|
+
* 记住「装完了没生效」这一格。装配那一刻把宿主预置的市场接上是隔壁
|
|
2351
|
+
* [host-marketplaces.ts](./host-marketplaces.ts)(刀口按时机下,判据在那个文件头)。
|
|
2352
|
+
*
|
|
2353
|
+
* ## 一、为什么不塞进 [build.ts](./build.ts)
|
|
2354
|
+
*
|
|
2355
|
+
* 判据逐字同 [session-model.ts](./session-model.ts) / [role-write.ts](./role-write.ts)
|
|
2356
|
+
* 单开那一条:**它有一整套自己的判据要有个落点**(下面四节,每一节都不是偏好)。
|
|
2357
|
+
*
|
|
2358
|
+
* ## 二、⚠️ **不做热加载 —— 装完要重建 runtime 才生效**,而这是对外承诺的形态
|
|
2359
|
+
*
|
|
2360
|
+
* 不是「还没做」,是做不到而且不该假装做得到:hook 和角色表**没有热加载路径**,
|
|
2361
|
+
* 而 [ToolRegistry](../../core/src/tools/registry.ts) 压根没有反注册 / 替换 API。
|
|
2362
|
+
* 补一半的表现是「有些扩展物立刻生效、有些要重启」,而用户没有任何办法分辨。
|
|
2363
|
+
*
|
|
2364
|
+
* 所以这一层给的是一格 {@link PluginControl.pendingRestart},**怎么说是宿主的决定**
|
|
2365
|
+
* (它比我们知道自己的用户能不能接受重启)。
|
|
2366
|
+
*
|
|
2367
|
+
* ### 而「重建一次就能拿到新装的扩展物」**成立**,理由不是碰巧
|
|
2368
|
+
*
|
|
2369
|
+
* `loadPlugins` **一个字节的 JS 都不执行**:六类扩展物(命令目录 / 角色 / 技能目录 /
|
|
2370
|
+
* `hooks.json` / `settings.json` / `mcp.json`)全是**路径和数据**,插件里那个
|
|
2371
|
+
* `tools/` 目录我们认得出来但刻意不加载([loader.ts](../../core/src/plugin/loader.ts)
|
|
2372
|
+
* 的 `collect`:「第一版刻意不在 epoch 进程里跑第三方代码」)。
|
|
2373
|
+
*
|
|
2374
|
+
* **没有模块缓存这一层,是这条路能走通的全部理由。** 如果插件是 JS,
|
|
2375
|
+
* `import()` / `require` 的缓存会让「同一路径、新内容」在重建之后**照样是旧的那份** ——
|
|
2376
|
+
* 而那是一个「服务、日志、状态码、诊断全部正常,一个字不报」的失败形态。
|
|
2377
|
+
* 这一句不是附注,删掉它这一整条路就没有判据了。
|
|
2378
|
+
*
|
|
2379
|
+
* ## 三、⚠️ 本地 source 先行,远程默认关 —— 因为风险不是同一个量级
|
|
2380
|
+
*
|
|
2381
|
+
* | source | 装的时候真的会发生什么 |
|
|
2382
|
+
* | -------------------------- | --------------------------- |
|
|
2383
|
+
* | 本地目录 | 建一条**软链** |
|
|
2384
|
+
* | `github:` / `git+https:` | spawn `git clone --depth 1` |
|
|
2385
|
+
* | `npm:` | spawn `npm pack` + 解包 |
|
|
2386
|
+
* | `https://….zip` | 下载 + 解压 |
|
|
2387
|
+
*
|
|
2388
|
+
* 后三档要起子进程或者拉网,第一档只是 `symlinkSync` —— 两者本来就该是两个开关。
|
|
2389
|
+
* 所以 `HostMarketplaces.allowRemote` **默认 false**,而被它挡下的那一条
|
|
2390
|
+
* **是拒绝 + 一句话,不是静默跳过**:静默的表现是搜索里明明有那一条、点了没反应。
|
|
2391
|
+
*
|
|
2392
|
+
* ## 四、⚠️ 这一层**只收 `<市场>/<插件>`,收不了一条路径** —— 比 `skill-import` 更紧
|
|
2393
|
+
*
|
|
2394
|
+
* {@link PluginControl.preview} / {@link PluginControl.install} 的入参上**没有**
|
|
2395
|
+
* 「source」这个东西,只有一个市场引用。于是「指一条任意路径装一个插件」这件事
|
|
2396
|
+
* 在这个形状下**说不出口** —— 同 `HostAgentRole` 少一个 `source` 的手法,
|
|
2397
|
+
* 不是靠 review 盯住的,是编译期的事。逐条对着今天那条最像的路
|
|
2398
|
+
* (`server/src/skill-import.ts`):
|
|
2399
|
+
*
|
|
2400
|
+
* | 判据 | `skill-import` 今天 | 这一条 |
|
|
2401
|
+
* | ---------------- | ------------------------- | -------------------------- |
|
|
2402
|
+
* | 能不能传字节 | 不能(只收路径) | 不能 |
|
|
2403
|
+
* | 能不能指任意路径 | **能**(收一个任意 path) | **不能**(只能从清单里挑) |
|
|
2404
|
+
* | 清单是谁放的 | — | 宿主主进程 / 用户自己敲的 |
|
|
2405
|
+
*
|
|
2406
|
+
* ⚠️ 「清单」= `~/.epoch/marketplaces.json` 里那些,也就是**宿主预置的**加上
|
|
2407
|
+
* **用户自己 `epoch plugin marketplace add` 加的**。后者照样算数:那是用户在自己
|
|
2408
|
+
* 机器上做的决定,和宿主主进程同一档。这条路唯一挡死的是「谁都没列过的那个东西」——
|
|
2409
|
+
* 要装清单外的插件,路仍然在,只是**在命令行上**(那一问的闸门只该有一份)。
|
|
2410
|
+
*
|
|
2411
|
+
* ## 五、preview → install 是两段,中间那条缝用 token 缝上
|
|
2412
|
+
*
|
|
2413
|
+
* {@link PluginControl.preview} 回一个**内容摘要**,install 那一发对着**这一次
|
|
2414
|
+
* 真正要装的那份预览**重算一遍,对不上就整件事作废(`stale-token`)。
|
|
2415
|
+
* 实现和完整判据(含「为什么**不**哈希文件内容」)在
|
|
2416
|
+
* [plugin-install.ts](./plugin-install.ts) 的文件头 —— 这一层只负责在调它之前
|
|
2417
|
+
* 把上面第三 / 第四节那两道闸过掉。
|
|
2418
|
+
*/
|
|
2419
|
+
|
|
2420
|
+
/**
|
|
2421
|
+
* 装着的一个插件在宿主眼里的样子。
|
|
2422
|
+
*
|
|
2423
|
+
* 就是 `plugins.json` 里那条记录,**多一格 {@link active}** —— 而那一格是这个
|
|
2424
|
+
* 列表存在的全部理由,见它自己的注释。
|
|
2425
|
+
*/
|
|
2426
|
+
interface PluginListEntry {
|
|
2427
|
+
name: string;
|
|
2428
|
+
version: string;
|
|
2429
|
+
source: string;
|
|
2430
|
+
sourceType: PluginSourceType;
|
|
2431
|
+
path: string;
|
|
2432
|
+
/** 本地目录装的是软链(改源码、重建即生效),其余是真实拷贝 */
|
|
2433
|
+
linked: boolean;
|
|
2434
|
+
enabled: boolean;
|
|
2435
|
+
installedAt: number;
|
|
2436
|
+
/** 从市场装的话是市场名 */
|
|
2437
|
+
marketplace?: string;
|
|
2438
|
+
/**
|
|
2439
|
+
* **这一程真的加载了它吗。**
|
|
2440
|
+
*
|
|
2441
|
+
* `false` 有两种成因,宿主要分开说:这一程刚装上(要重建才生效,见
|
|
2442
|
+
* {@link PluginControl.pendingRestart}),或者它被停用 / 版本不符 / 目录没了
|
|
2443
|
+
* (那时启动诊断里有一条指名道姓的)。
|
|
2444
|
+
*
|
|
2445
|
+
* 少了这一格,一个刚装完的插件在列表里和一个正在生效的插件长得一模一样 ——
|
|
2446
|
+
* 而那正是「点了半个反应」的形态。
|
|
2447
|
+
*/
|
|
2448
|
+
active: boolean;
|
|
2449
|
+
/**
|
|
2450
|
+
* 六类扩展物各贡献了几条。**只有 {@link active} 那些有** ——
|
|
2451
|
+
* 没加载的插件我们没有数过它,给一个 0 会被读成「它什么都不带」。
|
|
2452
|
+
*/
|
|
2453
|
+
counts?: PluginCounts;
|
|
2454
|
+
}
|
|
2455
|
+
/** 市场里的一条,加上「在当前开关下装不装得上」 */
|
|
2456
|
+
interface PluginSearchHit {
|
|
2457
|
+
marketplace: string;
|
|
2458
|
+
entry: MarketplaceEntry;
|
|
2459
|
+
/** `<市场>/<插件>` —— 直接喂给 {@link PluginControl.preview} */
|
|
2460
|
+
ref: string;
|
|
2461
|
+
/** 解析出来的来源档位;这一条的 `source` 看不懂时 `undefined` */
|
|
2462
|
+
sourceType?: PluginSourceType;
|
|
2463
|
+
/**
|
|
2464
|
+
* 在**当前**开关下装得上吗。`false` = 它是远程 source 而远程档关着。
|
|
2465
|
+
*
|
|
2466
|
+
* ⚠️ **不从结果里剔掉它**,判据同方案 59 §2.3 那条「如实回报比替调用方决定诚实」:
|
|
2467
|
+
* 剔掉的话界面上少一条,而用户唯一能得出的结论是「这个市场里没有那个东西」。
|
|
2468
|
+
* 该画成不可点 + 说出原因。
|
|
2469
|
+
*/
|
|
2470
|
+
installable: boolean;
|
|
2471
|
+
}
|
|
2472
|
+
type PluginActionOutcome = {
|
|
2473
|
+
ok: true;
|
|
2474
|
+
detail: string;
|
|
2475
|
+
} | PluginRefusal;
|
|
2476
|
+
/**
|
|
2477
|
+
* 收窄到六个动词的控制面。
|
|
2478
|
+
*
|
|
2479
|
+
* 只有这六个,于是上层(`@epoch-agent/server` 那一页,方案 59 E2)**写不出**
|
|
2480
|
+
* 「往任意路径装一个插件」和「改一条安装记录」—— 同 `McpControl` 只露
|
|
2481
|
+
* `reconnect`、`SkillControl` 只露两个导入动作。
|
|
2482
|
+
*
|
|
2483
|
+
* ⚠️ 刻意**没有 enable / disable**(core 的 `setPluginEnabled` 在那儿摆着):
|
|
2484
|
+
* 那两个动作的正当性和「装 / 卸」不是一回事(停用一个插件会**静默拿掉**别人正
|
|
2485
|
+
* 依赖的一批命令),要开得单独判一次。判据同 `RoleWriteControl` 只有 `add`。
|
|
2486
|
+
*/
|
|
2487
|
+
interface PluginControl {
|
|
2488
|
+
/**
|
|
2489
|
+
* 装着的插件。**读的是盘上那份记录**(`plugins.json`),不是这一程加载出来的
|
|
2490
|
+
* 那张表 —— 否则刚装完的那个不会出现在列表里。两者的差就是
|
|
2491
|
+
* {@link PluginListEntry.active}。
|
|
2492
|
+
*/
|
|
2493
|
+
list(): PluginListEntry[];
|
|
2494
|
+
/**
|
|
2495
|
+
* 在**已加的市场**里搜。空关键词 = 列出全部(那是「里面有什么」的直接答案)。
|
|
2496
|
+
*
|
|
2497
|
+
* 纯本地:目录是加市场那一刻快照下来的,搜索一次网都不上。
|
|
2498
|
+
*/
|
|
2499
|
+
search(keyword?: string): PluginSearchHit[];
|
|
2500
|
+
/**
|
|
2501
|
+
* 「将安装什么」。**一个字节都不写**,回的 `token` 拿去 {@link install}。
|
|
2502
|
+
*
|
|
2503
|
+
* @param ref `<市场>/<插件>`。**收不了一条路径**,判据见文件头第四节
|
|
2504
|
+
* @param lang 这一次 `detail` 用哪个语言说。不给 = 进程语言。⚠️ 只影响回给这
|
|
2505
|
+
* 一次调用的话,落到磁盘上的东西一个字都不受它影响
|
|
2506
|
+
*/
|
|
2507
|
+
preview(ref: string, lang?: Lang): Promise<PluginPreviewOutcome>;
|
|
2508
|
+
/**
|
|
2509
|
+
* 真装。`token` 来自上一次 {@link preview},对不上就拒(`stale-token`)。
|
|
2510
|
+
*
|
|
2511
|
+
* ⚠️ 装完**不会生效**,{@link pendingRestart} 会变成 `true`。
|
|
2512
|
+
*/
|
|
2513
|
+
install(ref: string, token: string, lang?: Lang): Promise<PluginInstallOutcome>;
|
|
2514
|
+
/**
|
|
2515
|
+
* 「更新会带来什么」。同 {@link preview},只是来源取自那条安装记录。
|
|
2516
|
+
*
|
|
2517
|
+
* ⚠️ 更新**也要过这一道**,不是多余的谨慎:新版本可以加 hook(会跑 shell 命令),
|
|
2518
|
+
* 一个当初只带三条命令的插件更新之后可能带上一条 `PreToolUse`。判据逐字同
|
|
2519
|
+
* CLI 的 `epoch plugin update` 为什么在非交互终端下拒绝无 `--yes` 运行。
|
|
2520
|
+
*/
|
|
2521
|
+
previewUpdate(name: string, lang?: Lang): Promise<PluginPreviewOutcome>;
|
|
2522
|
+
/** 按记录里的来源重装一遍。`token` 来自 {@link previewUpdate} */
|
|
2523
|
+
update(name: string, token: string, lang?: Lang): Promise<PluginInstallOutcome>;
|
|
2524
|
+
/**
|
|
2525
|
+
* 卸载。**软链只解链,不删源码目录**(那是 core 的立场,这里不重判一次)。
|
|
2526
|
+
*
|
|
2527
|
+
* ⚠️ 卸完它就从 {@link list} 里消失了,但它这一程带进来的 hook / 命令
|
|
2528
|
+
* **还活着** —— 同样要重建,见 {@link pendingRestart}。
|
|
2529
|
+
*/
|
|
2530
|
+
uninstall(name: string, lang?: Lang): Promise<PluginActionOutcome>;
|
|
2531
|
+
/**
|
|
2532
|
+
* **这一程动过插件、而那些改动还没生效。**
|
|
2533
|
+
*
|
|
2534
|
+
* 一次成功的 install / update / uninstall 之后变 `true`,此后不再变回去 ——
|
|
2535
|
+
* 让它变回去的唯一办法是 `dispose()` + 重新 `buildRuntime()`,那时是一个新的
|
|
2536
|
+
* runtime,这一格从 `false` 开始。
|
|
2537
|
+
*
|
|
2538
|
+
* ⚠️ **它答的是「这一程」,不是「盘上有没有变化」**:另一个进程装的插件不会
|
|
2539
|
+
* 让它变 true(这个 runtime 确实没变过),而那是对的 —— 这一格的消费方是
|
|
2540
|
+
* 「我刚点完那一下,要不要提示用户重启」。
|
|
2541
|
+
*
|
|
2542
|
+
* ⚠️ 这一格从方案 59 §6.5 那一笔挪进来:**宿主自己就要判「装完了没」**,
|
|
2543
|
+
* E2 那个页面只是它的第二个消费方。
|
|
2544
|
+
*/
|
|
2545
|
+
readonly pendingRestart: boolean;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
1810
2548
|
/**
|
|
1811
2549
|
* 身份的写口在装配层的那一格 —— 「新建一个身份」(2026-08-18)。
|
|
1812
2550
|
*
|
|
1813
2551
|
* ```
|
|
1814
|
-
* runtime.roleWrite.add({name, description, prompt, tools, maxTurns})
|
|
2552
|
+
* runtime.roleWrite.add({name, description, prompt, tools, skills, maxTurns})
|
|
1815
2553
|
* → ~/.epoch/agents/<name>.md 落盘 + **这个进程当场认得它**
|
|
1816
2554
|
* ```
|
|
1817
2555
|
*
|
|
@@ -1871,6 +2609,15 @@ interface RoleAddInput {
|
|
|
1871
2609
|
description: string;
|
|
1872
2610
|
prompt?: string;
|
|
1873
2611
|
tools?: readonly string[];
|
|
2612
|
+
/**
|
|
2613
|
+
* 技能索引白名单(2026-08-27)。**这一层原样透传,一个字都不校验** ——
|
|
2614
|
+
* 和上面 `tools` 刻意不同,判据全文在 protocol 的 `WireRoleAddRequest.skills` 上,
|
|
2615
|
+
* 一句话:工具表装配期定死,技能表**会话中途还会变**(`SkillLearner` 学一条就
|
|
2616
|
+
* 往用户级目录里落一份),把「此刻没装」判成 400 是拒掉一个明天就成立的名字。
|
|
2617
|
+
*
|
|
2618
|
+
* 打错的那一档由启动诊断 `unknownRoleSkills()` 报,不在这条路上报。
|
|
2619
|
+
*/
|
|
2620
|
+
skills?: readonly string[];
|
|
1874
2621
|
maxTurns?: number;
|
|
1875
2622
|
}
|
|
1876
2623
|
/**
|
|
@@ -2369,6 +3116,18 @@ interface ScheduleCapability {
|
|
|
2369
3116
|
* 不许写成「已启用」——那是这一整节唯一真正要守住的东西。
|
|
2370
3117
|
*/
|
|
2371
3118
|
canRegister: boolean;
|
|
3119
|
+
/**
|
|
3120
|
+
* 底下那张表打开了没有(2026-08-26 补的一格)。
|
|
3121
|
+
*
|
|
3122
|
+
* `false` = `sessions.db` 打不开,这一片**整个没法用**:一条都列不出来、
|
|
3123
|
+
* 一条都存不进去。界面上要说的是「读不出来」而**不是「这台机器上没有定时任务」**
|
|
3124
|
+
* —— 后者是一句假话,判据同 `no-goal-store` 那一格(「503 而不是一份空清单」)。
|
|
3125
|
+
*
|
|
3126
|
+
* ⚠️ 和 `canRegister: false` 不是一回事,别合并成一个布尔:那一格是
|
|
3127
|
+
* 「存得下但到点不会自己跑」(一个**降级**,路径 B 完全正当),
|
|
3128
|
+
* 这一格是「什么都做不了」(一个**故障**)。两句话在界面上是两种画法。
|
|
3129
|
+
*/
|
|
3130
|
+
storeAvailable: boolean;
|
|
2372
3131
|
}
|
|
2373
3132
|
/** 存下来了没有;注册成没成是另一半 */
|
|
2374
3133
|
interface ScheduleSaveResult {
|
|
@@ -2380,6 +3139,15 @@ interface ScheduleSaveResult {
|
|
|
2380
3139
|
registration?: RegisterOutcome;
|
|
2381
3140
|
}
|
|
2382
3141
|
interface ScheduleControl {
|
|
3142
|
+
/**
|
|
3143
|
+
* 底下那张 SQLite 表打开了没有。**同步的**,因为调用方要拿它当一道前置
|
|
3144
|
+
* (服务端那九条端点在碰 store 之前就要判得出来,而 `capability()` 是 async)。
|
|
3145
|
+
*
|
|
3146
|
+
* ⚠️ **`false` 时下面每一个方法都会抛,不回空清单。** 这是刻意的:一份空清单
|
|
3147
|
+
* 在界面上和「这台机器上没有定时任务」一模一样,而那是假话。抛出去的那一下
|
|
3148
|
+
* 是给**没先看这一格**的宿主的 —— 响亮地坏掉比静静地说谎好。
|
|
3149
|
+
*/
|
|
3150
|
+
readonly storeAvailable: boolean;
|
|
2383
3151
|
list: () => ScheduleDefinition[];
|
|
2384
3152
|
get: (id: string) => ScheduleDefinition | undefined;
|
|
2385
3153
|
/** 某条任务最近几次运行,最新的在前 */
|
|
@@ -2424,10 +3192,34 @@ interface ScheduleControlOptions {
|
|
|
2424
3192
|
/** 平台。可注入是为了让用例能验 Linux 那条「暂不支持」的路 */
|
|
2425
3193
|
platform?: NodeJS.Platform;
|
|
2426
3194
|
}
|
|
2427
|
-
/**
|
|
3195
|
+
/**
|
|
3196
|
+
* 装一份控制面 + 它的关闭钩子(`dispose` 时要放掉 store 的引用计数)。
|
|
3197
|
+
*
|
|
3198
|
+
* ## ⚠️ 开库那一下**不许抛出这个函数**(2026-08-26 修的一条真 bug)
|
|
3199
|
+
*
|
|
3200
|
+
* `ScheduleStore` 开的是 `dbPath(homeDir)` —— 也就是 `sessions.db`,**和
|
|
3201
|
+
* `SessionManager` / `TrackerDB` 同一个文件**。那两个都是套着 `safeInit` 开的
|
|
3202
|
+
* (起不来就是 null + 一条诊断),而这一处原来是裸调的:于是那个文件打不开时
|
|
3203
|
+
* `buildRuntime()` 整个抛,`epoch web` **退出码 1**,而不是起来之后让相关端点
|
|
3204
|
+
* 各回自己那句「读不出来」。
|
|
3205
|
+
*
|
|
3206
|
+
* 那条 bug 是这么撞见的:方案 53 §6.9 想验会话候选端点的 503 `no-session-store`,
|
|
3207
|
+
* 破坏 `sessions.db` 之后**进程根本起不来**,那条分支在真进程上永远走不到 ——
|
|
3208
|
+
* 一个「起不来的东西挡住了另一个东西的降级路径」。根因和结论记在
|
|
3209
|
+
* [RECORD-53 §6.9](../../../../docs/verify/VERIFY_RECORD-53-session-reference.md)。
|
|
3210
|
+
*
|
|
3211
|
+
* **为什么不是把 `EpochRuntime.schedules` 改成可为 null**(那样最像它那三个邻居):
|
|
3212
|
+
* 那是一处真破坏(宿主的 `runtime.schedules.list()` 当场编译不过),而
|
|
3213
|
+
* `0.1.0` 2026-08-19 已经发出去了 —— 按 `.changeset/README.md` 那条现行规则要写
|
|
3214
|
+
* `major`,而 major 传染给全组,十五个包一起进 `1.0.0`。为一条降级路径抬整个
|
|
3215
|
+
* 产品的大版本号,代价和收益不成比例。所以这里选的是**加法**:字段照旧不为 null,
|
|
3216
|
+
* 多一格 {@link ScheduleControl.storeAvailable} 说实话。
|
|
3217
|
+
*/
|
|
2428
3218
|
declare function createScheduleControl(opts: ScheduleControlOptions): {
|
|
2429
3219
|
control: ScheduleControl;
|
|
2430
3220
|
dispose: () => void;
|
|
3221
|
+
/** 开库失败时是那个异常,成功时 null。装配层拿它写那条诊断 */
|
|
3222
|
+
openError: Error | null;
|
|
2431
3223
|
};
|
|
2432
3224
|
|
|
2433
3225
|
/**
|
|
@@ -2444,6 +3236,23 @@ interface PolicyDirStatus {
|
|
|
2444
3236
|
/** 这个目录里加载到的 `[[rule]]` 条数。目录不存在就是 0 */
|
|
2445
3237
|
ruleCount: number;
|
|
2446
3238
|
}
|
|
3239
|
+
/**
|
|
3240
|
+
* hook 的来源账(方案 50 PR-3 的 `epoch doctor` 要用)。
|
|
3241
|
+
*
|
|
3242
|
+
* 为什么要在这一层留住:`loadHookSources()` 的返回值原来在下一行就被
|
|
3243
|
+
* `loadEntries(hooks.entries)` 吃掉了,只剩一个合并好的表。而 doctor 要答的问题
|
|
3244
|
+
* 全在被吃掉的那一半里 ——「这几条 hook 分别来自哪个文件」。
|
|
3245
|
+
*
|
|
3246
|
+
* 方案 50 之后这个问题第一次变得非答不可:以前七成用户只有一份
|
|
3247
|
+
* `~/.epoch/hooks.json`,现在我们会去读 **Claude Code 的三份**,
|
|
3248
|
+
* 于是「我没写过这条命令,它为什么会跑」成了一个真会被问到的问题。
|
|
3249
|
+
*/
|
|
3250
|
+
interface HookStatus {
|
|
3251
|
+
/** 逐个来源,按加载顺序。含**存在但因不受信任而没读**的那些(`skipped: true`) */
|
|
3252
|
+
sources: HookSourceStatus[];
|
|
3253
|
+
/** 项目级那几份因为工作区不受信任而整份没加载 */
|
|
3254
|
+
projectSkipped: boolean;
|
|
3255
|
+
}
|
|
2447
3256
|
/** 策略规则的来源账(方案 42 PR-3 的安全中心要用) */
|
|
2448
3257
|
interface PolicyStatus {
|
|
2449
3258
|
dirs: PolicyDirStatus[];
|
|
@@ -2570,6 +3379,14 @@ interface Services {
|
|
|
2570
3379
|
* 「一个策略目录都没生效」是个真结论,null 会让调用方多一个分支去猜。
|
|
2571
3380
|
*/
|
|
2572
3381
|
policy: PolicyStatus;
|
|
3382
|
+
/**
|
|
3383
|
+
* 这一次装配读了哪几份 hook 配置、每份贡献了几条、项目那几份是不是因为
|
|
3384
|
+
* 不受信任整份没加载(方案 50 PR-3)。
|
|
3385
|
+
*
|
|
3386
|
+
* 和 {@link PolicyStatus} 是同一种东西的两个主题,摊在一屏里要说清区别:
|
|
3387
|
+
* 策略规则管「这个工具碰这个路径算什么」,hook 是**会 spawn 子进程的命令**。
|
|
3388
|
+
*/
|
|
3389
|
+
hook: HookStatus;
|
|
2573
3390
|
/**
|
|
2574
3391
|
* 已装插件带来的扩展物(方案 32)。**永远非 null** —— 没装过插件时是
|
|
2575
3392
|
* `noPlugins()`,九个数组全空,调用方因此不用为「有没有插件」分支。
|
|
@@ -2881,7 +3698,17 @@ interface LiveSession {
|
|
|
2881
3698
|
* 「不是本进程正在跑的那个」,而这个进程正握着它的检查点管理器(方案 30 §9.5)。
|
|
2882
3699
|
*/
|
|
2883
3700
|
readonly checkpoints: CheckpointControl;
|
|
2884
|
-
/**
|
|
3701
|
+
/**
|
|
3702
|
+
* 这个会话绑在哪儿;没绑过 null。
|
|
3703
|
+
*
|
|
3704
|
+
* ⚠️ **现读,不是装配那一刻的快照**(迟绑,2026-08-20):`unbound` 那一档的
|
|
3705
|
+
* 会话是先建出来、后由用户在输入框底栏那张 picker 里挑目录的
|
|
3706
|
+
* (`POST /api/sessions/:id/workspace`),快照答不出那一次。
|
|
3707
|
+
*
|
|
3708
|
+
* 快照的失败形态有两个,而且都不会红:`create({workspace, reuse:true})` 找不到
|
|
3709
|
+
* 那段刚刚绑上这个目录的会话(于是又建一段新的),以及 `delegate_task` 派出去的
|
|
3710
|
+
* 子 agent 跑在启动目录里(`subRunner` 读的就是这一格)。
|
|
3711
|
+
*/
|
|
2885
3712
|
readonly workspace: SessionWorkspace | null;
|
|
2886
3713
|
/**
|
|
2887
3714
|
* **这一条消息用哪个专家**(方案 57 §3.4,2026-08-17)。
|
|
@@ -3578,6 +4405,37 @@ declare function writeMcpServer(input: McpAddInput, opts?: {
|
|
|
3578
4405
|
|
|
3579
4406
|
interface BuildToolsInput {
|
|
3580
4407
|
homeDir: string;
|
|
4408
|
+
/**
|
|
4409
|
+
* `tools.deferMcp`(2026-08-26)。**开着才注册 `tool_search`。**
|
|
4410
|
+
*
|
|
4411
|
+
* ⚠️ 注册与否只看这个开关,**不看「当下有没有 Deferred 工具」** —— 这一步跑在
|
|
4412
|
+
* `registerMcpTools()` 之前,那时一个 MCP 工具都还没进注册表,按数量判的话
|
|
4413
|
+
* 它永远不注册。
|
|
4414
|
+
*
|
|
4415
|
+
* 反过来,关着时不注册也是判据:那时没有任何 Deferred 工具,一个搜什么都搜不到
|
|
4416
|
+
* 的工具进了工具表,白占 token 还让模型多一条会失败的路。
|
|
4417
|
+
*/
|
|
4418
|
+
deferMcp?: boolean;
|
|
4419
|
+
/**
|
|
4420
|
+
* `tools.mode`(方案 51 PR-1 / PR-2)。**`'both'` 和 `'code'` 才注册 `run_code`。**
|
|
4421
|
+
*
|
|
4422
|
+
* 不给 = `'tools'` 档 = 这一轮之前的行为逐字节相同(方案 51「默认关」)。
|
|
4423
|
+
* 这一跳的缺省朝「关」是刻意的:一个嵌入宿主自己拼 `EpochConfig`、整段不写
|
|
4424
|
+
* `tools` 的时候,它拿到的不该是一个能放大审批粒度的工具。
|
|
4425
|
+
*
|
|
4426
|
+
* `'code'` 比 `'both'` 多做一件事:**程序真能调的那些工具从模型的工具表里
|
|
4427
|
+
* 切走**,改以一段生成的 SDK 声明进 system prompt。那一档不在这个函数里落实
|
|
4428
|
+
* (判据见下面 `new ToolRegistry` 那一行)—— 它是 registry 的一格,因为注册
|
|
4429
|
+
* 不止这一处。
|
|
4430
|
+
*/
|
|
4431
|
+
toolMode?: 'tools' | 'both' | 'code';
|
|
4432
|
+
/**
|
|
4433
|
+
* 引导会话那一份放开集。**必须和 `buildSessionGuards` 拿到的是同一个实例** ——
|
|
4434
|
+
* 各建一份的表现是:进程里注册的那个 `tool_search` 往 A 记,而引导会话的
|
|
4435
|
+
* provider 读 B,于是「搜到了但工具没出现」,而两边都答得出「我干了我该干的」。
|
|
4436
|
+
* 判据同 `permission` / `planState` 那条「引导会话拿到的就是进程那一份本体」。
|
|
4437
|
+
*/
|
|
4438
|
+
bootstrapReveal?: ToolRevealState;
|
|
3581
4439
|
trackerDb: TrackerDB | null;
|
|
3582
4440
|
/**
|
|
3583
4441
|
* 目标(方案 52 PR-3)。null = 会话库没起来 —— 那时 `goal` 工具**不注册**。
|
|
@@ -3589,6 +4447,16 @@ interface BuildToolsInput {
|
|
|
3589
4447
|
goals: GoalService | null;
|
|
3590
4448
|
memory: MemoryManager | null;
|
|
3591
4449
|
sandbox: CodeSandbox | null;
|
|
4450
|
+
/**
|
|
4451
|
+
* 技能系统(`services.skill`)。null = 它初始化失败了(`safeInit` 降级成
|
|
4452
|
+
* 「没有技能」)—— 那时 `skill_view` **不注册**,同 `todo` / `memory` 那几条。
|
|
4453
|
+
*
|
|
4454
|
+
* ⚠️ 反过来,**它在就注册,不管当下扫出来几条技能**(哪怕零条)。判据在
|
|
4455
|
+
* `createSkillViewTool` 上:技能表会在会话中途变(`SkillLearner` 学一条就往
|
|
4456
|
+
* 用户级目录里落一份),而工具表是装配期定的 —— 按「启动那一刻有没有技能」
|
|
4457
|
+
* 决定注册与否的表现是,这一段会话里学到的东西这一段会话永远读不了。
|
|
4458
|
+
*/
|
|
4459
|
+
skills: ISkillSystem | null;
|
|
3592
4460
|
diags: DiagnosticSink;
|
|
3593
4461
|
/**
|
|
3594
4462
|
* web 搜索配置(方案 37)。**整段缺席也照样探测**:
|
|
@@ -3788,9 +4656,14 @@ interface McpServerBatch {
|
|
|
3788
4656
|
* `<namespace>__` 前缀)和插件带来的(PR-3,由 `wirePluginMcpServers` 拼好
|
|
3789
4657
|
* `<插件名>__` 前缀)。**和用户那批一样真连、共用同一份预算**(判据在
|
|
3790
4658
|
* {@link connectAllServers} 上),也一样在这个函数返回之前就把工具注册进
|
|
3791
|
-
* `registry` ——
|
|
3792
|
-
*
|
|
3793
|
-
*
|
|
4659
|
+
* `registry` —— 于是它们在**第一轮**就已经在模型的工具表里了。
|
|
4660
|
+
* 懒连的具体后果不是「稍微慢一点看到工具」:那张表是按第一轮的注册表算出来的,
|
|
4661
|
+
* 第二轮起 loop 重问工具表能自愈,第一轮不能。
|
|
4662
|
+
*
|
|
4663
|
+
* 📮 这段话 2026-08-26 之前写的是「在第一轮 system prompt 的 `## 可用工具` 里
|
|
4664
|
+
* 就在了」。**结论一个字没变,坏的是凭据**:那段散文清单当天删了(它和发给
|
|
4665
|
+
* provider 的 schema 数组完全重复,判据在 `core` 的 `SystemPromptSegments` 上),
|
|
4666
|
+
* 工具表现在只走 schema 那一条路。
|
|
3794
4667
|
*
|
|
3795
4668
|
* ⚠️ **再来一批就往这个数组里加一个元素,不是加第五个参数** —— 方案 44 §2.2
|
|
3796
4669
|
* 第 4 处逐字写着「这一处和宿主注入是同一个口子,别开两个」。PR-2 那一轮它还是
|
|
@@ -3799,7 +4672,9 @@ interface McpServerBatch {
|
|
|
3799
4672
|
*
|
|
3800
4673
|
* **顺序即优先级**(先到的赢,重名的后来者跳过),判据在 {@link dropTakenNames}。
|
|
3801
4674
|
*/
|
|
3802
|
-
declare function registerMcpTools(registry: ToolRegistry, homeDir: string, diags: DiagnosticSink, injected?: readonly McpServerBatch[]
|
|
4675
|
+
declare function registerMcpTools(registry: ToolRegistry, homeDir: string, diags: DiagnosticSink, injected?: readonly McpServerBatch[],
|
|
4676
|
+
/** `tools.deferMcp`:MCP 工具默认不进模型的初始工具列表,判据在 `registerMcpBatch` 上 */
|
|
4677
|
+
defer?: boolean): Promise<McpWiring>;
|
|
3803
4678
|
|
|
3804
4679
|
/**
|
|
3805
4680
|
* 用户直接敲的命令(TUI 的 `!ls -la`)和记忆速记(`#记一下`)—— 方案 25 §2.3 / §2.4。
|
|
@@ -3846,6 +4721,331 @@ interface UserActions {
|
|
|
3846
4721
|
}
|
|
3847
4722
|
declare function createUserActions(deps: UserShellDeps): UserActions;
|
|
3848
4723
|
|
|
4724
|
+
/**
|
|
4725
|
+
* 一次性补全 —— `EpochRuntime.utility`(方案 59 需求 B)。
|
|
4726
|
+
*
|
|
4727
|
+
* 一次**不属于任何会话**的模型调用:不建会话、不落历史、不装 system prompt、
|
|
4728
|
+
* 不带工具。宿主要的是「把这段表格格式化成 JSON」「给这个分片起个标题」这类杂活,
|
|
4729
|
+
* 而在这一格之前他们唯一的路是自己 `new` 一个第二个 provider 实例 —— 那个实例
|
|
4730
|
+
* 有自己的工厂缓存、自己的 `ModelFailureTracker`、自己的 `notices`
|
|
4731
|
+
* (没人取 = 漏),凭据链还得他们自己复刻一遍(`keyEnv` 在构造时快照,
|
|
4732
|
+
* 那条链跨三个包)。
|
|
4733
|
+
*
|
|
4734
|
+
* ## 「不带工具」是**类型上**的承诺,不是注释里的一句话
|
|
4735
|
+
*
|
|
4736
|
+
* {@link UtilityGenerateInput} 上压根没有 `tools` 这一格。于是「这条路不过审批
|
|
4737
|
+
* 闸门」不是我们答应的一件事,是这个形状下**说不出口**的一件事 —— 手法同
|
|
4738
|
+
* `HostAgentRole` 刻意少一个 `source`(见 [host-capabilities.ts](./host-capabilities.ts))。
|
|
4739
|
+
*
|
|
4740
|
+
* 连带三条语义,一并写死:这条路**不进 `sessions` 表、不发 `AgentEvent`、
|
|
4741
|
+
* 不占 hub 的活跃会话槽**。它底下就是一次 `router.generate()`,
|
|
4742
|
+
* 中间没有 `AgentLoop`,也没有 `AgentSession`。
|
|
4743
|
+
*
|
|
4744
|
+
* ## 挂哪个 router:**引导会话那一份 = 进程那一份**
|
|
4745
|
+
*
|
|
4746
|
+
* 判据在 [session-model.ts](./session-model.ts) 的文件头(「引导会话那一份**就是**
|
|
4747
|
+
* 进程那一份」)。这样宿主关心的四样东西 —— 降级状态、工厂缓存、通知队列、
|
|
4748
|
+
* 凭据来源 —— 都只有一份,而不是「主循环一份、杂活一份」。
|
|
4749
|
+
*
|
|
4750
|
+
* ⚠️ **代价如实记着,和 `getUtilityInfo()` 那条已知代价并排**:第二个会话
|
|
4751
|
+
* `/model` 换过模型之后,`utility` 跟的仍然是**引导**会话那个选择。这不是 bug
|
|
4752
|
+
* (它答的是「这个进程默认用什么」),但宿主必须知道 —— 否则就是「用户在窗口 2
|
|
4753
|
+
* 换了模型,宿主那批一次性调用还在打旧的」,而那正是方案 30 那一整轮要治的
|
|
4754
|
+
* 那种撒谎。要打别的,`model` 那一格显式点名。
|
|
4755
|
+
*
|
|
4756
|
+
* ## ⚠️ 记账:这一格的**两句话**必须让宿主看懂
|
|
4757
|
+
*
|
|
4758
|
+
* 桶的钥匙写死成一行({@link utilityBudgetKey}):
|
|
4759
|
+
*
|
|
4760
|
+
* ```
|
|
4761
|
+
* key = sessionId ?? `utility:${sanitize(label) || 'anonymous'}`
|
|
4762
|
+
* ```
|
|
4763
|
+
*
|
|
4764
|
+
* 1. **传一个真 `sessionId` 有后果,而且不只是「多记一笔账」。**
|
|
4765
|
+
* `BudgetGuard.syncFromStore()` 在**每一轮请求之前**重读那个会话的桶,
|
|
4766
|
+
* **盘上的数字更大就采纳**(`core/src/agent/loop.ts` 里 `budget.syncFromStore()`
|
|
4767
|
+
* 那一行 + `syncFromStore` 自己的「子 agent 只往 `BudgetStore` 里写、父这边
|
|
4768
|
+
* 采纳」那条判据)。于是:
|
|
4769
|
+
*
|
|
4770
|
+
* > 记到某个真会话名下的工具花费,**会去撞那个会话自己的 `budget.maxCostUsd`
|
|
4771
|
+
* > / `maxTokens`**;撞上了,那个会话的**下一轮直接被拦**,
|
|
4772
|
+
* > 用户看到的是「已达到本次会话的成本上限,任务提前终止」。
|
|
4773
|
+
*
|
|
4774
|
+
* 这**正是「记账才是真话」的完整含义**,不是副作用 —— 那笔钱确实是这段会话
|
|
4775
|
+
* 花的(和 `delegate_task` 派出去那笔走同一条路、同一套判据)。但用户看见的
|
|
4776
|
+
* 是「我让它跑个数据源,然后它就不说话了」,所以宿主界面上要接得住这一下。
|
|
4777
|
+
*
|
|
4778
|
+
* 2. **不传 `sessionId` 那一档,今天没有任何上界。**
|
|
4779
|
+
* `utility:<label>` 这个桶**没有任何 guard 在读它** —— `BudgetGuard` 只在
|
|
4780
|
+
* `AgentLoop` 上,而这条路上没有 `AgentLoop`。也就是说落在保留桶里的花费
|
|
4781
|
+
* **只被记下来,不被拦住**:`budget.maxCostUsd` 配多少都不管它。
|
|
4782
|
+
* 要给它上界得先有一个读它的闸门,那是另一笔(方案 59 §十「跨会话 / 日月预算」)。
|
|
4783
|
+
*
|
|
4784
|
+
* ⚠️ 顺带一条**别误会**的:`label` 今天的价值是「那个 JSON 文件里分得开、
|
|
4785
|
+
* 以后要做视图时数据是齐的」,**不是「今天就能在某一屏上看见」** ——
|
|
4786
|
+
* 全仓没有任何地方展示 `~/.epoch/budget.json` 这本账(`BudgetStore` 只被
|
|
4787
|
+
* `BudgetGuard` 读一次,用来恢复某段会话的累计),`epoch doctor` 上也没有这一行。
|
|
4788
|
+
*/
|
|
4789
|
+
|
|
4790
|
+
/** 一次一次性补全的入参。**没有 `tools` 这一格**,判据见文件头 */
|
|
4791
|
+
interface UtilityGenerateInput {
|
|
4792
|
+
/** 这一次要问的话。就是它的全部上下文 —— 这条路不拼历史、不注入记忆 */
|
|
4793
|
+
messages: EpochMessage[];
|
|
4794
|
+
/**
|
|
4795
|
+
* 系统提示词。**给什么就是什么** —— 引擎那一整套(身份 / 工具说明 / AGENTS.md /
|
|
4796
|
+
* 技能索引)在这条路上一个字都不装。不给就一个字都没有。
|
|
4797
|
+
*/
|
|
4798
|
+
system?: string;
|
|
4799
|
+
/** 最大输出 token */
|
|
4800
|
+
maxTokens?: number;
|
|
4801
|
+
/**
|
|
4802
|
+
* 这一次用哪个模型。
|
|
4803
|
+
*
|
|
4804
|
+
* 不给 = 跟随 `models.utility`;它没配就跟随**引导会话**当前的选择
|
|
4805
|
+
* (`getUtilityInfo()` 的口径,代价见文件头那条 ⚠️)。
|
|
4806
|
+
*
|
|
4807
|
+
* `provider` 不给 = 用当前那一家(同 {@link ModelRef} 的语义)。
|
|
4808
|
+
*/
|
|
4809
|
+
model?: ModelRef;
|
|
4810
|
+
/**
|
|
4811
|
+
* 强制这一轮只出 JSON(方案 59 需求 A)。**provider 无关**。
|
|
4812
|
+
*
|
|
4813
|
+
* ⚠️ **不支持的 provider 不报错,只回一条 warning** —— 请求照发、结果照回、
|
|
4814
|
+
* 那一轮的约束静默消失。所以这一格必须和回参里的
|
|
4815
|
+
* {@link UtilityGenerateResult.responseFormatApplied} **一起读**,
|
|
4816
|
+
* 别只看有没有抛异常。
|
|
4817
|
+
*/
|
|
4818
|
+
responseFormat?: ResponseFormat;
|
|
4819
|
+
/**
|
|
4820
|
+
* 直通给 AI SDK 的 `providerOptions`。**我们不校验、不翻译、不兜底** ——
|
|
4821
|
+
* 外层的 key 是 provider 名,写错就是静默无效。
|
|
4822
|
+
*/
|
|
4823
|
+
providerOptions?: Record<string, Record<string, unknown>>;
|
|
4824
|
+
/**
|
|
4825
|
+
* **这次替谁花的钱。** 不给 = 落保留桶(见 {@link label})。
|
|
4826
|
+
*
|
|
4827
|
+
* ⚠️ 给了它不只是「换一行记账」:那笔钱进这个会话的桶之后,**会去撞这个会话
|
|
4828
|
+
* 自己的 `budget.maxCostUsd` / `maxTokens`** —— 撞上了,这个会话的下一轮就被
|
|
4829
|
+
* 拦下来(`BudgetGuard.syncFromStore()` 每轮采纳盘上更大的那个值)。
|
|
4830
|
+
* 完整判据和「为什么这是真话不是副作用」在文件头第 1 条。
|
|
4831
|
+
*
|
|
4832
|
+
* 判据是**谁发起的**:agent 调你的工具、你拿 `ctx.sessionId` 去跑那一批 →
|
|
4833
|
+
* 传,不传才是假话;用户在你自己的控制台上点了一下、不在任何一次对话里 →
|
|
4834
|
+
* 别传,硬塞一个(尤其是塞引导会话的 id)最省事也最假。
|
|
4835
|
+
*/
|
|
4836
|
+
sessionId?: string;
|
|
4837
|
+
/**
|
|
4838
|
+
* 不属于会话的那些花费记在哪一行(桶名 `utility:<label>`)。
|
|
4839
|
+
*
|
|
4840
|
+
* 要它的理由是**量级**:几个调用点的花费能差两个数量级,混成一行那个数字对谁
|
|
4841
|
+
* 都没用。收窄成 `[a-z0-9_-]{1,32}`,**不合的整格丢掉、退回 `utility:anonymous`
|
|
4842
|
+
* 且不抛** —— 它是个标注不是语义,为一个诊断用的字段让调用失败不值。
|
|
4843
|
+
*
|
|
4844
|
+
* ⚠️ **给了 `sessionId` 时这一格被忽略**:一次调用只落一个桶,而
|
|
4845
|
+
* `key = sessionId ?? ...` 里 `sessionId` 赢。
|
|
4846
|
+
*
|
|
4847
|
+
* ⚠️ 落在这个桶里的花费**今天没有上界**,见文件头第 2 条。
|
|
4848
|
+
*/
|
|
4849
|
+
label?: string;
|
|
4850
|
+
/**
|
|
4851
|
+
* 中止信号。
|
|
4852
|
+
*
|
|
4853
|
+
* ⚠️ **只在发请求之前判一次。** `GenerateInput` 上没有 signal 这一格,
|
|
4854
|
+
* AI SDK 那一跳我们今天穿不过去 —— 所以它管得住「别再发下一个」,
|
|
4855
|
+
* 管不住「把已经发出去的那个掐掉」。宿主那种「循环里跑一百次」的用法,
|
|
4856
|
+
* 要的正好是前者:置了之后 `generate()` 立刻抛,不再产生新的花费。
|
|
4857
|
+
*/
|
|
4858
|
+
signal?: {
|
|
4859
|
+
readonly aborted: boolean;
|
|
4860
|
+
};
|
|
4861
|
+
}
|
|
4862
|
+
/** 一次一次性补全的结果。四格里后两格照 `GenerateOutput` 原样透,不在这一层重新发明 */
|
|
4863
|
+
interface UtilityGenerateResult {
|
|
4864
|
+
text: string;
|
|
4865
|
+
/** provider 没上报时**整个缺席**,不是一份填了 0 的对象(口径同 `GenerateOutput.usage`) */
|
|
4866
|
+
usage?: TokenUsage;
|
|
4867
|
+
/** 这次**实际**落到的模型 / provider(降级之后这两格才是真话) */
|
|
4868
|
+
model: string;
|
|
4869
|
+
provider: ProviderType;
|
|
4870
|
+
/**
|
|
4871
|
+
* 请求里带了 `responseFormat` 时才有这一格,没带就整个缺席。
|
|
4872
|
+
*
|
|
4873
|
+
* **拿不准时是 `false`**:假 `true` 的代价是调用方带着一个已经失效的约束接着跑,
|
|
4874
|
+
* 判据全文在 `core/src/provider/types.ts` 的 `GenerateOutput.responseFormatApplied` 上。
|
|
4875
|
+
*/
|
|
4876
|
+
responseFormatApplied?: boolean;
|
|
4877
|
+
/** provider 对这一次请求的抱怨,判别联合原样透出(一条都没有时整个缺席) */
|
|
4878
|
+
warnings?: ProviderWarning[];
|
|
4879
|
+
}
|
|
4880
|
+
/** 宿主的一次性补全出口。**只有一个动词** —— 这条路能做的事就只有「打一次」 */
|
|
4881
|
+
interface UtilityControl {
|
|
4882
|
+
generate(input: UtilityGenerateInput): Promise<UtilityGenerateResult>;
|
|
4883
|
+
}
|
|
4884
|
+
/**
|
|
4885
|
+
* 这一次的花费记在 `~/.epoch/budget.json` 的哪一行。
|
|
4886
|
+
*
|
|
4887
|
+
* ```
|
|
4888
|
+
* key = sessionId ?? `utility:${sanitize(label) || 'anonymous'}`
|
|
4889
|
+
* ```
|
|
4890
|
+
*
|
|
4891
|
+
* **导出它是为了宿主别自己拼这个字符串**(判据同 `EpochRuntime.artifactsRoot`
|
|
4892
|
+
* 不让宿主 `join(homeDir, 'artifacts')`):想在那份 JSON 里找到自己那一行,
|
|
4893
|
+
* 问这个函数;哪天前缀改了,照着函数读的宿主什么都不用动。
|
|
4894
|
+
*
|
|
4895
|
+
* 三条刻意的取舍:
|
|
4896
|
+
*
|
|
4897
|
+
* 1. **`label` 不合规就整格丢掉,不做「悄悄改一改」。** 大写字母不 lowercase、
|
|
4898
|
+
* 非法字符不删除 —— 因为这一格的全部价值是「宿主能在文件里找到自己那一行」,
|
|
4899
|
+
* 悄悄换掉之后宿主传的那一格和盘上那一行**对不上**,而它看起来完全正常。
|
|
4900
|
+
* 整格丢掉的表现相反:所有不合规的标注塌成同一行 `utility:anonymous`,
|
|
4901
|
+
* 那是一个看得见的信号。**两种都丢信息,只有一种丢得出声。**
|
|
4902
|
+
* 2. **空串 / 全是空白的 `sessionId` 不算「一个会话」。** `??` 只挡 `null`
|
|
4903
|
+
* 和 `undefined`,`'' ?? x` 是 `''` —— 不判一次就会在账本里落一行键为空串的
|
|
4904
|
+
* 记录,而它既不是任何会话,也不在 `utility:` 那一族里,谁都认不领;
|
|
4905
|
+
* 3. **`sessionId` 本身不收窄。** 键是任意字符串(`Record<string, PersistedBudget>`),
|
|
4906
|
+
* 而这一格的语义是「照你说的那个会话记」—— 替宿主改掉它等于记到另一段会话
|
|
4907
|
+
* 头上,那比记错一行标注严重得多。
|
|
4908
|
+
*/
|
|
4909
|
+
declare function utilityBudgetKey(input: {
|
|
4910
|
+
sessionId?: string;
|
|
4911
|
+
label?: string;
|
|
4912
|
+
}): string;
|
|
4913
|
+
|
|
4914
|
+
/**
|
|
4915
|
+
* `@文件` 补全在运行时那一侧的收窄面(方案 63)—— `EpochRuntime.workspaceFiles`。
|
|
4916
|
+
*
|
|
4917
|
+
* ## 和 `sessionReferences` 是同一件事的另一半
|
|
4918
|
+
*
|
|
4919
|
+
* 那一格(方案 53)让 web 能列候选、解析、读当前面,而**不让服务端自己去读
|
|
4920
|
+
* `sessions.db`**;这一格让 web 能列文件候选、解析路径、读正文,而**不让服务端
|
|
4921
|
+
* 自己去 `readFileSync`**。两条路收窄的是同一个东西:`@` 存在的前提是「它永远
|
|
4922
|
+
* 不比 `file_read` 宽松」,而判定只有一份 —— 权限层那一份。
|
|
4923
|
+
*
|
|
4924
|
+
* ## 收窄了哪几样
|
|
4925
|
+
*
|
|
4926
|
+
* 1. **候选清单只交出「`@` 表达得出来的路径」**(`isMentionableFilePath` 判的),
|
|
4927
|
+
* 而且**过滤和去尾都在这一侧做**。判据同会话那一侧的「过滤在 SQL 里」:
|
|
4928
|
+
* 候选被递到浏览器之前必须已经是「提交时会被认出来」的那一批,否则面板让你
|
|
4929
|
+
* 选一条提交时服务端不认的 —— 而屏幕上没有任何异常。
|
|
4930
|
+
* ⚠️ 被挡掉的那几条**不是悄悄丢掉**:数出来那个数要递到面板上
|
|
4931
|
+
* (`WireFileCandidatesResponse.unmentionable`)。沉默截断是这个仓库反复在骂
|
|
4932
|
+
* 的病(TUI 那侧 40 条池子的账记在方案 53 验收记录第六章)。
|
|
4933
|
+
* 2. **读正文 = 边界 + 权限 + 二进制,一趟走完**(`readFile` 里)。跑出工作区的
|
|
4934
|
+
* 路径和权限不放行是**同一个码 `denied`**(core 那条路也是 —— 部件上都记
|
|
4935
|
+
* `omitted: 'denied'`),读不到 / 是二进制各有各的码,见 {@link FileReadOutcome}。
|
|
4936
|
+
* 3. **权限判定和 `file_read` 同一条**(`toolName: 'file_read'`,
|
|
4937
|
+
* 判据逐字同 `core/src/context/mentions.ts` 的 `readOne` —— 审批缓存按工具名
|
|
4938
|
+
* 做 key,写别的名字等于给 `@` 开一份独立的授权账本)。放行才 `readFileSync`,
|
|
4939
|
+
* 而且**只回文本**:二进制探测在前 8KB 上做(判据同 `file_read` 那一条),
|
|
4940
|
+
* 不让一个 100MB 的 mp4 为了确认自己是二进制而整个进内存。
|
|
4941
|
+
*
|
|
4942
|
+
* ## 这一份是**镜像**,不是「把 core 的类型导出来」
|
|
4943
|
+
*
|
|
4944
|
+
* 三个函数和 core 的 `WorkspaceFiles` / `resolveInWorkspace` / `readOne` 逐个
|
|
4945
|
+
* 对应,但 `listFiles` / `resolveInWorkspace` 是**装配时递进来的**(core 的实现),
|
|
4946
|
+
* 这个文件一行都不 import core —— 于是它能像别的 view 一样拿假依赖直接单测,
|
|
4947
|
+
* 而 `check-layers.mjs` 那张表上 runtime → core 的方向照旧只有装配那一处。
|
|
4948
|
+
*
|
|
4949
|
+
* ## 候选清单的缓存:一次 git 的代价换每次按键
|
|
4950
|
+
*
|
|
4951
|
+
* 补全面板是**边打边刷**的,而 `listWorkspaceFiles` 走 `git ls-files` 要 spawn,
|
|
4952
|
+
* 一次几十毫秒。清单**按工作区根缓存**:根变了立刻重算(换会话换根是常态),
|
|
4953
|
+
* 没变就 60 秒才重算一次(同 core 那份文件头的判据:不做文件监听,
|
|
4954
|
+
* 「用户敲 `@` 的那一刻」不值得为它养一个 watcher;过期的代价只是少一个新文件,
|
|
4955
|
+
* 路径照样可以手打)。
|
|
4956
|
+
*
|
|
4957
|
+
* ## 排序:**仓库根那一层的文件在最上面**
|
|
4958
|
+
*
|
|
4959
|
+
* 打 `@` 之后第一屏列的是「所有目录的第一层 + 各自最浅的子文件」——
|
|
4960
|
+
* 和人对「这个仓库长什么样」的第一印象同一个顺序。粗径深排是这一格自己的决定
|
|
4961
|
+
* (core 那份清单是按路径字典序的,而补全面板卖的是「一眼看过去像这个仓库」)。
|
|
4962
|
+
* 稳定起见相等时退回字典序。
|
|
4963
|
+
*/
|
|
4964
|
+
|
|
4965
|
+
/** 一次候选查询的结果(要上 `WireFileCandidatesResponse` 的那三个数都在这儿) */
|
|
4966
|
+
interface FileCandidatesResult {
|
|
4967
|
+
candidates: string[];
|
|
4968
|
+
/** 匹配上了、但 `@` 表达不出来因而**没进清单**的条数(判据见文件头第 1 条) */
|
|
4969
|
+
unmentionable: number;
|
|
4970
|
+
/** 底层清单撞了 `MAX_FILES` 上限,清单之外的文件没参与匹配 */
|
|
4971
|
+
truncated: boolean;
|
|
4972
|
+
}
|
|
4973
|
+
/**
|
|
4974
|
+
* 装配时要递进来的 core 那一份。
|
|
4975
|
+
*
|
|
4976
|
+
* 写成**两个函数**而不是一个对象:这一格只要这两样,而它俩今天都住在 core 的
|
|
4977
|
+
* 两个不同文件里 —— 合成一个对象的话,宿主将来想自己供一份(比如内存文件树)
|
|
4978
|
+
* 就得把两个不相关的东西绑在一起。
|
|
4979
|
+
*/
|
|
4980
|
+
interface WorkspaceFilesDeps {
|
|
4981
|
+
listFiles(root: string): {
|
|
4982
|
+
files: string[];
|
|
4983
|
+
truncated: boolean;
|
|
4984
|
+
};
|
|
4985
|
+
resolveInWorkspace(root: string, userPath: string): string | null;
|
|
4986
|
+
}
|
|
4987
|
+
/**
|
|
4988
|
+
* 读一次文件的结果。
|
|
4989
|
+
*
|
|
4990
|
+
* **三档失败各有一个稳定的码**,而不是一个笼统的 null —— 调用方(web 那条路的
|
|
4991
|
+
* 收据)要按码分档说话:「被权限拦了」和「这是个二进制」和「读不到」是三种
|
|
4992
|
+
* 完全不同的交代。码和 core 的 `FileOmitReason` 对齐(去掉 `over-limit` /
|
|
4993
|
+
* `not-restored` 那两档 —— 它们不是「读」这件事的答案,是装配层的)。
|
|
4994
|
+
*/
|
|
4995
|
+
type FileReadOutcome = {
|
|
4996
|
+
ok: true;
|
|
4997
|
+
text: string;
|
|
4998
|
+
bytes: number;
|
|
4999
|
+
} | {
|
|
5000
|
+
ok: false;
|
|
5001
|
+
reason: 'denied' | 'unreadable' | 'binary';
|
|
5002
|
+
bytes?: number;
|
|
5003
|
+
};
|
|
5004
|
+
/**
|
|
5005
|
+
* `EpochRuntime.workspaceFiles` 在服务端眼里的样子 —— **三个方法,一个不多**。
|
|
5006
|
+
*
|
|
5007
|
+
* ⚠️ 每一次调用都收 `root`,**不许在门面上存一个「当前工作区」**:这个门面是
|
|
5008
|
+
* **进程级**的(像 `permission`),而工作区**一个会话绑一个**(决定 18)。存根的话,
|
|
5009
|
+
* 第二个会话拿到的是第一个会话的文件树 —— 编译得过、跑得通、只是答错会话
|
|
5010
|
+
* (同 `EpochRuntime.checkpoints` 那段 ⚠️ 骂的那件事)。
|
|
5011
|
+
*/
|
|
5012
|
+
interface WorkspaceFilesView {
|
|
5013
|
+
/**
|
|
5014
|
+
* 补全面板的候选(方案 63)。
|
|
5015
|
+
*
|
|
5016
|
+
* @param root 工作区根(`projectContext.rootDir`,由调用方按会话现算)。
|
|
5017
|
+
* **不是启动目录** —— 判据同 `visibilityOf` 的第 2 条。
|
|
5018
|
+
* @param query 正在打的那一截。**空串 = 刚敲下 `@`**,那一档要列最近的一屏。
|
|
5019
|
+
* @param limit 一屏列多少条。**过滤和截断都发生在它之前**,所以它不是
|
|
5020
|
+
* 「能搜到哪几条」的上限 —— 判据同会话那一侧的 `MAX_CANDIDATES`。
|
|
5021
|
+
* @returns `null` = 这个工作区连清单都列不出来(没有 .git 也不是目录)。
|
|
5022
|
+
* 和「一条候选都没有」必须分开:后者是一句真话(空仓库),前者是
|
|
5023
|
+
* 「答不出来」。
|
|
5024
|
+
*/
|
|
5025
|
+
candidates(query: string, limit: number, root: string): FileCandidatesResult | null;
|
|
5026
|
+
/**
|
|
5027
|
+
* 读一个文件的文本正文。
|
|
5028
|
+
*
|
|
5029
|
+
* **判定和 `file_read` 同源**(`toolName: 'file_read'`,见文件头第 3 条),
|
|
5030
|
+
* 而且不放行时**不弹审批** —— 判据同 core 那条路的「没放行时不弹审批,
|
|
5031
|
+
* 只是不附」。失败的三档码见 {@link FileReadOutcome},调用方按码分档说。
|
|
5032
|
+
*
|
|
5033
|
+
* `bytes` 是文件的**原始字节数**(成功和二进制那两档都带):调用方拿它和
|
|
5034
|
+
* 附件预算比,决定要不要截、截多少 —— 算术在 protocol 那张上限表,
|
|
5035
|
+
* 这一格不替调用方算(它不知道这条消息已经用掉多少预算)。
|
|
5036
|
+
*/
|
|
5037
|
+
readFile(path: string, root: string): FileReadOutcome;
|
|
5038
|
+
}
|
|
5039
|
+
/**
|
|
5040
|
+
* 建那个收窄面。
|
|
5041
|
+
*
|
|
5042
|
+
* @param deps core 递来的两样(装配那一行从 `@epoch-agent/core` import)。
|
|
5043
|
+
* @param permission 权限管理器;**可为 null** —— 那时的语义同 core 那条路的
|
|
5044
|
+
* 判据:权限模块没起来,模型调 `file_read` 照样读得到,
|
|
5045
|
+
* `@` 单方面收紧只会让两条路给出不一致的答案。
|
|
5046
|
+
*/
|
|
5047
|
+
declare function buildWorkspaceFilesControl(deps: WorkspaceFilesDeps, permission: PermissionManager | null): WorkspaceFilesView;
|
|
5048
|
+
|
|
3849
5049
|
/**
|
|
3850
5050
|
* buildRuntime() —— composition root。
|
|
3851
5051
|
*
|
|
@@ -3915,11 +5115,31 @@ declare function createUserActions(deps: UserShellDeps): UserActions;
|
|
|
3915
5115
|
* - ③ 新用例必须红,**而且红的是断言不是崩**。崩说明它撞在别的东西上,
|
|
3916
5116
|
* 而那个东西哪天变了它就不再守这一格了。
|
|
3917
5117
|
*
|
|
3918
|
-
* ##
|
|
5118
|
+
* ## 转出格那一层:别去发明一个自动检查
|
|
3919
5119
|
*
|
|
3920
5120
|
* 「每个转出格都要有对应用例」这种扫描会立刻变成一个人人绕着走的形式主义,
|
|
3921
5121
|
* 而且「哪几格该有」本身是判断(下面这条排序就是那个判断)。
|
|
3922
5122
|
*
|
|
5123
|
+
* ## ⚠️ 但**子系统**那一层有一条,2026-08-25 加的
|
|
5124
|
+
*
|
|
5125
|
+
* 上面那句话说的是**转出格**(可选参数、数组元素、`cleanups.push(...)`)——
|
|
5126
|
+
* 63 个格子,哪几格该有用例是判断。它**不覆盖**另一个窄得多的集合:
|
|
5127
|
+
* 这个文件里每一处 `wire*` / `register*` 调用(今天 15 处)。那些不是转出格,
|
|
5128
|
+
* 每一处的语义都是同一句话 —— **一整个子系统在这里接进来**。这个集合是结构性
|
|
5129
|
+
* 命名的、封闭的、名字自己认得出来的,「该不该有装配用例」上没有判断余地。
|
|
5130
|
+
*
|
|
5131
|
+
* 于是 `__tests__/assembly-wiring-sites.test.ts` 拿 TS 的 parser 扫这个文件,
|
|
5132
|
+
* 三个方向都断:清单外没有新的装配点 / 清单里每一格今天仍然在 / 点名的那份用例
|
|
5133
|
+
* **必须真的调 `buildRuntime()`**(拿一份只调那个函数的单元用例来交差会红)。
|
|
5134
|
+
* 它接的是三次同形状的事故:44 §4.3 ①(摘掉插件那一批,6586 条 0 红)、
|
|
5135
|
+
* 59 §十 #2(删 `describeTarget` 转发行,820 条 0 红)、59 §十 #3(注册那行挪
|
|
5136
|
+
* 位置,0 红)。那一轮顺带补出两个当时还空着的格子:`wireWorkspace`
|
|
5137
|
+
* (`--add-dir` 的额外根没告诉权限层)和 `reloadRoles` 里那次 `wireAgentRoles`
|
|
5138
|
+
* (界面上新建的身份要到下次启动才认得)。
|
|
5139
|
+
*
|
|
5140
|
+
* **加一处 `wireXxx()` / `registerXxx()` 就得同时配一条装配用例**,
|
|
5141
|
+
* 否则那份点名册会红。转出格那一层照旧按下面这条排序人工补。
|
|
5142
|
+
*
|
|
3923
5143
|
* ## 排序依据:按「坏了看不出来」补,不按「格数」补
|
|
3924
5144
|
*
|
|
3925
5145
|
* 转出面上的格子补不完,所以顺序是判断的一部分。从上到下:
|
|
@@ -4088,7 +5308,8 @@ interface BuildRuntimeOptions {
|
|
|
4088
5308
|
*/
|
|
4089
5309
|
hostPreset?: HostPreset;
|
|
4090
5310
|
/**
|
|
4091
|
-
* 宿主注入的能力清单 —— 它自己 ship 的**专家 / 技能 /
|
|
5311
|
+
* 宿主注入的能力清单 —— 它自己 ship 的**专家 / 技能 / 连接器 / 工具**
|
|
5312
|
+
* (方案 44 §2.1、方案 59 §四)。
|
|
4092
5313
|
*
|
|
4093
5314
|
* ```ts
|
|
4094
5315
|
* await buildRuntime({
|
|
@@ -4098,6 +5319,7 @@ interface BuildRuntimeOptions {
|
|
|
4098
5319
|
* roles: [{ name: 'triage', description: '分拣工单,只读', tools: ['file_read'] }],
|
|
4099
5320
|
* skillDirs: [join(app.getAppPath(), 'resources', 'skills')],
|
|
4100
5321
|
* mcpServers: [{ name: 'github', transport: 'http', url: 'http://127.0.0.1:3456/mcp' }],
|
|
5322
|
+
* tools: [listJobsTool], // name: 'list_jobs' → 模型看到的是 `acme__list_jobs`
|
|
4101
5323
|
* },
|
|
4102
5324
|
* });
|
|
4103
5325
|
* ```
|
|
@@ -4114,15 +5336,20 @@ interface BuildRuntimeOptions {
|
|
|
4114
5336
|
* 1. **命名空间是强制的** —— 前缀由装配层按 `namespace` 拼,不由宿主在每条数据
|
|
4115
5337
|
* 上自己写。不这么做的话,一个叫 `general` 的注入会顶掉 `BUILTIN_ROLES`
|
|
4116
5338
|
* 里那条,于是每一次没指定角色的派活都跑在宿主那份 prompt 上。
|
|
4117
|
-
* ⚠️
|
|
4118
|
-
*
|
|
4119
|
-
*
|
|
5339
|
+
* ⚠️ **四样里 MCP server 和工具那两格拼的是 `<namespace>__` 而不是
|
|
5340
|
+
* `<namespace>:`**,因为它们都会原样变成工具名的一部分发给 provider,
|
|
5341
|
+
* 而带 `:` 的工具名被硬拒(实测,判据全文在 `McpServerStatus.source` 上);
|
|
4120
5342
|
* 2. **来源枚举各加一格**(`AgentRoleSource` / `SkillMeta.scope` 的 `'host'`,
|
|
4121
5343
|
* 外加 `McpServerStatus.source` 这个**新增**字段),界面据此把它们单独分
|
|
4122
|
-
*
|
|
5344
|
+
* 一组、标上来源。工具那一样走的是注册表既有的来源标签
|
|
5345
|
+
* (`registry.sourceOf()` 回 `'host'`),没有新枚举;
|
|
4123
5346
|
* 3. **坏的那一条只跳过它自己**,其余照常进来,各留一条诊断。唯一整份作废的是
|
|
4124
5347
|
* `namespace` 本身坏掉那一档 —— 那时没有前缀,而没有前缀就能顶掉内置角色。
|
|
4125
5348
|
*
|
|
5349
|
+
* ⚠️ **注入的工具不因为跑在同一个进程里就少一道闸**:审批闸门在
|
|
5350
|
+
* `ToolExecutor.execute` 上,它对**任何**来源的工具过同一道 `checkPermission`。
|
|
5351
|
+
* 这不是我们答应的一件事,是那一层压根不知道工具是谁给的(方案 59 §4.2)。
|
|
5352
|
+
*
|
|
4126
5353
|
* **注入是装配时一次性的**(同 `secretStore`):宿主 ship 了什么是它的构建期
|
|
4127
5354
|
* 属性,一个 Electron 应用不会在跑着的时候长出一个新专家。⚠️ 这句话**不等于**
|
|
4128
5355
|
* 「注入进去的东西是死的」—— 宿主注入的技能照样被 `SkillLearner` 影响,
|
|
@@ -4131,6 +5358,34 @@ interface BuildRuntimeOptions {
|
|
|
4131
5358
|
* 不给这个选项时**一行都不跑**,连一条诊断都不多。
|
|
4132
5359
|
*/
|
|
4133
5360
|
hostCapabilities?: HostCapabilities;
|
|
5361
|
+
/**
|
|
5362
|
+
* 宿主预置的**插件市场** —— 「宿主替用户装一个扩展物」那条路(方案 59 §六 E1)。
|
|
5363
|
+
*
|
|
5364
|
+
* ```ts
|
|
5365
|
+
* await buildRuntime({
|
|
5366
|
+
* homeDir,
|
|
5367
|
+
* hostMarketplaces: { sources: [join(app.getAppPath(), 'resources', 'market')] },
|
|
5368
|
+
* });
|
|
5369
|
+
* // → runtime.plugins.search('office') 搜得到,install() 装成软链,全程不联网
|
|
5370
|
+
* ```
|
|
5371
|
+
*
|
|
5372
|
+
* ⚠️ **给了才有 {@link EpochRuntime.plugins}**,不给就是 `null`、一行都不跑。
|
|
5373
|
+
* 它是一个真的新攻击面(往 `~/.epoch/plugins/` 里写东西),所以是**选进来**的,
|
|
5374
|
+
* 不是白送的 —— 同 `installSignalHandlers` 默认 false 那条判据。
|
|
5375
|
+
*
|
|
5376
|
+
* ⚠️ **和 {@link hostCapabilities} 不是一件事**:那一个是「宿主自己 ship 的东西,
|
|
5377
|
+
* 一个字节都不落盘、用户删不掉」;这一个是「宿主给用户一张单子,用户挑一个
|
|
5378
|
+
* **装下来**」—— 装完之后它就是用户的插件,`epoch plugin list` 查得到、
|
|
5379
|
+
* `epoch plugin uninstall` 卸得掉。
|
|
5380
|
+
*
|
|
5381
|
+
* ⚠️ **装完不会立刻生效**(扩展物在启动时加载),要 `dispose()` + 再
|
|
5382
|
+
* `buildRuntime()` 一次。这条不是欠账,是这一轮对外承诺的形态 ——
|
|
5383
|
+
* 判据、以及「为什么重建一次就够」,在
|
|
5384
|
+
* [plugin-control.ts](./plugin-control.ts) 的文件头第二节。
|
|
5385
|
+
*
|
|
5386
|
+
* 远程 source(`github:` / `npm:` / `https:`)**默认关着**,判据同上,第三节。
|
|
5387
|
+
*/
|
|
5388
|
+
hostMarketplaces?: HostMarketplaces;
|
|
4134
5389
|
/**
|
|
4135
5390
|
* 这一程开出来的会话在 `sessions.source` 那一列记成什么(方案 45 §五)。
|
|
4136
5391
|
*
|
|
@@ -4330,6 +5585,26 @@ interface ManagedNote {
|
|
|
4330
5585
|
bypassDisabled: boolean;
|
|
4331
5586
|
}
|
|
4332
5587
|
|
|
5588
|
+
/**
|
|
5589
|
+
* 一次撤销审批缓存的结果(2026-08-27)。
|
|
5590
|
+
*
|
|
5591
|
+
* ⚠️ 两个拒绝码是**两回事,不许合成一个布尔**:
|
|
5592
|
+
*
|
|
5593
|
+
* - `no-permission-layer` —— 这个进程整个没有权限层,也就**没有缓存这回事**。
|
|
5594
|
+
* 宿主该说「这一节不适用」(web 那侧回 503,界面上根本不画这一节);
|
|
5595
|
+
* - `unknown-id` —— 权限层好好的,只是那条不在表里了(另一个界面先撤了,
|
|
5596
|
+
* 或者 `allow-session` 过期被 `prune()` 清掉了)。**这不是错误**:用户要的
|
|
5597
|
+
* 结果已经成立,宿主该做的是把新表画上去,不是报错让他重试。
|
|
5598
|
+
*
|
|
5599
|
+
* 合成一个 `false` 的话,第二档会被画成第一档的样子 —— 屏幕上一句
|
|
5600
|
+
* 「权限层不可用」,而权限层其实好好的。
|
|
5601
|
+
*/
|
|
5602
|
+
type ApprovalRevokeResult = {
|
|
5603
|
+
ok: true;
|
|
5604
|
+
} | {
|
|
5605
|
+
ok: false;
|
|
5606
|
+
reason: 'no-permission-layer' | 'unknown-id';
|
|
5607
|
+
};
|
|
4333
5608
|
/**
|
|
4334
5609
|
* 权限规则的可解释性出口(方案 22 §2.6 / §2.7 / §2.8)。
|
|
4335
5610
|
*
|
|
@@ -4349,6 +5624,22 @@ interface ManagedNote {
|
|
|
4349
5624
|
* 具体代价是**第三步会被漏掉** —— 漏了 `plan.forget()` 的那一侧,用户在 plan
|
|
4350
5625
|
* 模式里换档之后,随后一次计划批准会把级别悄悄改回进入前那一档,
|
|
4351
5626
|
* 而这件事只在「plan 模式 + 中途换档 + 批准计划」三件事凑齐时才看得见。
|
|
5627
|
+
*
|
|
5628
|
+
* ## ⚠️ 第八样 {@link revokeApproval} 也是**写**(2026-08-27),它同样不破那条
|
|
5629
|
+
*
|
|
5630
|
+
* 上面那条否掉的还是「从 UI 改一条**规则**」。**审批缓存不是规则** ——
|
|
5631
|
+
* 它就是那条判据里点名的那个东西(「那正是审批缓存已经在干的事」):
|
|
5632
|
+
* 用户在弹窗上点出来的、只属于这台机器这一程的记忆,不写 git、不该写 git。
|
|
5633
|
+
*
|
|
5634
|
+
* **补它的理由是它今天根本收不回来。** `allow-always` 落
|
|
5635
|
+
* `~/.epoch/approvals.json`,用户至少删得掉那个文件;而 `deny` 只在内存里、
|
|
5636
|
+
* `ttlMs: 0` 永不过期 —— 一次点错的「拒绝」之后,那个工具在这个进程里就死了,
|
|
5637
|
+
* 屏幕上只会一直印「操作被拦截 [权限]: 已缓存拒绝」。判据全文在 core 的
|
|
5638
|
+
* `ApprovalCache.revoke()` 上。
|
|
5639
|
+
*
|
|
5640
|
+
* ⚠️ **它撤的只是缓存那一层。** 规则的 deny(三张列表 / `policies/*.toml`)
|
|
5641
|
+
* 排在缓存**前面**(core 的 `checkRulesAndCache`),这一发一个字都不动它们 ——
|
|
5642
|
+
* 宿主的界面上这两者必须分得开,不然用户点了「撤销」发现还是被拦。
|
|
4352
5643
|
*/
|
|
4353
5644
|
interface PermissionsControl {
|
|
4354
5645
|
/** 当前生效的全部规则,按 deny → ask → allow 排,每条标出来源层 */
|
|
@@ -4399,6 +5690,36 @@ interface PermissionsControl {
|
|
|
4399
5690
|
* 没有权限层就等于什么都没判过,而那句话是真的。
|
|
4400
5691
|
*/
|
|
4401
5692
|
audit: (scope?: PermissionAuditScope) => PermissionAuditSnapshot;
|
|
5693
|
+
/**
|
|
5694
|
+
* 这个进程记下了哪些**弹窗答案**(审批缓存,2026-08-27)。
|
|
5695
|
+
*
|
|
5696
|
+
* ⚠️ **和 {@link audit} 不是同一本账,别拿一个顶另一个。** 流水记「判过什么」
|
|
5697
|
+
* (只增不减的历史,一次判定一行);这张表记「**还记着什么**」(撤一条少一行)。
|
|
5698
|
+
* 同一次「用户点了拒绝」在两处各留一行,而下一步完全不同:流水那行是历史,
|
|
5699
|
+
* 这一行是现在还在拦人的东西。
|
|
5700
|
+
*
|
|
5701
|
+
* ⚠️ **和 {@link rules} 也不是一回事**:那一张是配置文件里写下来的策略
|
|
5702
|
+
* (能提交进 git),这一张是本机这一程的记忆。判定时**规则的 deny 排在缓存
|
|
5703
|
+
* 前面**,所以撤销这里的一条不等于放行 —— 宿主的界面必须说得清这一点。
|
|
5704
|
+
*
|
|
5705
|
+
* 权限层起不来时是**空表**而不是抛错:没有权限层就等于什么都没记过,
|
|
5706
|
+
* 而那句话是真的(同 {@link audit} 那条兜底,两处必须一致)。
|
|
5707
|
+
*/
|
|
5708
|
+
approvals: () => readonly CachedApproval[];
|
|
5709
|
+
/**
|
|
5710
|
+
* 撤销**一条**缓存的决定,判据全文在接口文档最后一节。
|
|
5711
|
+
*
|
|
5712
|
+
* 按 id 不按目标:同一个「工具名 + 目标」上可以躺着不止一条,按目标撤的话
|
|
5713
|
+
* 一次调用会删掉几条而调用方说不出是哪几条 —— 界面上那一行「撤销」
|
|
5714
|
+
* 就成了一个作用范围不确定的按钮。id 是 {@link approvals} 每一行自己带的。
|
|
5715
|
+
*
|
|
5716
|
+
* ⚠️ **没有「全清」那一档,而 core 那边 `ApprovalCache.clear()` 一直都在。**
|
|
5717
|
+
* 不转出来是刻意的:那一下会把用户攒了一整程的「总是允许」一起清掉,
|
|
5718
|
+
* 于是「收回一个误点」的代价变成「后面每一次都重新问一遍」。
|
|
5719
|
+
* 真要那一档时它是另一个方法(另一个动词、宿主另一次确认),
|
|
5720
|
+
* 不是这个方法多一个参数。
|
|
5721
|
+
*/
|
|
5722
|
+
revokeApproval: (id: string) => ApprovalRevokeResult;
|
|
4402
5723
|
/**
|
|
4403
5724
|
* 切到某一档。**这是宿主改权限档位的唯一入口**(见接口文档最后一节)。
|
|
4404
5725
|
*
|
|
@@ -4450,7 +5771,12 @@ interface PermissionsControl {
|
|
|
4450
5771
|
interface ToolGateRow extends ToolGate {
|
|
4451
5772
|
name: string;
|
|
4452
5773
|
description: string;
|
|
4453
|
-
/**
|
|
5774
|
+
/**
|
|
5775
|
+
* `builtin` / `plugin-file` / `mcp:<服务器名>` / `host`,同 {@link EpochRuntime.tools}。
|
|
5776
|
+
*
|
|
5777
|
+
* 最后那一格是宿主经 `hostCapabilities.tools` 注入的进程内工具(方案 59 §四)——
|
|
5778
|
+
* 它在这张表上**和别人一档**,那正是它该在的位置:闸门不看来源。
|
|
5779
|
+
*/
|
|
4454
5780
|
source: string;
|
|
4455
5781
|
/**
|
|
4456
5782
|
* 命中的那条规则来自哪一层(`user` / `project` / …)。
|
|
@@ -4610,6 +5936,22 @@ interface SessionDeletion {
|
|
|
4610
5936
|
* (对话已经没了),但也不能装作全干净了 —— 残留目录会一直占着磁盘。
|
|
4611
5937
|
*/
|
|
4612
5938
|
checkpointsRemoved: boolean;
|
|
5939
|
+
/**
|
|
5940
|
+
* `~/.epoch/artifacts/<id>/` 也清掉了(方案 47 PR-4,2026-08-20)。
|
|
5941
|
+
* 本来就没有产物时同样是 `true` —— 绝大多数会话就是这一档。
|
|
5942
|
+
*
|
|
5943
|
+
* 单独一格而不是和 `checkpointsRemoved` 合成一个「附属物清干净了」:
|
|
5944
|
+
* 两个存储的失败原因不一样(检查点是一堆小 blob,artifact 可能是一个正被
|
|
5945
|
+
* 别的进程按着的大文件),合成一格之后界面只能说「有东西没删掉」,
|
|
5946
|
+
* 而用户下一步该去哪个目录看,那句话答不了。
|
|
5947
|
+
*
|
|
5948
|
+
* ⚠️ **这一格没有进 `WireDeleteSessionResponse`**:protocol 那份契约这一轮
|
|
5949
|
+
* 一个字没动(并行分支正占着 protocol / server / web 三个目录)。它照样会随
|
|
5950
|
+
* JSON 出门 —— `conformsToWire()` 只查必填字段在不在、**不查多余字段**
|
|
5951
|
+
* (判据逐字写在 `wireFields()` 上:响应里多一个字段是向前兼容的正常演进)。
|
|
5952
|
+
* 要让 Web 界面**读**它,得先往那份契约上加一格。
|
|
5953
|
+
*/
|
|
5954
|
+
artifactsRemoved: boolean;
|
|
4613
5955
|
}
|
|
4614
5956
|
/** 会话的列举、检索、恢复与增删改(方案 25 PR-4;方案 30 补了后三个) */
|
|
4615
5957
|
interface SessionControl {
|
|
@@ -4626,9 +5968,10 @@ interface SessionControl {
|
|
|
4626
5968
|
*/
|
|
4627
5969
|
rename: (sessionId: string, title: string) => SessionSummary | null;
|
|
4628
5970
|
/**
|
|
4629
|
-
*
|
|
5971
|
+
* 真删一段会话,**连带删掉它的检查点和 artifact**(方案 30 §2.2 +
|
|
5972
|
+
* 方案 27 的交接 + 方案 47 PR-4)。
|
|
4630
5973
|
*
|
|
4631
|
-
* 不可撤销 ——
|
|
5974
|
+
* 不可撤销 —— 二次确认是宿主的事。异步是因为那两处都在文件系统上。
|
|
4632
5975
|
*/
|
|
4633
5976
|
delete: (sessionId: string) => Promise<SessionDeletion>;
|
|
4634
5977
|
/**
|
|
@@ -4673,6 +6016,25 @@ interface EpochRuntime {
|
|
|
4673
6016
|
* [session-factory.ts](./session-factory.ts) 的文件头,**别在别处抄第二份**。
|
|
4674
6017
|
*/
|
|
4675
6018
|
sessionFactory: SessionFactory;
|
|
6019
|
+
/**
|
|
6020
|
+
* 进程里所有会话产出的每一帧 `AgentEvent`(方案 61)。**不可为 null** ——
|
|
6021
|
+
* 它不依赖任何一样装配得起来的东西,provider 没起来时它照样在(那时它只是
|
|
6022
|
+
* 一帧都不会有,那是一句真话)。
|
|
6023
|
+
*
|
|
6024
|
+
* 在它之前,事件没有「存在」这个概念,只有「被某个 for-await 吃掉了」:
|
|
6025
|
+
* `run()` 是单消费者流,三条消费路各自独占一个 for-await。于是宿主自己驱动
|
|
6026
|
+
* `session.run()` 时,**同进程的其它部件(诊断面板、审计、埋点)一帧都旁听不到**
|
|
6027
|
+
* —— 方案 54 §三那条 `observe()` 只挂在 server hub 的广播层上,够不着这一半。
|
|
6028
|
+
*
|
|
6029
|
+
* ⚠️ **进程级,不是这份 runtime 级**:同一个进程里建两份 runtime,
|
|
6030
|
+
* 两个 `events` 是**同一个对象**,订阅者会看到另一份 runtime 的会话事件。
|
|
6031
|
+
* 这是有意的 —— 「任何会话一帧都没有」这句话只有进程级的总线答得出来。
|
|
6032
|
+
* 判据全文在 [events.ts](./events.js) 文件头。
|
|
6033
|
+
*
|
|
6034
|
+
* 用法与三条禁令(不许调 `respond`、不许在回调里干重活、不许指望顺序之外的
|
|
6035
|
+
* 任何保证)见 [docs/EMBEDDING.md](../../../docs/EMBEDDING.md) §13.5。
|
|
6036
|
+
*/
|
|
6037
|
+
events: AgentEventBus;
|
|
4676
6038
|
/**
|
|
4677
6039
|
* 会话持久化;SQLite 起不来时为 null。
|
|
4678
6040
|
*
|
|
@@ -4687,6 +6049,33 @@ interface EpochRuntime {
|
|
|
4687
6049
|
* 而那正是方案 20 §缺口 1 花一整条线修掉的东西。
|
|
4688
6050
|
*/
|
|
4689
6051
|
sessionStore: SessionStore | null;
|
|
6052
|
+
/**
|
|
6053
|
+
* `@:` 引用会话那一侧(方案 53);SQLite 起不来时为 null。
|
|
6054
|
+
*
|
|
6055
|
+
* 摘出来是给**宿主的输入框**用:`@` 提及的解析(`resolveMentions`)要过
|
|
6056
|
+
* 可读性判定,而那条判定在 core 里 —— 让 TUI / web 自己去读 `sessions.db`
|
|
6057
|
+
* 等于给 `@:` 开一条绕过判定的读取通道(同 `resolveMentions` 不让 TUI
|
|
6058
|
+
* 自己 `readFileSync` 那条判据)。
|
|
6059
|
+
*
|
|
6060
|
+
* 和 {@link sessionStore} 分开摆而不是让宿主从那儿翻:前者是「回放旧会话」
|
|
6061
|
+
* 的读路(给 web 的详情页),这一份是「把另一段会话塞进这一条消息」的
|
|
6062
|
+
* 收窄面 —— 它只交出候选 / 解析 / 当前面三件事,交不出任意 SQL。
|
|
6063
|
+
*/
|
|
6064
|
+
sessionReferences: SessionReferences | null;
|
|
6065
|
+
/**
|
|
6066
|
+
* `@文件` 补全与解析(方案 63)。**不可为 null** —— 它不依赖任何会起不来的
|
|
6067
|
+
* 东西:底下的清单 `git ls-files` 失败会退到 walk,读文件失败回 null。
|
|
6068
|
+
* 权限层缺席时它照样在,只是 `readFile` 跳过那道判定 —— 判据逐字同 core 的
|
|
6069
|
+
* `resolveMentions`:权限模块没起来时模型调 `file_read` 照样读得到,`@` 单方面
|
|
6070
|
+
* 收紧只会让两条路给出不一致的答案。
|
|
6071
|
+
*
|
|
6072
|
+
* 和 {@link sessionReferences} 是同一件事的另一半:那一份让宿主列会话候选、
|
|
6073
|
+
* 读当前面而**不交出任意 SQL**;这一份让宿主列文件候选、读正文而**不交出任意
|
|
6074
|
+
* `readFileSync`**。收窄的东西是同一句话 —— `@` 永远不比 `file_read` 宽松,
|
|
6075
|
+
* 而判定只有权限层那一份(`toolName: 'file_read'`,判据全文在
|
|
6076
|
+
* [workspace-files.ts](./workspace-files.js) 的文件头)。
|
|
6077
|
+
*/
|
|
6078
|
+
workspaceFiles: WorkspaceFilesView;
|
|
4690
6079
|
/**
|
|
4691
6080
|
* **引导会话**的检查点与回退(方案 27)。**不可为 null** —— 它只是文件系统上的
|
|
4692
6081
|
* 一个目录,不像 provider / SQLite 那样有起不来的可能。
|
|
@@ -4830,6 +6219,17 @@ interface EpochRuntime {
|
|
|
4830
6219
|
* deny 时还能带一句自定义的话)。
|
|
4831
6220
|
*/
|
|
4832
6221
|
policy: PolicyStatus;
|
|
6222
|
+
/**
|
|
6223
|
+
* 这一次读了哪几份 hook 配置、每份贡献了几条、项目那几份是不是因为不受信任
|
|
6224
|
+
* 整份没加载(方案 50 PR-3)。
|
|
6225
|
+
*
|
|
6226
|
+
* 和 {@link policy} 摊在一屏里要说清区别:策略规则管「这个工具碰这个路径
|
|
6227
|
+
* 算什么」,hook 是**会 spawn 子进程的命令**。而这一份现在还要答一个更基本的
|
|
6228
|
+
* 问题 —— 方案 50 之后 hook 可能来自 Claude Code 的三份配置里的任意一份,
|
|
6229
|
+
* 甚至来自**仓库作者**写给 Claude Code 的那一份,于是
|
|
6230
|
+
* 「我没写过这条命令,它为什么会跑」需要一个能指到文件的答案。
|
|
6231
|
+
*/
|
|
6232
|
+
hook: HookStatus;
|
|
4833
6233
|
/**
|
|
4834
6234
|
* 对模型可见的工具清单(名字 + 一行说明)。
|
|
4835
6235
|
*
|
|
@@ -4915,6 +6315,23 @@ interface EpochRuntime {
|
|
|
4915
6315
|
* 在 [model-catalog.ts](./model-catalog.ts) 的文件头。
|
|
4916
6316
|
*/
|
|
4917
6317
|
modelCatalog: ModelCatalogControl;
|
|
6318
|
+
/**
|
|
6319
|
+
* 一次性补全(方案 59 需求 B)—— 不建会话、不落历史、不装 system prompt、
|
|
6320
|
+
* **不带工具**。provider 起不来时为 null(同 {@link model})。
|
|
6321
|
+
*
|
|
6322
|
+
* 它答的是宿主那批「把这段表格格式化成 JSON」的杂活:在这一格之前他们唯一的路
|
|
6323
|
+
* 是自己 `new` 一个第二个 provider 实例,而那个实例有自己的工厂缓存、自己的
|
|
6324
|
+
* 失败节流器、自己的通知队列(没人取 = 漏),凭据链还得自己复刻一遍。
|
|
6325
|
+
*
|
|
6326
|
+
* ⚠️ **它挂的是引导会话那一份 router**,所以第二个会话 `/model` 换过之后,
|
|
6327
|
+
* 这条路跟的仍然是引导会话那个选择。要打别的就显式给 `model`。
|
|
6328
|
+
*
|
|
6329
|
+
* ⚠️ **`sessionId` 这一格有后果**:给了它,这笔花费就去撞那个会话自己的
|
|
6330
|
+
* `budget.maxCostUsd` —— 撞上了那个会话下一轮被拦。不给它则落
|
|
6331
|
+
* `utility:<label>` 保留桶,而那个桶**今天没有任何上界**。
|
|
6332
|
+
* 两条判据全文在 [utility.ts](./utility.ts) 的文件头,别在别处抄第二份。
|
|
6333
|
+
*/
|
|
6334
|
+
utility: UtilityControl | null;
|
|
4918
6335
|
/**
|
|
4919
6336
|
* 可派给子 agent 的角色表(方案 18)。provider 起不来时是空数组 ——
|
|
4920
6337
|
* 没有 provider 就没有 `delegate_task`,角色也就无处可派。
|
|
@@ -4959,6 +6376,27 @@ interface EpochRuntime {
|
|
|
4959
6376
|
* 这里)。嵌入宿主要整份就该拿到整份。
|
|
4960
6377
|
*/
|
|
4961
6378
|
skillBody(name: string): string | null;
|
|
6379
|
+
/**
|
|
6380
|
+
* 每条技能**此刻进没进** `<available_skills>`,名字 → 三档(2026-08-27)。
|
|
6381
|
+
*
|
|
6382
|
+
* ## 它答的是 web 能力页那一栏行尾那个数到底还算不算数
|
|
6383
|
+
*
|
|
6384
|
+
* 那一栏印着「这条技能每一轮的常驻开销」(`WireSkillSummary.tokens`)。
|
|
6385
|
+
* 那句话在两道口子都关着时是真的,而它们**各自**都能让它对被排除的那几条
|
|
6386
|
+
* 变成假话:`skills.maxIndexed` 封了顶(进程级),或者这段会话的身份带着
|
|
6387
|
+
* `AgentRole.skills` 白名单(会话级)。记账全文在
|
|
6388
|
+
* [RECORD-skill-index-narrowing](../../../docs/verify/VERIFY_RECORD-skill-index-narrowing.md) §四第 1 条。
|
|
6389
|
+
*
|
|
6390
|
+
* @param role **这段会话的底座身份名**(`LiveSession.role?.name`),没绑就是
|
|
6391
|
+
* `null`。⚠️ 不是那枚 chip 挑的逐条身份 —— 「常驻」这个词的宾语是整段会话,
|
|
6392
|
+
* 而 chip 只活一个轮次(两者的差别逐字在 `LiveSession.role` 那张表上)。
|
|
6393
|
+
* 名字认不出来(角色表热加载过、那份 md 被删了)时按**不收窄**算:
|
|
6394
|
+
* 那时引擎自己也退回会话默认角色跑,报一句「被身份挡了」是假话。
|
|
6395
|
+
*
|
|
6396
|
+
* 技能系统起不来时是空 Map。**那不是「全都没进」** —— 那时 {@link skills}
|
|
6397
|
+
* 本身就是空数组,一格都不会被查到(判据同它为什么是空数组而不是 null)。
|
|
6398
|
+
*/
|
|
6399
|
+
skillIndexResidency(role: string | null): ReadonlyMap<string, SkillIndexResidency>;
|
|
4962
6400
|
/**
|
|
4963
6401
|
* 从**本机目录**导入技能(方案 42 §六,2026-08-18)。**不可为 null** ——
|
|
4964
6402
|
* 技能系统没起来时它照样在,只是每次都回 `missing`,而那是一句真话
|
|
@@ -4981,6 +6419,25 @@ interface EpochRuntime {
|
|
|
4981
6419
|
* 判据在 [role-write.ts](./role-write.ts) 文件头第二节。
|
|
4982
6420
|
*/
|
|
4983
6421
|
roleWrite: RoleWriteControl;
|
|
6422
|
+
/**
|
|
6423
|
+
* 插件的宿主控制面(方案 59 §六 E1):列 / 搜 / 预览 / 装 / 更新 / 卸。
|
|
6424
|
+
*
|
|
6425
|
+
* ⚠️ **没给 {@link BuildRuntimeOptions.hostMarketplaces} 时为 `null`**,而这一格
|
|
6426
|
+
* 的可空和 `model` / `memory` 那几格**不同档**:那几个是「底下那层起不来了」,
|
|
6427
|
+
* 这一个是「宿主没打算给用户这条路」。判据是它真的是一个新口子(往
|
|
6428
|
+
* `~/.epoch/plugins/` 里落东西、装完的插件下一程会被加载),
|
|
6429
|
+
* 而一个没预置任何市场的宿主,凭空多一个能装插件的方法只有坏处。
|
|
6430
|
+
*
|
|
6431
|
+
* ⚠️ **装完不会立刻生效** —— 装 / 更新 / 卸之后
|
|
6432
|
+
* `plugins.pendingRestart` 变 `true`,宿主要 `dispose()` + 重新 `buildRuntime()`。
|
|
6433
|
+
* **怎么把这件事说给用户听是宿主的决定**(它比我们知道自己的用户能不能接受),
|
|
6434
|
+
* 我们只给这一格事实。判据全文在
|
|
6435
|
+
* [plugin-control.ts](./plugin-control.ts) 的文件头第二节。
|
|
6436
|
+
*
|
|
6437
|
+
* ⚠️ 这条路**只收 `<市场>/<插件>`,收不了一条路径** —— 那是这一轮的安全性质
|
|
6438
|
+
* 本体(比 {@link skillImport} 那道闸更紧),判据在那个文件头第四节。
|
|
6439
|
+
*/
|
|
6440
|
+
plugins: PluginControl | null;
|
|
4984
6441
|
/**
|
|
4985
6442
|
* MCP server 的连接状态(方案 25 PR-4)。
|
|
4986
6443
|
*
|
|
@@ -5037,6 +6494,19 @@ interface EpochRuntime {
|
|
|
5037
6494
|
* 得向工厂要那个会话自己的({@link LiveSession.goals})。
|
|
5038
6495
|
*/
|
|
5039
6496
|
goals: GoalControl | null;
|
|
6497
|
+
/**
|
|
6498
|
+
* **任意一段会话**的目标(方案 52 PR-4)。会话库起不来时为 null,同上面那格。
|
|
6499
|
+
*
|
|
6500
|
+
* ⚠️ **它不是 `goals` 的另一种写法,两格答的是两个问题**:`goals` 答
|
|
6501
|
+
* 「**我这一段**的目标」(TUI 那种一个会话一个宿主的形态),这一格答
|
|
6502
|
+
* 「**那一段**的目标」(多会话宿主,也就是 web 服务端 —— 它每条请求的
|
|
6503
|
+
* sessionId 来自 URL,压根没有「我这一段」这个东西)。
|
|
6504
|
+
*
|
|
6505
|
+
* **它能答冷却掉的会话,而工厂那一份不能**:目标没有内存态,`GoalService`
|
|
6506
|
+
* 每次现读 SQLite。判据全文(含「为什么这里刻意没有 `block()`」)在
|
|
6507
|
+
* {@link GoalCatalog} 上。
|
|
6508
|
+
*/
|
|
6509
|
+
goalCatalog: GoalCatalog | null;
|
|
5040
6510
|
/**
|
|
5041
6511
|
* 会话的列举与检索(方案 25 PR-4 的 `/resume` 和 Ctrl+R)。
|
|
5042
6512
|
*
|
|
@@ -5070,8 +6540,21 @@ interface EpochRuntime {
|
|
|
5070
6540
|
*/
|
|
5071
6541
|
readonly contextBreakdown: ContextBreakdown | null;
|
|
5072
6542
|
/**
|
|
5073
|
-
* 定时任务(方案 45)。**不可为 null
|
|
5074
|
-
*
|
|
6543
|
+
* 定时任务(方案 45)。**不可为 null**;本平台没有 OS 后端时 `capability()`
|
|
6544
|
+
* 如实说 `backend: null`。
|
|
6545
|
+
*
|
|
6546
|
+
* ⚠️ **这一段原来写着「底下只有一张 SQLite 表和一个平台探测,没有起不来的可能」
|
|
6547
|
+
* —— 后半句是假的**(2026-08-26 改准)。那张表就是 `sessions.db`,和
|
|
6548
|
+
* `SessionManager` / `TrackerDB` 同一个文件,而那两个都套着 `safeInit` 开 ——
|
|
6549
|
+
* 也就是说这个仓库自己早就认了「它开不起来」这件事。真实后果比「一格是 null」
|
|
6550
|
+
* 严重得多:装配那一行原来是裸调的,于是那个文件打不开时**整个 `buildRuntime()`
|
|
6551
|
+
* 抛**,`epoch web` 退出码 1。
|
|
6552
|
+
*
|
|
6553
|
+
* 现在开库失败被兜住,这一格照旧不为 null,**但要先看
|
|
6554
|
+
* `schedules.storeAvailable`** —— `false` 时下面每个方法都会抛(不回空清单,
|
|
6555
|
+
* 那是「这台机器上没有定时任务」,是另一句话)。为什么选加一格而不是把这个
|
|
6556
|
+
* 字段改成可空(`major` 会传染给全组),判据在
|
|
6557
|
+
* [schedule/control.ts](./schedule/control.ts) 的 `createScheduleControl` 上。
|
|
5075
6558
|
*
|
|
5076
6559
|
* ⚠️ **这一份是给嵌入宿主的,CLI 不走它**(`epoch schedule` 自己开 store,
|
|
5077
6560
|
* 因为只有 CLI 知道 `process.execPath` 和入口 js 在哪)。判据、以及
|
|
@@ -5649,4 +7132,4 @@ declare function probeMcpServer(name: string, opts?: McpAdminOptions): Promise<{
|
|
|
5649
7132
|
needsLogin: boolean;
|
|
5650
7133
|
}>;
|
|
5651
7134
|
|
|
5652
|
-
export { AgentRoleError, AgentSession, type AgentSessionOptions, type ApplyHostPresetOptions, ApprovalRelay, type ApproveFn, type BindWorkspaceFailure, type BindWorkspaceResult, type BuildRuntimeOptions, type BuildToolsInput, type CommandControl, type CommandWiring, type DiscoverOptions, type EffectiveSetting, type EpochRuntime, type ExecutorRuntime, type FireResult, type FireScheduleOptions, type GoalControl, HOST_CAPABILITIES_MODULE, HOST_PRESET_MODULE, type HistoryMessage, type HostAgentRole, type HostCapabilities, type HostCapabilityWiring, type HostPreset, type HostPresetProvider, type HostPresetWorkspace, type HostSkillDir, type LevelChangeResult, type McpAddInput, type McpAddOutcome, type McpAdminOptions, type McpApplyAction, type McpApplyChange, type McpApplyOutcome, type McpApplySource, type McpConfigIssue, type McpConfigReadOutcome, type McpConfigSaveOutcome, type McpConfigSnapshot, type McpControl, type McpListResult, type McpLoginCliOptions, type McpReconnectOutcome, type McpServerBatch, type McpServerOverview, type McpWriteOutcome, type ModelCatalogControl, type ModelCatalogOptions, type ModelControl, type ModelSuggestions, type ModelTurnScope, type PermissionsControl, type PlanControl, type PolicyDirStatus, type PolicyStatus, type ProviderOption, QuestionRelay, type RelayClock, type RoleAddInput, type RoleAddOutcome, type RoleControl, type RoleTurnScope, type RoleWriteControl, type RunOptions, SCHEDULE_EXIT_CODES, type ScheduleCapability, type ScheduleControl, type ScheduleControlOptions, type ScheduleSaveResult, type SerializableAgentEvent, type SerializableApprovalRequest, type SerializableQuestionRequest, type SerializedEvent, type ServiceOptions, type Services, type SessionControl, type SessionDeletion, type SessionFileChange, type SessionHit, SessionStore, type SessionSummary, type SessionWorkspace, SessionWorkspaces, type SessionWorkspacesOptions, type SettingWriteDone, type SettingWriteInput, type SettingWriteRefused, type SettingWriteReport, type SettingsControl, type StoredMessage, type TurnDetail, type TurnStep, type UserActionResult, type UserActions, type WireWorkspaceInput, type WorkspaceBindingState, type WorkspaceControl, applyHostPreset, applyMcpConfig, buildModelCatalogControl, buildRuntime, buildServices, collectFileChanges, createScheduleControl, createUserActions, ensureSecretsReady, exitCodeFor, fireSchedule, listMcpServers, mcpConfigRevision, mcpLogin, mcpLogout, probeMcpServer, readMcpConfigFile, registerAskQuestion, registerLocalTools, registerMcpTools, serializeStream, splitDirList, toSerializable, wireCommands, wireHostCapabilities, wirePluginMcpServers, wireWorkspace, withModelScope, withRoleScope, withToolScope, writeMcpConfigFile, writeMcpServer };
|
|
7135
|
+
export { type AgentEventBus, type AgentEventSubscriber, AgentRoleError, AgentSession, type AgentSessionOptions, type ApplyHostPresetOptions, ApprovalRelay, type ApprovalRevokeResult, type ApproveFn, type BindWorkspaceFailure, type BindWorkspaceResult, type BuildRuntimeOptions, type BuildToolsInput, type CommandControl, type CommandWiring, type DiscoverOptions, type EffectiveSetting, type EpochRuntime, type ExecutorRuntime, type FileCandidatesResult, type FileReadOutcome, type FireResult, type FireScheduleOptions, type GoalControl, HOST_CAPABILITIES_MODULE, HOST_MARKETPLACE_MODULE, HOST_PRESET_MODULE, type HistoryMessage, type HookStatus, type HostAgentRole, type HostCapabilities, type HostCapabilityWiring, type HostMarketplaces, type HostPreset, type HostPresetProvider, type HostPresetWorkspace, type HostSkillDir, type LevelChangeResult, type McpAddInput, type McpAddOutcome, type McpAdminOptions, type McpApplyAction, type McpApplyChange, type McpApplyOutcome, type McpApplySource, type McpConfigIssue, type McpConfigReadOutcome, type McpConfigSaveOutcome, type McpConfigSnapshot, type McpControl, type McpListResult, type McpLoginCliOptions, type McpReconnectOutcome, type McpServerBatch, type McpServerOverview, type McpWriteOutcome, type ModelCatalogControl, type ModelCatalogOptions, type ModelControl, type ModelSuggestions, type ModelTurnScope, type PermissionsControl, type PlanControl, type PluginActionOutcome, type PluginControl, type PluginInstallOutcome, type PluginListEntry, type PluginPreviewOutcome, type PluginRefusal, type PluginRefuseReason, type PluginSearchHit, type PolicyDirStatus, type PolicyStatus, type ProviderOption, QuestionRelay, type RelayClock, type RoleAddInput, type RoleAddOutcome, type RoleControl, type RoleTurnScope, type RoleWriteControl, type RunOptions, SCHEDULE_EXIT_CODES, type ScheduleCapability, type ScheduleControl, type ScheduleControlOptions, type ScheduleSaveResult, type SerializableAgentEvent, type SerializableApprovalRequest, type SerializableQuestionRequest, type SerializedEvent, type ServiceOptions, type Services, type SessionControl, type SessionDeletion, type SessionFileChange, type SessionHit, SessionStore, type SessionSummary, type SessionWorkspace, SessionWorkspaces, type SessionWorkspacesOptions, type SettingWriteDone, type SettingWriteInput, type SettingWriteRefused, type SettingWriteReport, type SettingsControl, type StoredMessage, type TurnDetail, type TurnStep, type UserActionResult, type UserActions, type UtilityControl, type UtilityGenerateInput, type UtilityGenerateResult, type WireWorkspaceInput, type WorkspaceBindingState, type WorkspaceControl, type WorkspaceFilesDeps, type WorkspaceFilesView, applyHostPreset, applyMcpConfig, buildModelCatalogControl, buildRuntime, buildServices, buildWorkspaceFilesControl, collectFileChanges, createScheduleControl, createUserActions, ensureSecretsReady, exitCodeFor, fireSchedule, listMcpServers, mcpConfigRevision, mcpLogin, mcpLogout, probeMcpServer, providerLabel, readMcpConfigFile, registerAskQuestion, registerLocalTools, registerMcpTools, serializeStream, splitDirList, toSerializable, utilityBudgetKey, wireCommands, wireHostCapabilities, wirePluginMcpServers, wireWorkspace, withModelScope, withRoleScope, withToolScope, writeMcpConfigFile, writeMcpServer };
|