@epoch-agent/plugin-terminal 0.1.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/LICENSE +219 -0
- package/README.md +291 -0
- package/dist/index.d.ts +347 -0
- package/dist/index.js +1002 -0
- package/package.json +41 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { BackgroundTaskInfo, EpochTool, EpochPlugin } from '@epoch-agent/protocol';
|
|
2
|
+
import { SandboxMode, IsolationBackend, SandboxEnforcement, FailureVerdict, SandboxPolicy } from '@epoch-agent/infra';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 把一次 shell 调用包进沙箱,并算出这一次要**如实上报**的东西(方案 46 §四)。
|
|
6
|
+
*
|
|
7
|
+
* ## 为什么从 `exec.ts` 拆出来(2026-08-17,方案 46 PR-4/PR-5)
|
|
8
|
+
*
|
|
9
|
+
* 拆的判据不是行数,是**关注点**:`exec.ts` 的主题逐字写着「别留下孤儿进程」,
|
|
10
|
+
* 而这里的主题是「边界在哪、包没包上、包不上时该说哪句话」。两件事在
|
|
11
|
+
* PR-1 那一轮还只有一个消费者(管道),挤在一个文件里看不出区别;
|
|
12
|
+
* PR-4 / PR-5 落地之后**三条路径全都要用它**(管道 / 后台 / PTY),
|
|
13
|
+
* 挤着的那份就成了「谁都得 import 那个起进程的文件」。
|
|
14
|
+
*
|
|
15
|
+
* 于是它下沉成一个叶子:三条路径各自 import 它,它谁都不 import。
|
|
16
|
+
*
|
|
17
|
+
* ## 这一层只回答两个问题
|
|
18
|
+
*
|
|
19
|
+
* 1. **argv 要不要换**({@link confineShell} 返回的 `file` / `args`)
|
|
20
|
+
* 2. **这次的边界怎么说出口**({@link SandboxOutcome},以及事后的 `classify`)
|
|
21
|
+
*
|
|
22
|
+
* 「说成人话」是**下一层**的事,在 [sandbox-notice.ts](./sandbox-notice.js) ——
|
|
23
|
+
* 那份整份是给模型读的措辞,所以它单独进了 i18n 的 `SKIP_FILES`,
|
|
24
|
+
* 而这个文件里一个用户可见字符串都没有,不该跟着被豁免。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 这一次跑在什么沙箱里 —— **如实上报,不假装**(方案 46 §四)。
|
|
29
|
+
*
|
|
30
|
+
* 为什么要有这个东西:`terminal` 的调用频率比 `code_exec` 高一个量级,而
|
|
31
|
+
* 「沙箱:已启用」这句话在两个工具上的含义不一样。让每次执行自己说出边界,
|
|
32
|
+
* 比让读文档的人去推「我这一档大概是什么模式」可靠。
|
|
33
|
+
*/
|
|
34
|
+
interface SandboxOutcome {
|
|
35
|
+
/** 这次按哪一档包的。`null` = 调用方压根没要沙箱(见各路径的 `sandbox` 参数)*/
|
|
36
|
+
mode: SandboxMode | null;
|
|
37
|
+
backend: IsolationBackend;
|
|
38
|
+
/** 真包上了才有。`null` 时看 {@link skipped} */
|
|
39
|
+
enforcement: SandboxEnforcement | null;
|
|
40
|
+
/**
|
|
41
|
+
* 没包上的原因,包上了就是 `null`。
|
|
42
|
+
*
|
|
43
|
+
* 三档是三句不同的话,不能合成一个「没有沙箱」:
|
|
44
|
+
* `mode-disabled` 是**用户自己选的**(`bypass`),
|
|
45
|
+
* `no-backend` 是**平台限制**(Windows / 没装 bwrap),
|
|
46
|
+
* `not-requested` 是**调用方没传 policy**(今天只有用例会这样)。
|
|
47
|
+
*/
|
|
48
|
+
skipped: 'mode-disabled' | 'no-backend' | 'not-requested' | null;
|
|
49
|
+
/**
|
|
50
|
+
* 非零退出时这次失败属于哪一种(方案 46 §五)。
|
|
51
|
+
*
|
|
52
|
+
* ⚠️ **`command-failed` 和 `null` 不是一回事**:前者是「分类器跑过了,
|
|
53
|
+
* 结论是命令自己失败的」,后者是「压根没跑分类器」(退出码为 0,
|
|
54
|
+
* 或者根本没有沙箱可言)。
|
|
55
|
+
*/
|
|
56
|
+
failure: FailureVerdict | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 非交互式命令执行 —— 起进程、收两条流、到点杀干净。
|
|
61
|
+
*
|
|
62
|
+
* 从 `index.ts` 拆出来的:那边只留工具定义(参数 schema、危险命令拦截、
|
|
63
|
+
* workdir 校验、结果映射),这边只管「怎么起、怎么收」。拆的直接原因是
|
|
64
|
+
* 方案 16 把 `index.ts` 顶到了 630 行,越过 CLAUDE.md 铁律 3 的 500 行线。
|
|
65
|
+
*
|
|
66
|
+
* 这个文件的主题只有一个:**别留下孤儿进程**。
|
|
67
|
+
*
|
|
68
|
+
* ## ⚠️ 2026-08-17:它自己也超过 500 行了,所以又拆了一轮(方案 46 PR-4/PR-5)
|
|
69
|
+
*
|
|
70
|
+
* 上面那段文件头解释了「当初为什么从 `index.ts` 拆出来」,却一直没交代
|
|
71
|
+
* **它自己也越过了同一条线**(649 行)。这一轮补上,顺便把该拆的拆了。
|
|
72
|
+
*
|
|
73
|
+
* 拆的判据**不是行数,是关注点**(照方案 55 那一轮 `handlers.ts` / `composer/`
|
|
74
|
+
* 的做法)。原来这个文件塞着三条执行路径 + 沙箱 + 输出上限四样东西:
|
|
75
|
+
*
|
|
76
|
+
* | 拆出去的 | 为什么它是另一个关注点 |
|
|
77
|
+
* | ------------------ | ---------------------------------------------------------- |
|
|
78
|
+
* | [sandbox.ts](./sandbox.js) | 「边界在哪、包没包上」和「别留孤儿进程」是两件事。而且 PR-4/PR-5 之后**三条路径都要用它**,留在这儿等于谁都得 import 那个起进程的文件 |
|
|
79
|
+
* | [pty.ts](./pty.js) | PTY 的进程组语义和另外两条**相反**(`setsid()` 之后它是另一个会话的组长,终端的 Ctrl+C 送不到)。摆在一起读的人得先分辨每段注释在说哪条路 |
|
|
80
|
+
* | [limits.ts](./limits.js) | 输出上限是管道和 PTY 共用的常量。下沉成叶子,`exec ↔ pty` 那个环就不存在了 |
|
|
81
|
+
*
|
|
82
|
+
* 剩下的这一份只讲一条路:**管道(非交互)**。
|
|
83
|
+
*
|
|
84
|
+
* ## 2026-08-16 起的第二个主题:**沙箱**(方案 46)
|
|
85
|
+
*
|
|
86
|
+
* 在这之前,`code_exec` 跑一段 JS 有 OS 强制的隔离,而 `terminal` 跑
|
|
87
|
+
* `node -e "..."` **什么隔离都没有** —— 后者的调用频率高一个量级。
|
|
88
|
+
*
|
|
89
|
+
* ✅ **2026-08-17 起三条路径全包上了**:管道(PR-1)、后台(PR-4,真插入点在
|
|
90
|
+
* [background.ts](./background.js) 的 `startTask()`)、PTY(PR-5,在
|
|
91
|
+
* [pty.ts](./pty.js))。`SANDBOX_EXCLUDES` 那份「点名清单」也跟着空了。
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 清理这个插件起过的长期子进程 [Hermes]。杀的是整棵树而不是组长。
|
|
96
|
+
*
|
|
97
|
+
* 调用方是 `runtime` 的 `dispose()`(2026-08-08 接上;在那之前它零调用方,
|
|
98
|
+
* `terminal(background: true)` 起的进程在宿主退出后是全留着的)。
|
|
99
|
+
* 因为要 `pgrep`(POSIX)/ `taskkill`(Windows)收进程树,它只能是 async ——
|
|
100
|
+
* 这正是当初没接线的原因,`dispose()` 那时是同步的。
|
|
101
|
+
*
|
|
102
|
+
* ## ⚠️ 2026-08-17:这里原来收的是**两套**表,现在只剩一套半
|
|
103
|
+
*
|
|
104
|
+
* 老的那张 `bgProcesses` 跟着 `runBackground` 一起删了(方案 46 PR-4)——
|
|
105
|
+
* 那个函数从方案 36 起就没有任何调用方,判据写在这一轮的落地记录里。
|
|
106
|
+
* 现在收的是:
|
|
107
|
+
*
|
|
108
|
+
* 1. infra 的 `killAllTrackedProcesses()` —— 方案 36 的后台任务登记在那儿
|
|
109
|
+
* (`detached: false`,见 `infra/src/child-process.ts` 的文件头),
|
|
110
|
+
* 是所有长期子进程的唯一真源
|
|
111
|
+
* 2. PTY 会话 —— 那张表是 [pty.ts](./pty.js) 的私有状态,它自己交出待收的一批
|
|
112
|
+
*/
|
|
113
|
+
declare function cleanupBackgroundProcesses(): Promise<void>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 后台任务表(方案 36)。
|
|
117
|
+
*
|
|
118
|
+
* 一条 `terminal(background: true)` 起的命令在这里有一条记录:状态、退出码、
|
|
119
|
+
* 一段**环形**输出缓冲、以及一个「结束了」的 promise。
|
|
120
|
+
*
|
|
121
|
+
* ## 三条上限,各挡各的
|
|
122
|
+
*
|
|
123
|
+
* | 上限 | 挡什么 |
|
|
124
|
+
* | ------------------------ | ------------------------------------------------- |
|
|
125
|
+
* | **每会话**同时 8 个任务 | 模型一口气起十个 watch,机器和上下文一起崩 |
|
|
126
|
+
* | 单任务输出 256KB | 一个刷屏的 `pnpm dev` 跑一夜把内存吃光 |
|
|
127
|
+
* | `task_output` 单次 8KB | 取一次输出就把上下文塞满 |
|
|
128
|
+
*
|
|
129
|
+
* 第二条用**环形缓冲**(丢头保尾)而不是「满了就截断」:后台任务里最有价值的
|
|
130
|
+
* 永远是最后几行(构建结果、报错栈),前台命令那边正好相反(一次性命令的头部是
|
|
131
|
+
* 「跑的是什么命令、什么配置」,尾部是结论,**两头都要**)。两处不一致是刻意的。
|
|
132
|
+
*
|
|
133
|
+
* ## 溢出的那部分现在会落盘(方案 47 PR-1),但「丢头保尾」没有被反转
|
|
134
|
+
*
|
|
135
|
+
* 环形缓冲一满就开始丢头,丢掉的字节改造前是真的没了。现在第一次溢出时会开一个
|
|
136
|
+
* artifact 文件,把**到那一刻为止的全部输出**写进去,之后每一块新输出都追加 ——
|
|
137
|
+
* 于是文件里始终是完整的一份,而内存里仍然只有最后 256KB。
|
|
138
|
+
*
|
|
139
|
+
* 落盘只是让被丢掉的头也留下来,**不是**让预览改成头尾都取:
|
|
140
|
+
* `task_output` 给的仍然是尾部,因为那条决定本来就是对的。
|
|
141
|
+
*
|
|
142
|
+
* ## ✅ 2026-08-15:这张表从**进程级**收窄成了**会话级**
|
|
143
|
+
*
|
|
144
|
+
* 原来这里是一个模块级的 `Map<taskId, Task>`,理由原话是:「宿主是**会话**而不是
|
|
145
|
+
* 一次工具调用(方案 36 的『一轮结束继续跑』要求它),而 plugin 拿不到会话对象」。
|
|
146
|
+
*
|
|
147
|
+
* 前半句今天仍然成立 —— 所以它**还是**一个模块级的 Map,只是按会话分了区
|
|
148
|
+
* (`Map<sessionId, Map<taskId, Task>>`)。**变的是后半句**:plugin 拿不到会话
|
|
149
|
+
* *对象*,但一直拿得到会话 *id* —— `ToolContext.sessionId` 是工具契约上的必填字段,
|
|
150
|
+
* 而这张表从方案 47 起就已经在收它了(用来定 artifact 落在哪个目录)。
|
|
151
|
+
* 也就是说这一轮**没有为它新开一条通路**,只是把一条早就通着的路走完。
|
|
152
|
+
*
|
|
153
|
+
* 收窄之前的代价是具体的:同一个进程里两个会话,`task_list` 给模型的是**同一批**
|
|
154
|
+
* 任务,`GET /api/sessions/:id/tasks` 回的也是同一批 —— 于是 A 会话每轮被告知
|
|
155
|
+
* 一批它没起过的命令在跑,而一句 `task_stop t1` 能停掉 B 会话的构建。
|
|
156
|
+
*
|
|
157
|
+
* ⚠️ **同时 8 个** 也跟着变成每会话一份(原来是进程级)。判据是那句话本身:
|
|
158
|
+
* 满了时给的建议是「先用 `task_stop` 停掉一个」,而按会话分之后,别人的任务
|
|
159
|
+
* 这个会话**够不着** —— 一条做不到的建议比没有建议更坏。代价写下来不掩饰:
|
|
160
|
+
* 进程里的总数现在是 `8 × 活跃会话数`,「机器」那一半的保护变松了。真要重新
|
|
161
|
+
* 收住,那是**另一条**进程级上限(和这一条并存),不是把这一条改回去。
|
|
162
|
+
*
|
|
163
|
+
* ## ⚠️ 进程退出时的清理:**不走这张表**,所以分区不影响它
|
|
164
|
+
*
|
|
165
|
+
* 真正的回收是 infra 的 `killAllTrackedProcesses()` —— 那张表按 pid 记,是所有
|
|
166
|
+
* 长期子进程的唯一真源,宿主 `dispose()` 调的就是它。这张表只记「谁起过什么」,
|
|
167
|
+
* 一个进程都不负责杀({@link clearAllTasks} 只负责忘掉记录)。
|
|
168
|
+
* 所以「按会话分了会不会漏杀某个会话的进程」这个问题的答案是**不会**:
|
|
169
|
+
* 回收压根不遍历这张表。这条由 `__tests__/session-scope.test.ts` 最后一组钉着。
|
|
170
|
+
*
|
|
171
|
+
* ## ⚠️ 会话被冷却(`live: false`)时:这批任务**一个字都不动**
|
|
172
|
+
*
|
|
173
|
+
* 冷却(`SessionHub` 的 LRU + `SessionFactory.release()`)是「把内存还回去」:
|
|
174
|
+
* 对话历史、`AgentLoop`、审批桥都放掉。**任务表不在被放掉的那一批里**,
|
|
175
|
+
* 判据与 `release()` 里「工作区绑定不在这里解」逐字同款 —— 会话可以被释放,
|
|
176
|
+
* 而**进程还在跑**是一个仍然成立的事实,且这张表是唯一还能停掉它的把手。
|
|
177
|
+
* 跟着冷却清掉的话,那几个 `pnpm dev` 会一直烧到进程退出,而没有任何人能报出
|
|
178
|
+
* 它们的存在;跟着冷却**杀掉**更糟 —— 用户开第 9 个标签页,第 1 个会话跑了
|
|
179
|
+
* 二十分钟的构建就没了。
|
|
180
|
+
*
|
|
181
|
+
* 于是被冷却的会话重新 `register()` 回来时,它那一格原样还在。代价是一个
|
|
182
|
+
* 再也不会被打开的会话会在这张表里留一格(≤ 8 条记录 + 各自 ≤ 256KB 缓冲)——
|
|
183
|
+
* 和收窄之前的总量**完全一样**,只是换了个摆法:那些记录本来也只增不减。
|
|
184
|
+
*
|
|
185
|
+
* ⚠️ **真删会话(`DELETE /api/sessions/:id`)那条路今天没有收这一格** ——
|
|
186
|
+
* 那是一处该改但不归这一轮改的地方,判据和处置写在 {@link clearAllTasks} 上面。
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
/** **每个会话**同时最多几个在跑(2026-08-15 从进程级收窄,判据见文件头) */
|
|
190
|
+
declare const MAX_TASKS = 8;
|
|
191
|
+
/**
|
|
192
|
+
* 起一个后台任务。名额满了返回一句话而不是抛。
|
|
193
|
+
*
|
|
194
|
+
* @param sessionId 这个任务归谁(`ToolContext.sessionId`)。**必填**:一条不知道
|
|
195
|
+
* 属于谁的后台任务在这张表里没有位置 —— 它要么被所有会话看见(收窄之前那个
|
|
196
|
+
* 毛病),要么谁都看不见。它同时决定溢出的输出落进哪个 artifact 目录
|
|
197
|
+
* @param homeDir 数据目录(`ToolContext.homeDir`)。**不能省**(方案 54 §二):
|
|
198
|
+
* 省掉就落进全局 `~/.epoch/artifacts/`,而宿主的界面从 `<homeDir>/artifacts/` 读
|
|
199
|
+
* @param sandbox 这次的沙箱边界(方案 46 PR-4)。**不传 = 不上沙箱**,如实标成
|
|
200
|
+
* `skipped: 'not-requested'`。口径和 `runCommand` / `runCommandPty` 逐字一致:
|
|
201
|
+
* 口子是给只关心任务表的用例留的,工具那条真实路径每次都传
|
|
202
|
+
*/
|
|
203
|
+
declare function startTask(sessionId: string, command: string, cwd?: string, homeDir?: string, sandbox?: SandboxPolicy): {
|
|
204
|
+
task: BackgroundTaskInfo;
|
|
205
|
+
sandbox: SandboxOutcome;
|
|
206
|
+
} | {
|
|
207
|
+
error: string;
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* 一个任务当下的沙箱状况。`task_output` 用它把「被沙箱挡了 / 沙箱没起来」
|
|
211
|
+
* 说成另一句话 —— 后台任务的失败和前台的一样需要这个区分,而且更需要:
|
|
212
|
+
* 前台失败时模型手里有 stderr,后台失败时它得先想起来去 `task_output` 取。
|
|
213
|
+
*/
|
|
214
|
+
declare function taskSandbox(sessionId: string, id: string): SandboxOutcome | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* 这个会话起过的任务,**只有这个会话的**。
|
|
217
|
+
*
|
|
218
|
+
* 用 `get` 不用 {@link bucketOf}:这条被读得很勤(检视面板每次回合状态变化都拉
|
|
219
|
+
* 一次,每个会话各拉各的),现开一格的话,只要有人打开过面板就在这张表里留一个
|
|
220
|
+
* 空 Map —— 一个没有任何人写过的会话不该在这里占位置。
|
|
221
|
+
*/
|
|
222
|
+
declare function listTasks(sessionId: string): BackgroundTaskInfo[];
|
|
223
|
+
interface TaskOutputResult {
|
|
224
|
+
info: BackgroundTaskInfo;
|
|
225
|
+
/** 这一段输出 */
|
|
226
|
+
output: string;
|
|
227
|
+
/** 下次传回来的游标(绝对偏移) */
|
|
228
|
+
nextCursor: number;
|
|
229
|
+
/** 从 `since` 到现在有多少字节已经滚出环形缓冲 */
|
|
230
|
+
missed: number;
|
|
231
|
+
/** 还有没有更多(这次被 `MAX_OUTPUT_CHUNK` 截断了) */
|
|
232
|
+
hasMore: boolean;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* 取增量输出。
|
|
236
|
+
*
|
|
237
|
+
* `since` 是**绝对偏移**而不是「第几次调用」:后者在两次调用之间任务又输出了
|
|
238
|
+
* 一大段时会算错,而绝对偏移天然对得上。丢掉的那一段单独报 `missed`,
|
|
239
|
+
* 不静默 —— 模型据此知道自己看到的不是全部。
|
|
240
|
+
*
|
|
241
|
+
* 别的会话的 id 一律 `undefined`(见 {@link taskIn})—— 输出里可能有 token、
|
|
242
|
+
* 路径、别人的仓库名,那是这道墙最该拦住的东西。
|
|
243
|
+
*/
|
|
244
|
+
declare function taskOutput(sessionId: string, id: string, since?: number): TaskOutputResult | undefined;
|
|
245
|
+
/**
|
|
246
|
+
* 停掉一个任务(杀整棵树)。已经结束的返回 false。
|
|
247
|
+
*
|
|
248
|
+
* **别的会话的 id 也返回 false**,和「已经结束了」同一个出口:这是四个工具里
|
|
249
|
+
* 唯一真会动手的那个,一句 `task_stop t1` 曾经能停掉另一个会话的构建。
|
|
250
|
+
*/
|
|
251
|
+
declare function stopTask(sessionId: string, id: string): Promise<boolean>;
|
|
252
|
+
interface TaskWaitResult {
|
|
253
|
+
info: BackgroundTaskInfo;
|
|
254
|
+
/** 等超时了(任务还在跑)。**没杀它** */
|
|
255
|
+
timedOut: boolean;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* 等一个任务结束。
|
|
259
|
+
*
|
|
260
|
+
* 超时**不杀它** —— 「我等不及了」和「我要停掉它」是两件事,
|
|
261
|
+
* 后者有 `task_stop`。把它们合并的话,模型一次 `task_wait` 超时就会把一个
|
|
262
|
+
* 跑了十分钟的构建白白毁掉。
|
|
263
|
+
*/
|
|
264
|
+
declare function waitTask(sessionId: string, id: string, timeoutMs: number): Promise<TaskWaitResult | undefined>;
|
|
265
|
+
/**
|
|
266
|
+
* `/resume` 换了会话 id,这一格跟着搬(方案 25 PR-4)。
|
|
267
|
+
*
|
|
268
|
+
* 判据与 `build.ts` 里紧挨着的 `workspaces.seed(id, …)` 逐字同款:**resume 只是
|
|
269
|
+
* 把另一段对话接上这个引擎,它照旧跑在这个进程、这个工作区里**。不搬的话,
|
|
270
|
+
* TUI 里 `/resume` 一下,刚才起的那条 `pnpm dev` 从 `/tasks` 里消失,
|
|
271
|
+
* 而它还在跑 —— 一个停不掉也报不出来的进程,比一条错误归属的记录坏得多。
|
|
272
|
+
*
|
|
273
|
+
* 目标会话已经有一格时**合并**而不是覆盖(resume 回一段在这个进程里跑过任务的
|
|
274
|
+
* 会话)。id 全进程唯一,所以合并不会撞键。
|
|
275
|
+
*/
|
|
276
|
+
declare function rekeyTasks(from: string, to: string): void;
|
|
277
|
+
/**
|
|
278
|
+
* 清空**所有**会话的任务表(**不杀进程**)。只给用例用。
|
|
279
|
+
*
|
|
280
|
+
* 真正的回收走 `killAllTrackedProcesses()` —— 那张表在 infra,是所有长期子进程
|
|
281
|
+
* 的唯一真源,宿主 `dispose()` 调的是它。这里只负责忘掉记录。
|
|
282
|
+
*
|
|
283
|
+
* ⚠️ **给合并的人:`DELETE /api/sessions/:id` 那条路今天不收这一格。**
|
|
284
|
+
* 表按会话分了之后,删掉一个会话 = 它那几个后台任务从此**没有任何界面看得见**
|
|
285
|
+
* (收窄之前它们至少还错误地出现在别的会话那一格里)。该有的处置是删会话时
|
|
286
|
+
* 连同它们一起停掉 —— 那是一次真的杀进程,语义上和 `hub.unregister()` 里
|
|
287
|
+
* 「放弃挂起的审批 + 中止在跑的回合」同一档。
|
|
288
|
+
*
|
|
289
|
+
* 这一轮**没做**,两个理由:入口在 `server/src/api.ts` 的删会话处理里,不在
|
|
290
|
+
* 这一轮的文件所有权内(.agents/plans/30 §十二那条规矩);而且「删会话要不要
|
|
291
|
+
* 杀掉它起的构建」是一次产品决定,不该由一次收窄顺手替人做了。
|
|
292
|
+
* 要收的话这里加一个 `stopSessionTasks(sessionId)`(遍历那一格 `stopTask`),
|
|
293
|
+
* 在删会话那条路上调一次 —— **不是** `clearAllTasks` 的会话版:只忘掉记录
|
|
294
|
+
* 而不杀进程,正好是上面说的那个洞。
|
|
295
|
+
*
|
|
296
|
+
* ⚠️ **2026-08-15 合并时看了,明确不做**(账在
|
|
297
|
+
* [30 §13.4](../../../../docs/verify/VERIFY_RECORD-30-web-multi-session.md)):
|
|
298
|
+
* 上面那句判据成立 —— 「删会话要不要杀掉它起的构建」是一次产品决定,
|
|
299
|
+
* 合并收账不是替人做决定的场合。**这个洞今天真的在**,收的人该判的是
|
|
300
|
+
* 「值不值得单独一轮」,不是「上一轮为什么没做」。
|
|
301
|
+
*/
|
|
302
|
+
declare function clearAllTasks(): void;
|
|
303
|
+
/** 一行摘要,四个工具和宿主展示共用一份措辞 */
|
|
304
|
+
declare function describeTask(info: BackgroundTaskInfo): string;
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 四个后台任务工具(方案 36):`task_list` / `task_output` / `task_wait` /
|
|
308
|
+
* `task_stop`。
|
|
309
|
+
*
|
|
310
|
+
* ## 为什么 `task_wait` 是必须有的
|
|
311
|
+
*
|
|
312
|
+
* 没有它,模型只能轮询 `task_output` —— 那是纯浪费 token,而且它没法知道该隔多久
|
|
313
|
+
* 问一次。有了它,「先放后台跑,需要结果时再等」这条路才完整。
|
|
314
|
+
*
|
|
315
|
+
* ## 权限:后台不是旁路
|
|
316
|
+
*
|
|
317
|
+
* 这四个工具本身**不执行任何命令**(`operation: 'file_read'` 那一档的内部工具),
|
|
318
|
+
* 真正的命令在 `terminal` 那一步就已经过完危险表和权限判定了。
|
|
319
|
+
* 把它们标成 `command` 反而会让模型每取一次输出都弹一次确认框。
|
|
320
|
+
*
|
|
321
|
+
* ⚠️ 反过来说,`terminal(background: true)` **一定**要和前台走同一条权限判定 ——
|
|
322
|
+
* 「后台」听起来像个能绕过去的旁路,而它不是。那条有用例守着。
|
|
323
|
+
*
|
|
324
|
+
* ## 四个都吃 `ctx.sessionId`(2026-08-15)
|
|
325
|
+
*
|
|
326
|
+
* 任务表按会话分了(判据全文在 [background.ts](./background.ts) 的文件头),
|
|
327
|
+
* 于是这四个工具**每一个**都要说出「我是谁」。别的会话的 id 一律走
|
|
328
|
+
* {@link notFound} 那条出口 —— 和「压根没这个 id」同一句话,理由见
|
|
329
|
+
* `background.ts` 里 `taskIn()` 的注释。
|
|
330
|
+
*
|
|
331
|
+
* `task_list` 的描述里那句「本次会话起过的后台任务」从方案 36 那天就写着,
|
|
332
|
+
* 收窄之前它是句**假话**(回的是整个进程的)。这一轮把它兑现了,
|
|
333
|
+
* **一个字都不用改** —— 也正因为如此,工具描述和工具表这一轮都没有变化。
|
|
334
|
+
*/
|
|
335
|
+
|
|
336
|
+
declare const TASK_TOOLS: readonly EpochTool[];
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 终端插件 —— 工具定义。
|
|
340
|
+
*
|
|
341
|
+
* 「怎么起进程、怎么收干净」在 [exec.ts](./exec.ts),这里只管工具契约:
|
|
342
|
+
* 参数 schema、危险命令拦截、workdir 边界、以及执行结果到 `ToolResult` 的映射。
|
|
343
|
+
*/
|
|
344
|
+
|
|
345
|
+
declare const terminalPlugin: EpochPlugin;
|
|
346
|
+
|
|
347
|
+
export { MAX_TASKS, TASK_TOOLS, cleanupBackgroundProcesses, clearAllTasks, describeTask, listTasks, rekeyTasks, startTask, stopTask, taskOutput, taskSandbox, terminalPlugin, waitTask };
|