@plumeai/plugin-sdk 0.1.8 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,17 +2,18 @@
2
2
  * 插件逻辑入口的 API:插件默认导出 `Plugin`,在 apply 里通过 ctx 注册工具、往钩子里加层、登记清理。
3
3
  * 界面入口的 API 在 `ui.ts`(`@plume/contract/ui`)。
4
4
  */
5
- import type { FileChangesService, RevertPlan } from "./changes.ts";
6
- import type { BackgroundJobsService, SessionObservation, SessionWakeupsService } from "./diagnostics.ts";
7
- import type { ResourcesService } from "./resources.ts";
8
- import type { AssistantMessage, DeliveredReport, Message, PeerLetter, ThinkingOption, ToolResultMessage, ToolSpec } from "../../core/llm/types.d.ts";
9
- import type { ApprovalDecision, ApprovalRequest, CompactionCode, NoticeLevel, PermissionMode, Personalization, OutgoingMessage, SessionSummary, ThinkingLevel, TurnStop } from "./service.ts";
5
+ import type { PluginChanges } from "./changes.ts";
6
+ import type { BackgroundJobInfo, BackgroundJobOutput, SessionObservation, SessionWakeup } from "./diagnostics.ts";
7
+ import type { PluginResources, Resource } from "./resources.ts";
8
+ import type { AssistantMessage, DeliveredReport, FileChanges, Message, PeerLetter, ThinkingOption, ToolResultMessage, ToolSpec } from "../../core/llm/types.d.ts";
9
+ import type { ApprovalDecision, ApprovalRequest, CompactionCode, HumanRequest, NoticeLevel, PermissionMode, Personalization, OutgoingMessage, SessionKind, SessionSummary, ThinkingLevel, TurnStop } from "./service.ts";
10
10
  import type { UsageScan } from "./usage.ts";
11
11
  /** 界面语言。只有这两种;跟随系统时先解析成其中一种再告诉 agent 进程。 */
12
12
  export type Locale = "zh-CN" | "en";
13
13
  export interface ToolCall {
14
14
  id: string;
15
15
  name: string;
16
+ /** 模型给的参数,别名已换成规范名(`ToolDefinition.aliases`);外层改过的照改过的,改成别名的也换回规范名。 */
16
17
  args: Record<string, unknown>;
17
18
  }
18
19
  export interface ToolOutput {
@@ -32,8 +33,20 @@ export interface ToolOutput {
32
33
  * 本体照 `images` 把大图挪进媒体目录,通用卡片把它画在 `images` 后面。
33
34
  */
34
35
  shownImages?: ToolOutput["images"];
35
- /** 给工具卡片和插件自己用的结构化数据,存进会话,不发给模型。形状由插件定,本体不看里面。 */
36
+ /**
37
+ * 给工具卡片和插件自己用的结构化数据,存进会话,不发给模型。形状由插件定,本体不看里面,只认这几个通用声明:
38
+ * 带 `checklist`(`ChecklistDetails`)是一份进度清单,带 `job`(`BackgroundJobDetails`)是在后台起了一个任务,
39
+ * 带 `path`、`added`、`removed`、`hunks`(`FileDiffDetails`)是改了一个文件、通用卡片画出 diff,
40
+ * 出错的结果带 `cancelled: true`(`CancelledDetails`)是人按了停、不是失败,带 `spill`(`SpillDetails`)是结果截过、全文在别处。
41
+ * 插件之间另认一个:让模型看了文件的哪一块(`FileViewDetails`)。改了哪些文件不在这里,见 `changes`。
42
+ */
36
43
  details?: unknown;
44
+ /**
45
+ * 这次调用改了哪些文件(`FileChanges`),由本体填:经 `ToolRun.files.change` 写成的、经 `ToolRun.exec`(`recordChanges`)观测到的命令改的,
46
+ * 和 `ToolRun.files.includeChanges` 算进来的。工具自己填的不算,钩子 tool.execute 的外层读得到、改不了(本体最后照它记下的重填)。
47
+ * 记进会话里的结果(`ToolResultMessage.changes`),撤回连文件一起退、交付卡、压缩都按它认改过的文件。
48
+ */
49
+ changes?: FileChanges;
37
50
  /**
38
51
  * 这一步的结果都写进会话以后,这一轮就此结束,不再把结果交给模型:`done` 是完成,`stalled` 是卡住了。
39
52
  * 一步里有几个结果都带了它时,有 `stalled` 就按 `stalled`。结果本身照常存,下一轮模型照样看得到。
@@ -44,12 +57,118 @@ export interface ToolOutput {
44
57
  * 不占后面每一轮的上下文。给人看的卡片照旧是原文。
45
58
  */
46
59
  uneventful?: boolean;
60
+ /**
61
+ * 这条结果是要照着做的说明(载入的技能正文这类),不是一次观察:记在会话里的结果上,压缩时原样留着,
62
+ * 不按大小剪,也不当作被后来的结果顶替的旧观察清掉。剪了它就是在改规矩。
63
+ */
64
+ instructions?: boolean;
65
+ }
66
+ /** 进度清单的一项。`activeForm` 是正在做这一项时的说法(「正在接上路由」)。 */
67
+ export interface ChecklistItem {
68
+ content: string;
69
+ status: "pending" | "in_progress" | "completed";
70
+ activeForm?: string;
71
+ }
72
+ /**
73
+ * 工具结果的 `details` 带上 `checklist`,就是声明「这是这件事最新的一份进度清单」:每次给的都是整张,会话里最新一条成功的那份算数。
74
+ * 宿主按它决定一轮收尾时要不要提醒还有没做完的(转录底下的「继续」);`details` 里别的字段照旧归插件。
75
+ * 哪个工具记清单由工具定义上的 `category: "checklist"` 说,转录据它把这次调用当成一段活的记账(见 `ToolDefinition.category`)。
76
+ */
77
+ export interface ChecklistDetails {
78
+ checklist: ChecklistItem[];
79
+ }
80
+ /**
81
+ * 工具结果的 `details` 带上 `job`,就是声明「这次调用在后台起了一个任务」:之后查它输出、停下它的调用按 `id` 找回是哪一件,
82
+ * 转录里那一行说的是起它那次调用写的说明。`command` 是它跑的命令。`details` 里别的字段照旧归插件。
83
+ */
84
+ export interface BackgroundJobDetails {
85
+ job: {
86
+ id: string;
87
+ command?: string;
88
+ };
89
+ }
90
+ /**
91
+ * 工具结果的 `details` 带上 `fileView`,就是声明「这次调用让模型看了这个文件的这一块」:`path` 是调用里写的路径解析成的绝对路径
92
+ * (`~/` 展开、相对路径按工作目录,不追符号链接,同一个文件的读和改才对得上);`lines` 是第几行到第几行(从 1 数,含两头),
93
+ * 不给就是从头到尾;`columns` 是每一行给了第几个到第几个字符,不给就是整行;`verbatim` 说给的是不是这一块的原文——大纲、
94
+ * 截断过的抽取文字只是看过这一块,不能拿来顶替别的读。
95
+ *
96
+ * 压缩据此清掉被后来的观察顶替掉的旧读:之后又原样读到了盖住它的一块,或者这个文件被改过。不带它的读文件结果压缩不动。
97
+ * 谁让模型读的谁填,不认是哪个工具交的;`details` 里别的字段照旧归插件。
98
+ */
99
+ export interface FileViewDetails {
100
+ fileView: {
101
+ path: string;
102
+ lines?: {
103
+ from: number;
104
+ to: number;
105
+ };
106
+ columns?: {
107
+ from: number;
108
+ to: number;
109
+ };
110
+ verbatim: boolean;
111
+ };
112
+ }
113
+ /**
114
+ * 工具结果的 `details` 带上 `spill`,就是声明「这次结果截过,全文在 `address`」:`address` 是资源地址(`run.files.spill` 给的
115
+ * `artifact://`),`read` 能按行、按窗口读回来;`path` 是它在磁盘上的位置,给人打开、给别的插件核对用;`characters` 是全文多少字。
116
+ *
117
+ * 本体和插件都不按工具名找它:诊断从结果上认出它就能看全文,不再拼某个插件的目录。`details` 里别的字段照旧归插件。
118
+ */
119
+ export interface SpillDetails {
120
+ spill: {
121
+ address: string;
122
+ path: string;
123
+ characters: number;
124
+ };
125
+ }
126
+ /**
127
+ * 出错的工具结果(`isError`)的 `details` 带上 `cancelled: true`,就是声明「没做完是因为人按了停,不是失败」:转录画成「已停止」,
128
+ * 不画成出错。工具被停下时自己交结果的(命令跑到一半被杀)自己带上;本体替停下的调用交结果、或停下以后工具交回的错误结果
129
+ * 没有 `details` 时由本体带上。`details` 里别的字段照旧归插件。
130
+ */
131
+ export interface CancelledDetails {
132
+ cancelled: true;
133
+ }
134
+ /**
135
+ * 钩子输入和 `ToolRun` 上的 `session`:这次调用、这一步、这次请求所在的会话是哪一种、挂在哪,本体照实填。
136
+ * 只想知道「是不是侧栏里的会话、是不是自己的子会话」时看它,不用为此去 `ctx.sessions.list()`。
137
+ * 字段的意思同 `SessionSummary` 上的同名字段;`id` 就是 `sessionId`。
138
+ */
139
+ export interface SessionScope {
140
+ id: string;
141
+ kind: SessionKind;
142
+ parentId?: string;
143
+ owner?: string;
144
+ spawnedFrom?: {
145
+ sessionId: string;
146
+ toolCallId?: string;
147
+ };
148
+ project?: string;
149
+ /** 代管这个会话的插件,见 `SessionSummary.steward`。插件据此只对自己代管的会话收紧工具、改提示词。 */
150
+ steward?: string;
47
151
  }
48
152
  /** 审批卡上写什么,见 `ApprovalRequest`。`expiresAt` 由本体弹卡时定。 */
49
- export type ApprovalContent = Omit<ApprovalRequest, "toolCallId" | "tool" | "expiresAt">;
153
+ export type ApprovalContent = Omit<ApprovalRequest, "type" | "toolCallId" | "tool" | "expiresAt">;
154
+ /** `ToolRun.request` 弹给人的那张卡,见 `HumanRequest`。 */
155
+ export type HumanRequestInput = Pick<HumanRequest, "kind" | "title" | "payload" | "waiting">;
156
+ /**
157
+ * 人怎么回的 `ToolRun.request`:交回了一份数据,或者没有——`declined` 人点了不答,`expired` 到时没人答,`stopped` 这一轮停了
158
+ * (会话删了也算)。`value` 来自渲染进程,类型参数只是工具期望的形状,用之前自己核一遍。
159
+ */
160
+ export type HumanAnswer<T = unknown> = {
161
+ answered: true;
162
+ value: T;
163
+ } | {
164
+ answered: false;
165
+ reason: "declined" | "expired" | "stopped";
166
+ };
50
167
  /** 工具执行时拿到的环境。 */
51
168
  export interface ToolRun {
52
169
  sessionId: string;
170
+ /** 调用所在的会话,见 `SessionScope`。 */
171
+ session: SessionScope;
53
172
  /** 这次调用的 id(和 `calls` 里的、工具结果的 `toolCallId` 是同一个):从这次调用开出去的会话记在 `SessionLaunch.spawnedFrom` 里。 */
54
173
  toolCallId: string;
55
174
  cwd: string;
@@ -74,30 +193,102 @@ export interface ToolRun {
74
193
  /** 执行中的输出,推给界面。 */
75
194
  progress(chunk: string): void;
76
195
  /**
77
- * 动手之前问一句(写了 `asks` 的工具用)。经钩子 tool.approve 决定,要不要真的弹给人由那里的层定(内置插件 plume.approval:
78
- * 完全访问、外层放行的、答过「以后不再问」的直接放行,提权总是问人)。参数校验完、注定失败的已经报掉之后再问:
79
- * 不先问人一个没法成立的问题。用户停止这一轮时得到 `"reject"`。
196
+ * 动手之前问一句(写了 `asks` 的工具用)。完全访问、外层放行的、答过「以后不再问」的由本体直接放行,提权总是问人;其余经钩子
197
+ * tool.approve,要不要真的弹给人由那里的层定(内置插件 plume.approval 按帮我批准的风险规则放行)。参数校验完、注定失败的已经报掉
198
+ * 之后再问:不先问人一个没法成立的问题。用户停止这一轮时得到 `"reject"`。
199
+ * `subject` 不用加插件名前缀:本体把这里答的「以后不再问」记在注册这件工具的插件名下,只放行这个插件的工具再问同一个 `subject`,
200
+ * 别的插件、本体自己的读边界(`read:<项目>`)都不认它(见 `ApprovalRequest.subject`)。
80
201
  */
81
202
  ask(content: ApprovalContent): Promise<ApprovalDecision>;
203
+ /**
204
+ * `ask` 得到 `"reject"` 以后交给模型的话:说这次调用被人拒了,叮嘱别原样重试、别换个工具或命令达到同样效果。原样作为错误结果交回,
205
+ * 工具不自己另写一份。本体自己拒绝时(替没写 `asks` 的工具在执行前问的、`escalate`、`files.read`)用的是同一句叮嘱,模型在哪看到都认得出。
206
+ */
207
+ rejection: string;
208
+ /**
209
+ * 停下来等人给一份数据:回答一个问题、批准一份计划、填一张表。卡片由界面入口按 `kind` 登记(`ui.requestCard`),
210
+ * 没登记时产品画一张只能「不答」的通用卡。和 `ask` 同排一队、同占输入框上方那一格,子会话问时卡片压在上级会话的输入框上方。
211
+ * 不是权限:不经钩子 tool.approve,完全访问、「以后不再问」都不替人答。等 30 分钟,到时得到 `expired`;这一轮停了得到
212
+ * `stopped`。`payload` 要能结构化克隆(它要跨进程送到界面)。
213
+ */
214
+ request<T = unknown>(input: HumanRequestInput): Promise<HumanAnswer<T>>;
82
215
  /** 设置里现在的权限模式,用到才读:中途改了,下一个调用就照新的来。 */
83
216
  permissionMode(): Promise<PermissionMode>;
84
- /** 现在工具表里有哪些工具、各是哪个插件注册的(同名的只列生效的那个)。 */
85
- tools(): {
86
- name: string;
87
- plugin: string;
88
- }[];
217
+ /** 现在工具表里有哪些工具、各是哪个插件注册的、是哪一类(`ToolDefinition.category`;同名的只列生效的那个)。 */
218
+ tools(): RegisteredToolInfo[];
89
219
  /** 这一步回复里的全部调用(含这一次),按回复里的顺序。工具要知道这一步是不是只做了它这一件事时看它(todo 清单收尾)。 */
90
220
  calls: readonly ToolCall[];
91
- /** 钩子 tool.execute 的外层要求这次调用不改动工作区(见 `ToolExecution.readOnly`):跑命令的工具只在只读沙箱里跑。 */
221
+ /** 钩子 tool.execute 的外层要求这次调用不改动工作区(见 `ToolExecution.readOnly`):`exec` 的命令只在只读沙箱里跑,`escalate` 一律不批。 */
92
222
  readOnly?: boolean;
223
+ /**
224
+ * 这次调用的命令跑在哪个 shell、哪一档沙箱(见 `ShellInfo`),本体按权限模式和 `readOnly` 算好,和系统提示词里点名的是同一个。
225
+ * 提权不改它:提权的命令仍是照这个 shell 写的。用到才读设置,同 `permissionMode()`。
226
+ */
227
+ shell(): Promise<ShellInfo>;
228
+ /**
229
+ * 在这次调用的沙箱里用 `shell()` 的那个 shell 起一条命令,环境向登录 shell 要(从图标启动时也找得到用户装的工具)。
230
+ * 沙箱档由本体定,工具选不了:完全访问不设限,帮我批准只能写项目目录和临时目录,请求批准只读,`readOnly` 一律只读;
231
+ * 这次调用经 `escalate` 批过更宽的一档时跑在那一档。`cwd` 不给是这次调用的工作目录。机器上给不了这一档的沙箱时拒绝(Linux 没有后端),
232
+ * 不悄悄不加沙箱就跑。问不问人不归这里:要问的工具自己先经 `ask` 问。只在这次调用里用:调用答完以后再调就抛错,已经起来的进程不受影响。
233
+ *
234
+ * `recordChanges`:这条命令可能改文件,本体在它起来之前、结束以后(连同它留下的进程)各看一次 git 工作区,改了的文件记进改动快照
235
+ * (来源是命令,不能自动撤销,撤回时说清),记进这次调用的结果(`ToolOutput.changes`);这次调用答完时还在跑的,结果里记一句「结束后才记」,
236
+ * 之后读它输出的调用用 `files.includeChanges` 把记下的算进去。观测期间撤销等着它结束。能证明什么都不写的命令(只读的 `git log`)不用带:
237
+ * 前后各起几个 git、读一遍工作区里改动着的文件,不便宜;挂着不结束的命令(监视)不该带:撤销会一直等它。
238
+ */
239
+ exec(command: string, options?: {
240
+ cwd?: string;
241
+ env?: Record<string, string>;
242
+ recordChanges?: boolean;
243
+ }): Promise<RunProcess>;
244
+ /**
245
+ * 申请这次调用的命令跑在比权限模式更宽的一档(提权)。本体弹提权卡(`ApprovalRequest.escalation`),只认人答,或代管这个会话的插件
246
+ * 经 `ctx.sessions.respond` 转达;完全访问、外层放行、钩子 tool.approve、「以后不再问」都不替人答。批了,这次调用之后的 `exec`
247
+ * 都跑在那一档,调用答完就作废,什么都不累积。`readOnly` 下、没给理由、要的不比当前宽、人拒绝时不批,`message` 是交给模型的说明。
248
+ * `title`、`detail` 是卡上的标题和正文(要跑的命令),`justification` 是模型给的理由,原样给人看。只在这次调用里用。
249
+ */
250
+ escalate(request: {
251
+ mode: "workspace-write" | "danger-full-access";
252
+ justification: string;
253
+ title: string;
254
+ detail: string;
255
+ }): Promise<{
256
+ granted: true;
257
+ } | {
258
+ granted: false;
259
+ message: string;
260
+ }>;
261
+ /**
262
+ * 这次调用经本体读工作区:读边界和问人都在里面,见 `RunFiles`。读文件的工具、要读用户文件的插件工具都经它,
263
+ * 不自己判断哪里能读。只在这次调用里用:调用答完以后再调就抛错。
264
+ */
265
+ files: RunFiles;
93
266
  /**
94
267
  * 调用答完以后还在后台接着跑的事(后台命令、监视)把结果送回这个会话的渠道,见 `BackgroundChannel`。
95
268
  * 只有顶层会话有:插件的子会话(侧边聊天、子 Agent)没人等它们的后台结果,拿不到就只能让模型自己去读。
96
269
  */
97
270
  background?(): BackgroundChannel;
271
+ /**
272
+ * 登记一项调用答完以后还在跑的后台任务(后台命令、监视),见 `BackgroundJobInput`。本体据此:算会话闲没闲——顶层会话里
273
+ * 登记着、没结束、这次调用的送达渠道(`background`)还没作废的任务,在它结束之前会话都不算闲(`LaunchedSession.idle`、
274
+ * `ParentStatus`、`SessionSummary.background`),挂着的除外(`wakes` 的监视、经 `TrackedJob.stand` 标成长跑服务的);删会话、退出应用、登记它的插件停用时调 `stop(true)` 停掉;
275
+ * 子会话经 `ctx.sessions.start` 派出的那一次运行结束时停掉它还在跑的;经 `ctx.sessions.jobs` 列给界面(环境卡片、任务面板)。
276
+ * 人按停、撤回截掉这次调用时本体不停它(渠道作废,它不再算在等),停不停由工具定。只在这次调用里用:调用答完以后再调就抛错,
277
+ * 交回的 `TrackedJob` 不受影响。
278
+ */
279
+ track(job: BackgroundJobInput): TrackedJob;
280
+ /**
281
+ * 让这个会话以后再接着做:到点经这次调用的送达渠道送一条运行时送达进来(闲着开一轮,忙着在下一个步骤边界读到),那一行记在
282
+ * 注册这个工具的插件名下(`DeliveredReport.plugin`)。见 `WakeupInput`。没触发的唤醒不挡会话闲下来:它是挂着的,到点照样叫醒会话。
283
+ * 人按停止时全部取消(本体跟人说一声取消了几个),撤回、编辑重发截掉这次调用时取消它约的,删会话、退出应用、注册工具的插件停用时取消。
284
+ * 只有顶层会话有(同 `background`)。cron 表达式写错时抛错(message 说哪一段不对,给模型看);在 `expiresAt` 之前一次也对不上时
285
+ * 不登记,返回 `undefined`。只在这次调用里用。
286
+ */
287
+ schedule?(wake: WakeupInput): SessionWakeup | undefined;
98
288
  /**
99
289
  * 给另一个顶层会话寄一封信(`to` 是它的 id),见 `LetterReceipt`。只有顶层会话有,也只寄得到别的顶层会话:
100
- * 子会话不是谁的通信对象。发件方的 id、工作目录由本体照实填上。对方不在、是子会话、是自己时抛错。
290
+ * 子会话不是谁的通信对象。发件方的 id、工作目录由本体照实填上。请求的答复由本体配对、没答时由本体代答,往来链也由本体数,
291
+ * 见 `LetterReceipt`。对方不在、是子会话、是自己时抛错;往来链太长、太绕时抛错,message 是给模型的话(让它停下、回去告诉人)。
101
292
  * 只在这次调用里用:调用答完以后再调就抛错(回执的 `answer` 不受影响)。
102
293
  */
103
294
  letter?(to: string, letter: LetterInput): LetterReceipt;
@@ -114,6 +305,118 @@ export interface ToolRun {
114
305
  */
115
306
  parent?: ParentSession;
116
307
  }
308
+ /**
309
+ * `ToolRun.files`:读边界是本体的,一个路径能不能读、要不要问人,不管哪个工具来读都是同一个答案(两个答案就有一条绕过去的路:
310
+ * 一个工具拒了的换个工具读得到,模型就学会去绕)。
311
+ *
312
+ * 不问的:工作区、项目的其他源码目录(`ToolRun.projectRoots`)、各会话的临时目录、系统临时目录、工具链要读的系统目录(`/usr`、`/etc`……)、
313
+ * 人拖进来的文件(`ToolRun.allowedPaths`)。要问的:本机的密钥文件(Plume 的设置和凭据、ssh 私钥、云服务凭据、netrc、gh 的登录……,
314
+ * 在工作区里也问,批准只管这一个文件);别的地方(批准覆盖它所属的项目,往上走到仓库根,不到家目录)。问人经过闸(`kind` 是 `view`,
315
+ * `subject` 由本体定、记在本体名下:哪个工具来读都认同一份,插件经 `run.ask` 给同样的字符串也不算),完全访问、「以后不再问」照常生效;
316
+ * 人拒绝时 `message` 是交给模型的话。判定前追符号链接:工作区里一个指向外面的链接按它指向的地方判。
317
+ */
318
+ export interface RunFiles {
319
+ /**
320
+ * 要读的路径或资源地址。
321
+ * - 路径(相对 `cwd`、`~/`、Windows 上 shell 的几种写法):按上面的边界判,过了交回 `path`,读就读它(判定用的绝对路径,
322
+ * 工作区自己在链接后面时写成工作区下的路径)。不存在的不问(密钥除外),照样交回,由工具自己报「找不到」。
323
+ * - 有人登记了 scheme 的资源地址(见 `PluginResources`):不问人,落点已由 scheme 限在它自己的地方。交回读到的 `resource`
324
+ * (带行范围的已经切好,`scheme://` 是列表);没有这样东西、越界时 `ok: false`,`message` 是 scheme 的说明。
325
+ * `scratch://<路径>`(不带行范围)是临时目录里的一个文件,交回它的 `path`,按文件读。
326
+ * - `directory: true`:要的是一个目录(列目录),总是交回 `path`。地址交回 scheme 给的磁盘位置(`ResourceHandler.locate`),给不出的照路径判。
327
+ * 没人登记的 `foo://` 照路径判。
328
+ */
329
+ read(target: string, options?: {
330
+ directory?: boolean;
331
+ }): Promise<{
332
+ ok: true;
333
+ path: string;
334
+ resource?: undefined;
335
+ } | {
336
+ ok: true;
337
+ resource: Resource;
338
+ path?: undefined;
339
+ } | {
340
+ ok: false;
341
+ message: string;
342
+ }>;
343
+ /**
344
+ * 一行命令写明要读的路径(照这次调用的 shell 的语法拆,只认字面写的:`$DIR/key`、通配符、替换不猜;重定向的目标、heredoc 的正文不算),
345
+ * 按同一个边界判。同一个批准范围只问一次。全部不用问或都批准了是 `ok: true`;问到第一个被拒的就停,`message` 是交给模型的话。
346
+ * 跑命令的工具在审批、起命令之前调它:命令改不改东西是另一回事,`cat ~/.ssh/id_ed25519` 什么都不改,却是在读密钥。
347
+ */
348
+ readCommand(command: string): Promise<{
349
+ ok: true;
350
+ } | {
351
+ ok: false;
352
+ message: string;
353
+ }>;
354
+ /**
355
+ * 模型写的路径(相对 `cwd`、`~/`、Windows 上 shell 的几种写法)指的是哪个绝对路径,和读、写边界判定用的是同一个解析。
356
+ * 不判能不能读写、不追符号链接、不认资源地址。给模型看路径、往结果里写 `FileViewDetails.path` 时用,不自己再解析一遍。
357
+ */
358
+ resolve(path: string): string;
359
+ /**
360
+ * 替模型改一个文本文件:写边界、按文件排队、拿当前内容、写、记改动快照、撤销门闩一次做完,写成的记进这次调用的结果(`ToolOutput.changes`)。
361
+ *
362
+ * 写边界:不写 Plume 的数据目录和 `~/.plume`(工作区、同一脉的会话的临时目录在里面的除外),工作区里一个指向外面的链接不写
363
+ * (审批卡写的是字面路径,改的却是别处);工作区外不在这里拒,审批由工具问。附件(`allowedPaths`)不算能写。
364
+ * `edit` 拿到这个文件现在的样子(`path` 是真实路径,不存在时 `text`、`stamp` 是 null),在同一个队里决定写不写:要写就交回 `text`,
365
+ * 不写就不给;`value` 原样交回。审批由工具在 `edit` 里经 `ask` 问(diff 只有工具写得出来)。写之前本体再核对一遍:等审批时文件被改了、
366
+ * 换成了链接,不写并抛错。只认 UTF-8 文本,二进制、不是普通文件的抛错;撤销进行中抛错。抛的错都是交给模型的话。
367
+ * 交回的 `written` 是写完那一刻的修改时间和大小(没写时不给)。只在这次调用里用。
368
+ */
369
+ change<T>(path: string, edit: (current: {
370
+ path: string;
371
+ text: string | null;
372
+ stamp: FileStamp | null;
373
+ }) => Promise<{
374
+ text?: string;
375
+ value: T;
376
+ }>): Promise<{
377
+ value: T;
378
+ path: string;
379
+ written?: FileStamp;
380
+ }>;
381
+ /**
382
+ * 写一个资源地址。能写的只有本体的 `scratch://`(会话的临时目录,落点不出目录、软链出不去),别的 scheme 都是只读的:能写
383
+ * `skill://<名字>` 的模型就能改掉它该遵守的说明。不问人、不记改动快照(临时目录是会话自己的草稿)。成了交回地址在磁盘上的位置;
384
+ * 只读、越界、不是登记着的地址时 `ok: false`,`message` 是交给模型的话。只在这次调用里用。
385
+ */
386
+ write(address: string, content: string): Promise<{
387
+ ok: true;
388
+ path?: string;
389
+ } | {
390
+ ok: false;
391
+ message: string;
392
+ }>;
393
+ /**
394
+ * 结果截过以后,把没交给模型的那份原文存在会话的临时目录里,交回它的地址和位置(`SpillDetails` 的形状,直接放进
395
+ * `ToolOutput.details.spill`)。地址是 `artifact://<id>`,按 `tool` 和内容算:同一段原文每次换回同一个地址,后来重发的历史里
396
+ * 那句提示逐字不变。模型要读了经 `read` 按行、按窗口取回;会话删掉时一起清掉,重开应用以后还在。
397
+ *
398
+ * 每个会话最多留 30 份,超了丢最旧的。写不进去(磁盘满、会话没有临时目录)时抛错,由工具自己决定少说一句还是报错——
399
+ * 截断本身不该因此失败。不记改动快照、不问人:临时目录是会话自己的草稿。只在这次调用里用。
400
+ *
401
+ * 同步的:调用它的都在同步地生成一条结果(压缩剪一条结果、mcp 把服务器的回复换成交给模型的样子),内容已经在内存里,
402
+ * 写成异步只会把「这条结果长什么样」摊成两步,还得让这些路径都变成异步。写的是几百 KB 到磁盘。
403
+ */
404
+ spill(tool: string, content: string): {
405
+ address: string;
406
+ path: string;
407
+ characters: number;
408
+ };
409
+ /**
410
+ * 这次调用的结果也算上一条命令改的文件:`process` 是经 `exec`(`recordChanges`)起的进程或它留下的进程,可以是之前的调用起的(后台命令)。
411
+ * 命令已经结束就算上它记下的,还在跑就记一句「结束后才记」。读后台命令输出的调用用。不是这个会话里经 `exec` 观测的进程时什么都不做。
412
+ */
413
+ includeChanges(process: SandboxProcess): void;
414
+ }
415
+ /** 一个文件的修改时间和大小:工具判断读过以后文件变没变用。 */
416
+ export interface FileStamp {
417
+ mtimeMs: number;
418
+ size: number;
419
+ }
117
420
  /**
118
421
  * 工具的后台结果回到会话的路。送出的每段和同一时刻别的后台结果(子会话交回的、别的后台命令的)聚合一下,作为一条运行时送达
119
422
  * 进会话:会话闲着时开一轮,忙着时在下一个步骤边界(全部工具结果之后、下一次请求之前)读入。送达经钩子 prompt.submit(`synthetic`)。
@@ -124,35 +427,97 @@ export interface ToolRun {
124
427
  export interface BackgroundChannel {
125
428
  /**
126
429
  * 送一段给模型读的话,可以送多次。给函数时到真要送出那一刻才取:返回 `undefined` 就不送了(这期间模型已经自己读过)。
127
- * `delivery` 给界面把这条送达画成对话里的一行(「后台命令完成」),不是一个人发的气泡。渠道已作废时返回 false。
430
+ * `delivery` 给界面把这条送达画成对话里的一行(「后台命令完成」),不是一个人发的气泡,见 `DeliveryInput`。渠道已作废时返回 false。
128
431
  */
129
- report(text: string | (() => string | undefined), delivery?: ChannelDelivery): boolean;
432
+ report(text: string | (() => string | undefined), delivery?: DeliveryInput): boolean;
130
433
  /** 渠道作废时触发。 */
131
434
  readonly signal: AbortSignal;
132
435
  }
436
+ /** `ToolRun.track` 登记的一项后台任务:给人看的几样,和本体要用时调的两个方法。 */
437
+ export interface BackgroundJobInput {
438
+ command: string;
439
+ /** 这条命令是干什么的,人读的是它。 */
440
+ description?: string;
441
+ pid?: number;
442
+ /**
443
+ * 每有新输出就叫醒会话开一轮(监视命令),界面据此标出来,见 `BackgroundJobInfo.wakes`。这样的任务是挂着的:它的事随每次输出
444
+ * 送回会话,没有「做完」的那一刻,不挡会话闲下来(同 `TrackedJob.stand`)。
445
+ */
446
+ wakes?: boolean;
447
+ /** 给人看的尾部输出(任务面板、环境卡片找端口):不能算成模型读过,也不能消费它的送达。 */
448
+ output(): Promise<BackgroundJobOutput>;
449
+ /** 停掉它(`force` 是强杀)。停不掉就抛错。 */
450
+ stop(force: boolean): void;
451
+ }
452
+ /** `ToolRun.track` 交回的句柄。 */
453
+ export interface TrackedJob {
454
+ /** 本体给这项任务的 id,在应用里唯一:工具交给模型、界面按它找这项任务都用它。 */
455
+ readonly id: string;
456
+ /** 它结束了(退出,或起不来:`error`);不再算在等。可以再调,以最后一次的退出码为准。 */
457
+ done(exitCode: number | null, error?: string): void;
458
+ /**
459
+ * 它是挂着的:起来以后才认得出是长跑的服务(开发服务器监听上了端口),不会自己做完,不再挡会话闲下来。只能标上、不能撤回;
460
+ * 照旧登记、列出、结束时照样可以送达、照样被本体停掉。
461
+ */
462
+ stand(): void;
463
+ }
133
464
  /**
134
- * `BackgroundChannel.report` 在对话里画的那一行:后台命令结束、监视送来一批输出(`job`),定时唤醒到点(`wakeup`)。
135
- * 宿主只留这几个字段、拷一份;`session` 那一行只由宿主在交回时写,插件写不了。
465
+ * `ToolRun.schedule` 约的一次「以后再接着做」:`at` 或 `cron` 二选一。
466
+ * 本体管什么时候到点、到点时这件事上一次开的那一轮做完了没有(没做完就跳过这次,记着跳过了几次)、什么时候取消;
467
+ * 到点送进会话的话由约它的插件给(`message`),本体不替它说。
136
468
  */
137
- export type ChannelDelivery = Omit<DeliveredReport, "kind" | "plugin" | "incomplete"> & {
138
- kind: "job" | "wakeup";
139
- };
469
+ export interface WakeupInput {
470
+ /** 一次性:到这个时刻(毫秒时间戳)送。 */
471
+ at?: number;
472
+ /** 5 段 cron 表达式,本地时间;第一次至少在一分钟以后。 */
473
+ cron?: string;
474
+ /** cron 到点以后留着等下一次,直到 `expiresAt`;不给是只触发一次。 */
475
+ recurring?: boolean;
476
+ /** cron 过了这个时刻不再触发,第一次也要落在它之前;循环的记进 `SessionWakeup.expiresAt`。不给时不设限。 */
477
+ expiresAt?: number;
478
+ /** 到点时要接着做的事,见 `SessionWakeup.prompt`。 */
479
+ prompt: string;
480
+ reason?: string;
481
+ /**
482
+ * 到点时送进会话的话(运行时送达,给模型读)和对话里那一行(见 `DeliveryInput`)。`wakeup` 是这一刻的样子(`fired` 已经算上这一次),
483
+ * `next` 是下一次触发的时刻,`null` 表示这是最后一次;`skipped` 是上一次送达之后因为那一轮还没做完而跳过的次数。抛错时这次不送。
484
+ */
485
+ message(wakeup: SessionWakeup, info: {
486
+ next: number | null;
487
+ skipped: number;
488
+ }): {
489
+ text: string;
490
+ delivery?: DeliveryInput;
491
+ };
492
+ }
493
+ /**
494
+ * 一份后台结果在对话里画成的那一行,`BackgroundChannel.report`、`ReportOptions.delivery`、`LaunchedSession.reportBack` 给的都是它。
495
+ * 宿主只留这几个字段、拷一份(`details` 要能存成 JSON),记下是哪个插件交的(`DeliveredReport.plugin`),那一行交给这个插件经
496
+ * `ui.agentDelivery` 登记的组件画,没登记就画成宿主默认的一行(`title` 和收场)。`sessionId`(点了去哪个会话)插件写不了,
497
+ * 只有新会话交回时宿主填上。
498
+ */
499
+ export type DeliveryInput = Pick<DeliveredReport, "id" | "title" | "status" | "incomplete" | "inline" | "details">;
140
500
  /**
141
501
  * 一封信写什么(`ToolRun.letter`)。字段的意思见 `PeerLetter`:`from` 是发件方的地址,收件方回信时原样当作收件人;
142
- * 是请求、答复还是通知,答复对着哪几问,所在的往来链,都由寄信的插件定。
143
- * `guide` 是给收件方模型的处理说明(「标着 reply="expected" 的用 reply_session 回」),一次送达里一样的只写一遍。
502
+ * 是请求、答复还是通知由寄信的插件定,往来链由本体数(见 `LetterReceipt`)。
503
+ * - 答复(`answer`,插件写的一般是 `replied`)对着哪几封请求由本体认:收件方读到了、还没答的、这封信的收件方问它的请求,一封答复
504
+ * 全部答掉;`re` 不给时本体按这几封请求的开头填。一封也对不上(问的都答过了、根本没问过)时按通知送,`answer`、`re` 都不带。
505
+ * - `recipient` 只对请求有用:收件方的地址,写法和 `from` 一样由寄信的插件定。本体替收件方代答(见 `LetterReceipt`)时拿它当
506
+ * 答复的 `from`;不给就用收件方此刻的标题。
507
+ * - `guide` 是给收件方模型的处理说明(「标着 reply="expected" 的用某件工具回」),一次送达里一样的只写一遍。各个标记本身是什么意思
508
+ * (请求没答会被代答、`answer` 的几种值)本体自己会说,插件只说怎么用它的工具。
144
509
  */
145
- export type LetterInput = Pick<PeerLetter, "from" | "text" | "request" | "answer" | "re" | "chain"> & {
510
+ export type LetterInput = Pick<PeerLetter, "from" | "text" | "request" | "answer" | "re"> & {
511
+ recipient?: string;
146
512
  guide?: string;
147
513
  };
148
514
  /**
149
- * 信的下落。`read`:写进了收件方的对话;`opened` 是这封信(和同批的)叫起了收件方的一轮,不是插进一轮已经在跑的。
515
+ * 信的下落。`read`:写进了收件方的对话。
150
516
  * `dropped`:没读到——请求还没读到收件方的人就按了停(通知和答复不丢,留到它下一轮开头读,这期间没有着落),或钩子 prompt.submit
151
517
  * 拦下了这次送达。`closed`:收件方删掉了,或应用退出了。
152
518
  */
153
519
  export type LetterFate = {
154
520
  status: "read";
155
- opened: boolean;
156
521
  } | {
157
522
  status: "dropped";
158
523
  } | {
@@ -161,21 +526,41 @@ export type LetterFate = {
161
526
  /**
162
527
  * 寄出的一封信。信和后台结果走同一条路:收件方忙着时在下一个步骤边界读到,闲着时为它开一轮(停下过的也开:一封信是
163
528
  * 一件新的事),送达经钩子 prompt.submit(`synthetic`)。前面带一句本体的话:这不是用户说的,不构成用户的授权。
529
+ *
530
+ * 请求(`request: true`)总会有一封答复,由本体配对,不靠哪个插件记账:
531
+ * - 收件方答了:它寄回一封答复(见 `LetterInput`),或者寄信的插件经 `answer` 替它答了。
532
+ * - 读到它的那一轮收场了还没答(完成、人按停、出错、跑到步数上限、原地打转都算):本体拿那一轮读到它以后最后一段回复代答,
533
+ * `answer` 是 `unreplied`(太长的截掉,没以完成收场的前面注一句是怎么收场的,什么也没说的也说一句)。
534
+ * - 没读到:收件方的人先按了停、或钩子 prompt.submit 拦下了这次送达,本体代答 `dropped`;收件方删掉了(读没读到都算)代答 `closed`。
535
+ * - 发件方删掉了、应用退出了:不再答。
536
+ * 代答从收件方寄回发件方,`from` 是请求的 `recipient`,和收件方自己回的一样走信的路。答过的请求再答一次(收件方答了以后又回、
537
+ * 插件在本体代答以后再代答)什么也不寄:`answer` 返回 `undefined`,寄来的答复按通知送或不寄(见 `answer`)。
538
+ *
539
+ * 往来链防几个会话原地来回,也由本体数,不靠哪个插件记账:一封信叫起收件方的一轮(不是插进一轮已经在跑的),这一轮里经
540
+ * `ToolRun.letter` 寄出的信就接着它的链再加一跳;人在这一轮里开口以后从头算。代答、经回执 `answer` 替收件方答的接着它答的
541
+ * 那封信的链,同样往下数,所以收件方总不正面回答时也照样数到上限。一条链超过 28 跳、或同一个会话经手超过 10 次时
542
+ * `ToolRun.letter` 抛错不寄;代答和回执的 `answer` 一封答一封,不拦。
164
543
  */
165
544
  export interface LetterReceipt {
166
545
  /** 这封信的 id,记在收件方对话里那条消息的 `peer` 上。 */
167
546
  id: string;
168
547
  /** 信有了着落时兑现,见 `LetterFate`。 */
169
548
  read: Promise<LetterFate>;
549
+ /** 这封信作为答复答掉了几封请求(见 `LetterInput`);不是答复、或一封也没对上时是 0。 */
550
+ answered: number;
170
551
  /**
171
- * 替收件方回发件方一封信(收件方没用工具回、没读到、关了时插件代答):从收件方寄回给发件方,只能用一次。
172
- * 发件方已经不在时返回 `undefined`。
552
+ * 替收件方答这一封请求:从收件方寄回给发件方,只答这一封,`answer` 不给就是 `replied`。请求已经有了答复(收件方答了、本体代答了、
553
+ * 这里答过了)、两边有一方不在了、应用在退出时返回 `undefined`,什么也不寄。通知的回执也能用一次,寄回去的是一封通知。
173
554
  */
174
555
  answer(letter: LetterInput): LetterReceipt | undefined;
175
556
  }
176
557
  /** 父会话此刻在做什么,见 `ParentSession.status`。 */
177
558
  export interface ParentStatus {
178
- /** `running` 正在跑一轮;`background` 这一轮收了尾,它派出去的后台运行还没都回来;`idle` 真闲着。 */
559
+ /**
560
+ * `running` 正在跑一轮;`background` 这一轮收了尾,还在等会自己结束、结束后要叫醒它的事:派出去的后台运行没都回来、交回的结果还没送进去,
561
+ * 登记着的一次性后台任务(`ToolRun.track`)没结束;`idle` 闲着。挂着的不算:监视、开发服务器这类长跑的任务(`BackgroundJobInput.wakes`、
562
+ * `TrackedJob.stand`),约好的唤醒和 cron(`ToolRun.schedule`),它们到点、有输出照样叫醒会话。被人停掉、撤回而作废的(不会再把结果送回会话的)不算。
563
+ */
179
564
  state: "running" | "background" | "idle";
180
565
  /** 队里还排着几条(人排的和交过来的都算)。 */
181
566
  queued: number;
@@ -207,22 +592,91 @@ export interface ParentSession {
207
592
  carryOn(text: string): Promise<ParentStatus>;
208
593
  status(): ParentStatus;
209
594
  }
595
+ /** 工具表里生效的一件工具:名字、注册它的插件、它声明的种类和类别。见 `ToolRun.tools()`、`ctx.tools.list()`、`AgentService.listTools()`。 */
596
+ export interface RegisteredToolInfo {
597
+ name: string;
598
+ plugin: string;
599
+ kind?: ToolKind;
600
+ category?: ToolDefinition["category"];
601
+ }
602
+ /**
603
+ * 一个工具对本机的文件和命令做的是哪种活,见 `ToolDefinition.kind`:
604
+ * - `view` 读一个文件,`list` 列一个目录,`find` 按名字找文件,`search` 按内容搜文件;
605
+ * - `create` 写出一个文件(整个正文就在参数里),`modify` 改一个已有的文件;
606
+ * - `command` 跑一条命令,`watch` 起一条长跑的命令、边跑边把新输出送回会话,`job` 查看或停掉一个后台任务(起它的那次结果带着
607
+ * `BackgroundJobDetails`)。
608
+ *
609
+ * 不碰本机文件和命令的工具(查网页、搜会话转录、问人)不在这几种里:别的插件按种类挑「能读、能改、能跑命令」的工具时不该挑到它们。
610
+ */
611
+ export type ToolKind = "view" | "list" | "find" | "search" | "create" | "modify" | "command" | "watch" | "job";
210
612
  export interface ToolDefinition {
211
613
  name: string;
212
614
  description: string;
213
615
  /** JSON Schema。 */
214
616
  parameters: Record<string, unknown>;
617
+ /**
618
+ * 模型常把参数名写错时,宽容地认哪些:规范名(`parameters` 里的)→ 模型会写成的别名(`{ path: ["file", "filePath"] }`)。
619
+ * 宿主在这一步的回复写进会话之前就换好:规范名没给(或是 null)时取第一个给了的别名,别名一律从参数里删掉;
620
+ * tool.execute 的层改写参数往里交时又写成别名的,交给里层之前再换一次。于是 tool.execute、tool.approve 的各层、审批卡、转录、导出、压缩和工具自己看到、存下的都是规范名:按参数拦调用的守卫
621
+ * 不会因为模型换了个写法就读不到,工具和别处也不用各抄一份别名表。别名不能是 `parameters.properties` 里的名字。
622
+ */
623
+ aliases?: Record<string, readonly string[]>;
215
624
  /** 会改动工作区(写文件、执行命令):不和相邻的调用同时跑;没写 `asks` 的,宿主在执行前按参数问用户。 */
216
625
  mutating?: boolean;
217
626
  /** `mutating` 的工具里,这一次调用能证明什么都不改(只读的命令):可以和相邻的只读调用同时跑。 */
218
627
  readsOnly?(args: Record<string, unknown>): boolean;
219
628
  /** 工具自己在 execute 里经 `run.ask` 问(能给出 diff、命令原文),宿主不在执行前替它问。 */
220
629
  asks?: boolean;
630
+ /**
631
+ * 导出会话成 Markdown 时这一次调用写成的一行(英文,「Generate image a red fox」)。不给、返回空或抛错时写工具名。
632
+ * 导出在 agent 进程,够不着界面入口的 `ui.toolLine`,所以说法跟着工具定义走。
633
+ */
634
+ describe?(args: Record<string, unknown>): string | undefined;
635
+ /**
636
+ * 导出会话成 Markdown 时,这次调用的结果里要引出来的那段(子智能体交回的结论):写在调用列表后面,引成一段。
637
+ * 不给、返回空或抛错时不引,结果照旧不导出。
638
+ */
639
+ exportResult?(result: ToolResultMessage): string | undefined;
640
+ /**
641
+ * 这一次调用是替另一个工具调的(工具多了按需加载时,模型经一个总入口按名字调它们):返回被调的那个工具和交给它的参数。
642
+ * 导出会话成 Markdown 时这一行按被调的那个写(它的名字、它的 `describe`)。不是转调的调用返回 undefined。
643
+ */
644
+ forwards?(args: Record<string, unknown>): {
645
+ name: string;
646
+ args: Record<string, unknown>;
647
+ } | undefined;
648
+ /**
649
+ * 做的是哪种活(见 `ToolKind`)。一个工具做什么只在这里声明一次,宿主、界面和别的插件都从这里认,不认工具名
650
+ * (`run.tools()`、`ctx.tools.list()`、`AgentService.listTools()` 带着它):
651
+ * - 转录按它给默认的说法、图标、运行行和归类,回复里的文件签按 `view`、`create`、`modify` 的调用认读写过的文件(从参数里取什么见 `ui.toolLine`)。
652
+ * - `create`、`modify` 成功一次就是工作区有了实打实的进展:重复检测从头数,同样的调用之前反复得到同样的结果不再算原地打转。
653
+ * - 给模型一套收窄的工具表的插件按它留下工具(侧边聊天留读、搜、改、跑命令的,只读的子智能体留读和搜的),提示词要点名
654
+ * 「读文件的那个」「跑命令的那个」时按它在工具表里找它叫什么。
655
+ *
656
+ * 不给的不算这几种活:转录照没登记说法的工具说,自成一类。
657
+ */
658
+ kind?: ToolKind;
659
+ /**
660
+ * 这是哪一类能力,宿主和别的插件按它认,不认工具名(`run.tools()`、`ctx.tools.list()` 带着它):
661
+ * - `mcp`:MCP 服务的工具(连同按需搜索、调用它们的总入口)。上下文用量里单独算「MCP 工具」一段。
662
+ * - `checklist`:记进度清单的工具,结果的 `details` 带 `ChecklistDetails`。转录把它的调用当成一段活的记账:跟旁边那段活接成一段、
663
+ * 不自己占一行,一段里有别的活时总的那句不数它,回答之后被替人追问、只更新了清单的那一步不画,运行行说在排步骤。
664
+ * 别的插件要对模型说「用它记下步骤」时,按这一类在工具表里找它叫什么,找不到就不说。
665
+ * - `question`:停下来问人一个问题、等人回答的工具(经 `run.request`)。别的插件要对模型说「拿不准就问人」时同样按这一类找。
666
+ * - `web`:只读地查网页的工具(搜索网页、把网址读成文字),不碰工作区。给模型一套收窄的工具表的插件按这一类留下它们。
667
+ * - `delegate`:把一件活派给子智能体、等它交回结论的工具。别的插件要对模型说「派子智能体去做」时按这一类找它叫什么,
668
+ * 找不到就换成不派、自己做的说法;怎么填它的参数归它自己的说明。
669
+ *
670
+ * 不给的算普通工具。做的是哪种活(读、改文件、跑命令)用 `kind`,不在这里。
671
+ */
672
+ category?: "mcp" | "checklist" | "question" | "web" | "delegate";
221
673
  execute(args: Record<string, unknown>, run: ToolRun): Promise<ToolOutput>;
222
674
  }
223
675
  /** 钩子 tool.execute 的输入。 */
224
676
  export interface ToolExecution {
225
677
  sessionId: string;
678
+ /** 调用所在的会话,见 `SessionScope`。 */
679
+ session: SessionScope;
226
680
  cwd: string;
227
681
  call: ToolCall;
228
682
  /** `plugin` 是注册它的插件(read、write、edit、ls、glob、grep、bash 是内置插件 plume.tools 的)。 */
@@ -231,13 +685,18 @@ export interface ToolExecution {
231
685
  mutating: boolean;
232
686
  plugin: string;
233
687
  };
234
- /** 外层已经放行的理由。设了它,本体就不再问人(提权、`userOnly` 和层自己问的除外),也不经钩子 tool.approve。 */
688
+ /** 外层已经放行的理由。设了它,本体就不再问人(提权和层自己问的除外),也不经钩子 tool.approve。 */
235
689
  approved?: string;
236
690
  /**
237
- * 外层要求这次调用不改动工作区(计划模式)。交给工具的 `ToolRun.readOnly`:plume.tools 的 bash 不按权限模式选沙箱,
238
- * 一律在只读沙箱里跑,机器上给不了只读沙箱时不跑。只能收紧:里层不该把它去掉。
691
+ * 外层要求这次调用不改动工作区(计划模式)。只能收紧:里层不该把它去掉。本体照它执行,从设上它的那一层往里:
692
+ * - 写、改文件的工具(`ToolDefinition.kind` 是 `create`、`modify`)不再往里交、不执行,这次调用的结果就是 `refusal`
693
+ * (交给模型的话:为什么没跑、接下来怎么办);
694
+ * - 别的工具照常往里交、执行,拿到 `ToolRun.readOnly`:`run.exec` 的命令不按权限模式选沙箱,一律在只读沙箱里跑(机器上给不了时不跑),
695
+ * `run.escalate` 一律不批。
239
696
  */
240
- readOnly?: boolean;
697
+ readOnly?: {
698
+ refusal: string;
699
+ };
241
700
  signal: AbortSignal;
242
701
  /**
243
702
  * 问一次要不要放行,卡上是这次调用的参数和 `reason`。经钩子 tool.approve(标着 `byLayer`),由那里的层定要不要弹给人
@@ -250,14 +709,28 @@ export interface ToolExecution {
250
709
  * 系统提示词的一段。plume.prompt 给的段(按顺序):`identity`(身份)、`guidelines`(做事的准则)、`boundaries`(边界)、
251
710
  * `environment`(平台、shell、是不是 git 仓库、模型)、`tone`(语气)、`userInstructions`(全局指令)、
252
711
  * `projectInstructions`(项目里的 AGENTS.md 等)、`workspace`(工作目录和会话的临时目录)。plume.tools 再加一段 `resources`(资源地址的清单)。
712
+ * 段名是提供它的插件自己的:别的插件要找某一类段(身份、准则)时按 `category` 找,不认段名。
253
713
  */
254
714
  export interface PromptSection {
255
- /** 段的名字,插件按它替换、删除,或把自己的段插在它前后。插件自己的段用自己的名字。 */
715
+ /** 段的名字,提供它的插件按它替换、删除自己的段。插件自己的段用自己的名字。 */
256
716
  id: string;
257
717
  text: string;
258
718
  /**
259
- * 这段里哪几截来自哪个文件:`chars` 是那一截在 `text` 里的字数(连同包着它的标签)。宿主把 `projectInstructions`、
260
- * `projectMemory` 两段的来源按文件列进上下文用量的「记忆文件」,这些文件在会话边界外也能在文件面板里只读打开(`path` 给绝对路径);
719
+ * 这段是什么。宿主按它分段统计,别的插件按它找段,都不认段名:
720
+ * - `instructions`:人写给 agent 的全局指令(设置里填的,不分项目)。上下文用量里算系统提示词,使用统计 Token 构成里的「指令和记忆」。
721
+ * - `conventions`:项目里写给 agent 的指令文件(AGENTS.md 这类)。上下文用量的「项目指令」,Token 构成里的「指令和记忆」。
722
+ * - `memory`:插件记下、每轮带上的记忆(用户的、项目的都算)。上下文用量的「记忆」,Token 构成里的「指令和记忆」。
723
+ * - `skills`:技能目录(有哪些技能、各自什么时候用)。上下文用量的「技能」,Token 构成里的「技能与智能体」。
724
+ * - `catalog`:别的按需取用的能力目录(可以派的智能体、没放进工具表的工具)。上下文用量里算系统提示词,Token 构成里算「技能与智能体」。
725
+ * - `identity`:agent 是谁的那一段。子会话要换一个身份(子智能体、侧边聊天)时换掉带这一类的段,不认段名。宿主算系统提示词。
726
+ * - `guidelines`:做事的通用准则。要换一套准则、或往准则里补几条的插件按这一类找它。宿主算系统提示词。
727
+ *
728
+ * 不给的段算系统提示词。
729
+ */
730
+ category?: "memory" | "skills" | "catalog" | "identity" | "guidelines" | "instructions" | "conventions";
731
+ /**
732
+ * 这段里哪几截来自哪个文件:`chars` 是那一截在 `text` 里的字数(连同包着它的标签)。宿主把 `conventions` 和 `memory` 两类段的来源
733
+ * 按文件列进上下文用量的「记忆文件」,这些文件在会话边界外也能在文件面板里只读打开(`path` 给绝对路径);
261
734
  * 别的段带了不显示。改了 `text` 的层要么跟着改它,要么去掉它。
262
735
  */
263
736
  files?: Array<{
@@ -274,6 +747,8 @@ export interface PromptSection {
274
747
  */
275
748
  export interface ModelContext {
276
749
  sessionId: string;
750
+ /** 这次请求所在的会话,见 `SessionScope`。下面的 `parentId`、`owner` 和它里面的同名字段是同一件事。 */
751
+ session: SessionScope;
277
752
  /**
278
753
  * 这个会话挂在哪个会话下面,和 `SessionSummary.parentId` 同一件事:只有插件建的子会话(侧边聊天、子 Agent)有,
279
754
  * 顶层会话不带这个键。按会话藏工具、改提示词时看它,不用为此去 `ctx.sessions.list()`。
@@ -298,13 +773,18 @@ export interface ModelContext {
298
773
  id: string;
299
774
  name: string;
300
775
  };
301
- /** 这个会话现在的权限模式(含项目覆盖)。Windows 上受约束的命令跑在 PowerShell、完全访问跑在 Git Bash,系统提示词按它点名 shell。 */
776
+ /** 这个会话现在的权限模式(含项目覆盖)。 */
302
777
  permissionMode: PermissionMode;
303
778
  /**
304
779
  * 外层要求这一轮的命令都在只读沙箱里跑(计划模式,和 `ToolExecution.readOnly` 同一件事)。宿主不给;要设就挂在
305
- * plume.prompt(order -1000)外面,它按这一档点名 shell:Windows 上只读的命令跑 PowerShell,权限模式是完全访问也一样。
780
+ * 点名 shell 的那一层(plume.prompt,order -1000)外面:宿主据此重算 `shell`。
306
781
  */
307
782
  readOnly?: boolean;
783
+ /**
784
+ * 这一轮的命令跑在哪个 shell、哪一档沙箱(见 `ShellInfo`),和 `ToolRun.shell()` 是同一个,系统提示词按它点名 shell 和写法。
785
+ * 宿主按 `permissionMode` 和 `readOnly` 算,每层交接时重算:改它不作数,要收紧就设 `readOnly`。
786
+ */
787
+ shell: ShellInfo;
308
788
  /** 设置 › 个性化:语气和全局指令。 */
309
789
  personalization: Personalization;
310
790
  /** 发出去的系统提示词,就是 `sections` 依次用空行连起来。只读:每层交接时宿主按 `sections` 重拼,改它不作数。 */
@@ -329,6 +809,8 @@ export interface StepEnd {
329
809
  nudge: string;
330
810
  };
331
811
  sessionId: string;
812
+ /** 这一步所在的会话,见 `SessionScope`。 */
813
+ session: SessionScope;
332
814
  cwd: string;
333
815
  /** 这一轮的第几步,从 1 数。 */
334
816
  step: number;
@@ -358,6 +840,8 @@ export type StepVerdict = {
358
840
  /** 钩子 prompt.submit 的输入:一轮开始时,人发来的这条消息还没写进会话。给的是副本,改了不影响会话。 */
359
841
  export interface PromptSubmit {
360
842
  sessionId: string;
843
+ /** 这条消息要进的会话,见 `SessionScope`。 */
844
+ session: SessionScope;
361
845
  cwd: string;
362
846
  /** 这条消息的文字(命令发出去的是展开后的那段)。 */
363
847
  text: string;
@@ -368,8 +852,8 @@ export interface PromptSubmit {
368
852
  /** 会话里在它之前一条消息都还没有:这是会话的第一轮。 */
369
853
  fresh: boolean;
370
854
  /**
371
- * 这句话是插件经 `ctx.sessions.launch` 发的(它开出来的会话的第一句),不是人在输入框里发的:人为自己正在写的那条消息
372
- * 选的东西(输入框上的开关)不算到它头上。
855
+ * 这句话是插件发的,不是人在输入框里发的:经 `ctx.sessions.launch` 开出来的会话的第一句,或代管插件经 `ctx.sessions.post`
856
+ * 转进来的话。人为自己正在写的那条消息选的东西(输入框上的开关)不算到它头上。
373
857
  */
374
858
  launched: boolean;
375
859
  signal: AbortSignal;
@@ -397,6 +881,8 @@ export type PromptVerdict = {
397
881
  */
398
882
  export interface ContextCompact {
399
883
  sessionId: string;
884
+ /** 要压的会话,见 `SessionScope`。 */
885
+ session: SessionScope;
400
886
  cwd: string;
401
887
  /**
402
888
  * 这个会话现在用的模型。`tierLines` 是它的价格分档线(一次请求的输入越过这么多 token,整次按高一档计),从低到高;
@@ -431,6 +917,17 @@ export interface ContextCompact {
431
917
  messages: Message[];
432
918
  tools: ToolSpec[];
433
919
  };
920
+ /**
921
+ * `request` 有多满,本体按和上下文用量圆环同一套算法算好:最近一条落定回复服务商量的数(整份请求,含系统提示词和工具表)加上它之后
922
+ * 的消息的估算;`rewrittenAt` 之前(含)落定的回复不算。没有可用的时 `measured` 为假,`tokens` 是整份请求(系统提示词、工具表、
923
+ * 消息)的估算。
924
+ */
925
+ usage: {
926
+ tokens: number;
927
+ measured: boolean;
928
+ };
929
+ /** 本体估 token 的那一份算法,量一截历史、一段文字有多大(决定从哪切、摘要请求放不放得下)时用,不要自己再写一份。 */
930
+ estimate: TokenEstimate;
434
931
  /** 服务商说超长了,或人敲了 `/compact`:不管量出来多满都该压。 */
435
932
  force: boolean;
436
933
  /** 人敲的 `/compact`:`instructions` 是命令后面的话(希望摘要保留的内容,可以是空串)。 */
@@ -455,6 +952,17 @@ export interface ContextCompact {
455
952
  /** 压缩走到哪一步了,界面上那一行跟着变:写摘要、等重试、摘要没拿到正在本地整理。 */
456
953
  progress(event: CompactionProgress): void;
457
954
  }
955
+ /**
956
+ * 本体按字数估 token,按文字分别计:ASCII 约 3.5 个字符一个,其余字符(中日韩文字为主)一个一个,图片按定数。各家分词器不同,
957
+ * 一类文本上差个两三成常见(满是数字的命令输出会低估到一半),所以能用服务商量的数(`ContextCompact.usage`)就用它,
958
+ * 估算拿来补位、分比例、按比例校准。估出来的不取整,逐条、逐段估的可以直接相加。
959
+ */
960
+ export interface TokenEstimate {
961
+ messages(messages: readonly Message[]): number;
962
+ text(text: string): number;
963
+ /** 工具表:名字、说明、参数表。 */
964
+ tools(tools: readonly ToolSpec[]): number;
965
+ }
458
966
  /** `ContextCompact.progress` 的一步。 */
459
967
  export type CompactionProgress = {
460
968
  phase: "summarizing";
@@ -501,11 +1009,17 @@ export type CompactSummary = {
501
1009
  * 最里层照着传进来的 `request` 把审批卡弹给人、等人答。
502
1010
  *
503
1011
  * 权限模式的含义在本体,到不了这里:完全访问、tool.execute 外层设了 `approved`、`subject` 答过「以后不再问」的,本体直接放行。
504
- * 提权(`request.escalation`)和 `request.userOnly` 只能由人答:它们在完全访问下也到这里,层答放行不算数、本体照样弹卡,层答拒绝照算。
505
- * 内置插件 plume.approval 在这里按帮我批准(`auto`)的风险规则放行;没有任何层时每一次都问人。
1012
+ * 提权(`request.escalation`)只能由人答:它在完全访问下也到这里,层答放行不算数、本体照样弹卡,层答拒绝照算。
1013
+ * 弹出来的卡可以由人点,也可以由代管这个会话的插件经 `ctx.sessions.respond` 转达,那条路不经这里。
1014
+ * 工具要人给答案(`ToolRun.request`)不是审批,不到这里。
1015
+ * 内置插件 plume.approval 在里层(order 1000)按帮我批准(`auto`)的风险规则放行,命中规则的把理由写进卡片再交给里层;
1016
+ * 要在它之前替人答、或整个换掉这套规则的层挂在外面(order 小于 1000);只管真要弹给人的那些(它放行的到不了里面,到了的
1017
+ * 卡上带着它写的理由)的层挂在里面(order 大于 1000)。没有任何层时每一次都问人。
506
1018
  */
507
1019
  export interface ToolApproval {
508
1020
  sessionId: string;
1021
+ /** 调用所在的会话,见 `SessionScope`。 */
1022
+ session: SessionScope;
509
1023
  cwd: string;
510
1024
  /** 这个会话能当草稿区用的临时目录,同 `ToolRun.scratchDirs`;帮我批准把写进、删掉这些里面的东西算作正当干活。 */
511
1025
  scratchDirs?: readonly string[];
@@ -520,28 +1034,11 @@ export interface ToolApproval {
520
1034
  request: ApprovalContent;
521
1035
  /** 是钩子 tool.execute 的某一层自己要问(`ToolExecution.ask`),不是工具问的:没有 `subject`,只能答这一次。 */
522
1036
  byLayer?: boolean;
523
- /** 这次调用所在项目现在的权限模式。完全访问只有提权和 `userOnly` 会带到这里。 */
1037
+ /** 这次调用所在项目现在的权限模式。完全访问只有提权会带到这里。 */
524
1038
  permissionMode: PermissionMode;
525
1039
  /** 用户停止这一轮时中止。 */
526
1040
  signal: AbortSignal;
527
1041
  }
528
- /**
529
- * 钩子 session.rewind 的输入:人撤回或编辑重发,要截掉从某条人发的消息起的那段对话。截断由宿主做,这里问的是那段里
530
- * 插件记着的改动(文件改动等)能不能、要不要一起退回到截断点之前。
531
- *
532
- * 先以 `restore: false` 问一次,只演算:答那段改过哪些文件、各自能不能安全退回,界面据此问人。人选了连文件一起退,宿主在截断
533
- * 之前以 `restore: true` 再问一次:重新校验、真的写回去;写不回去就抛错(message 原样给人看,按界面语言写),这时宿主不截对话。
534
- * 写回要全有或全无:一个文件不能安全退回,就一个都不写,已经写了的退回原样再抛错。
535
- * 只截对话、不动文件时不问。最里层答 `{ files: [], unrecorded: false }`;每一层把自己的结论并进 `next` 的结论里。
536
- */
537
- export interface SessionRewind {
538
- sessionId: string;
539
- cwd: string;
540
- /** 要截掉的那段:从被撤回、编辑的那条人发的消息起到最后,副本。 */
541
- messages: Message[];
542
- /** 真的写回去,还是只演算。 */
543
- restore: boolean;
544
- }
545
1042
  /** 钩子名 → [输入, 输出]。 */
546
1043
  export interface Hooks {
547
1044
  "tool.execute": [ToolExecution, ToolOutput];
@@ -550,7 +1047,6 @@ export interface Hooks {
550
1047
  "prompt.submit": [PromptSubmit, PromptVerdict];
551
1048
  "context.compact": [ContextCompact, CompactSummary];
552
1049
  "tool.approve": [ToolApproval, ApprovalDecision];
553
- "session.rewind": [SessionRewind, RevertPlan];
554
1050
  }
555
1051
  /** 输入框 `/` 菜单里的一条命令。 */
556
1052
  export interface SlashCommand {
@@ -749,6 +1245,25 @@ export interface ModelAccess {
749
1245
  */
750
1246
  usage(): Promise<UsageScan>;
751
1247
  }
1248
+ /**
1249
+ * 插件的 `ctx.secrets`:把要存的密钥(令牌、密码、请求头里的凭据)加密成一段文本,插件把它写回自己的配置文件。
1250
+ * AES-256-GCM,钥匙由本体从它给供应商 key 用的那把(`vault.key`)按插件名派生:各插件的密文互相解不开。
1251
+ *
1252
+ * 只管加解密,不替插件存:密文跟着它所属的那条配置走,删掉配置就是删掉密钥,不用另外清。
1253
+ * 挡的是配置文件被同步、拷走、贴进问题报告时带出明文,以及一个插件的密文被抄进另一个插件的配置后被它解开;
1254
+ * 挡不住同一台机器上同一个用户的程序——钥匙就在数据目录里,插件也都跑在同一个进程里。
1255
+ */
1256
+ export interface PluginSecrets {
1257
+ /** 加密一个值,返回带固定前缀的文本(`isSealed` 认得出),能放进 JSON 之类的任何文本里。 */
1258
+ seal(value: string): Promise<string>;
1259
+ /**
1260
+ * 解回来。不是 `seal` 出来的、不是这个插件封的、或者这把钥匙打不开(钥匙丢了换了新的、密文来自别的机器)时是 null,当成「要重新填」。
1261
+ * 钥匙文件在却读不出来时抛错:这时不能当成「没了」,否则调用方把空值写回去,等钥匙又读得出来时已经没东西可解了。
1262
+ */
1263
+ unseal(sealed: string): Promise<string | null>;
1264
+ /** 这段文本是不是 `seal` 出来的样子:手写进配置的明文和密文分得开。 */
1265
+ isSealed(value: string): boolean;
1266
+ }
752
1267
  /**
753
1268
  * 会话的媒体目录:会话里的图片挪出消息后按内容(sha1)存在这里,消息里只留名字(`ImageContent.media`)。
754
1269
  * 存进去的名字和会话存图时起的一样,同一张图不会存两份。
@@ -783,11 +1298,8 @@ export interface SessionRun {
783
1298
  export interface ReportOptions {
784
1299
  /** 和本插件同一次送达里别的几份合成一段,见 `ReportFrame`。 */
785
1300
  frame?: ReportFrame;
786
- /**
787
- * 给界面把这份结果画成对话里的一行(「消息来自 @explore」),不是一个人发的气泡。不带 `kind`:那一行由本插件经
788
- * `ui.agentDelivery` 画(宿主记下是哪个插件交的)。不给就只有模型看得到。
789
- */
790
- delivery?: Omit<DeliveredReport, "kind" | "command" | "exitCode" | "monitor" | "plugin">;
1301
+ /** 给界面把这份结果画成对话里的一行(「消息来自 @explore」),不是一个人发的气泡,见 `DeliveryInput`。不给就只有模型看得到。 */
1302
+ delivery?: DeliveryInput;
791
1303
  }
792
1304
  /**
793
1305
  * `ReportOptions.frame`。几份结果落在同一次送达里时,同一个插件带 `frame` 的合成一段:`head` 写一次,
@@ -837,22 +1349,31 @@ export interface SessionLaunch {
837
1349
  };
838
1350
  /** 标题;不给就取第一句话的开头。 */
839
1351
  title?: string;
1352
+ /**
1353
+ * 开出来的会话由本插件代管(记进摘要的 `steward`):之后本插件能经 `post` 往里转话、经 `respond`、`answer` 替人答它和它下面的子会话
1354
+ * 弹的卡,直到人在会话菜单里停止代管。会话照旧是人的顶层会话:进侧栏,人能接着聊,权限照这个会话的设置。
1355
+ */
1356
+ steward?: boolean;
840
1357
  }
841
1358
  /** `SessionAccess.launch` 开出来的会话。 */
842
1359
  export interface LaunchedSession {
843
1360
  session: SessionSummary;
844
1361
  /** 第一轮结束时给出它为什么结束。 */
845
1362
  done: Promise<TurnStop>;
846
- /** 它第一次真闲下来时兑现,见 `SessionAccess.launch`;带上那时的收场和最后一段回复,交回用它。 */
1363
+ /**
1364
+ * 它第一次真闲下来时兑现,见 `SessionAccess.launch`;带上那时的收场和最后一段回复,交回用它。还在等一次性的后台命令时不兑现;
1365
+ * 挂着的监视、认出来的开发服务器、约着还没触发的唤醒不挡它(见 `ParentStatus`),之后它们叫醒会话做的几轮不算进这一次。
1366
+ */
847
1367
  idle: Promise<LaunchedIdle>;
848
1368
  /**
849
1369
  * 把它做出来的结果交回开出它的那个会话(`spawnedFrom`),只能交一次:`text` 是写给模型的话,作为一条运行时送达写进那个会话
850
- * (前面带着「不是用户说的话」的抬头),界面上画成「来自新会话 · 标题」一行,点了到这个会话;`status` 是它做成了没有。
1370
+ * (前面带着「不是用户说的话」的抬头);`delivery` 是对话里那一行(见 `DeliveryInput`),宿主给它填上这个会话的 id
1371
+ * (`DeliveredReport.sessionId`),默认画成「来自新会话 · 标题」,点了到这个会话。
851
1372
  * 那个会话闲着(人按停过也算,这是一件新的事)就为它开一轮,忙着在两步之间读到。
852
1373
  * 没有 `spawnedFrom`、那个会话删了或归档了、已经交过、正在退出时不送,返回 false。
853
1374
  * `spawnedFrom.toolCallId` 不是本插件的工具在那个会话里的一次调用(或撤回时被截掉了)、插件已经停用时抛错。
854
1375
  */
855
- reportBack(text: string, status: "done" | "failed"): boolean;
1376
+ reportBack(text: string, delivery: DeliveryInput): boolean;
856
1377
  }
857
1378
  /** `LaunchedSession.idle` 兑现时的样子。 */
858
1379
  export interface LaunchedIdle {
@@ -867,6 +1388,8 @@ export type SessionEndReason = "deleted" | "archived" | "moved" | "quit";
867
1388
  * 子会话复用产品的循环、审批、存储和事件,插件只能运行自己拥有的会话。
868
1389
  * 不开放任意向用户会话发送消息的能力:插件够得着的会话都从宿主给的句柄来,目标不由插件随手填。
869
1390
  * - 新开的会话:`launch` 开一个新的顶层会话、发出第一句话,之后它就是人的会话,插件不能再往里发;
1391
+ * - 代管的会话:`launch({ steward: true })` 开出来的那些,人没停止代管时,开它的插件能经 `post` 往里转话、经 `respond`、`answer`
1392
+ * 替人答它和挂在它下面的子会话弹的卡;别的会话(人已有的、别的插件代管的)不行,也不能事后认领;
870
1393
  * - 开出它的那个会话:`LaunchedSession.reportBack` 交回一次,来处要是本插件的工具在那里的一次调用;
871
1394
  * - 调用所在的会话:`ToolRun.background` 送后台结果、`ToolRun.archiveAfterTurn` 收尾后归档;
872
1395
  * - 子会话的父会话:插件自己建的子会话里,它注册的工具的那次调用拿得到 `ToolRun.parent`(排活、停下、接着做),
@@ -877,12 +1400,28 @@ export type SessionEndReason = "deleted" | "archived" | "moved" | "quit";
877
1400
  * 身份和工具限制由插件经 model.context、tool.execute 管理。
878
1401
  */
879
1402
  export interface SessionAccess {
880
- /** 只读观察请求、工具开始、审批、重试和运行状态;副本不含配置密钥,停用时自动撤下。 */
1403
+ /**
1404
+ * 只读观察请求、工具开始、审批、重试和运行状态;副本不含配置密钥,停用时自动撤下。
1405
+ * 只收到和本插件有关的会话的:本插件建的(`owner` 是它的子会话、独立会话),本插件代管的会话和挂在它下面的子会话,
1406
+ * 本插件经 `launch` 开出、还没兑现 `idle` 的会话。要看全部会话的(诊断轨迹、按每个会话的运行状态做事)在 `Plugin.observe` 声明 `"all"`。
1407
+ */
881
1408
  observe(listener: (observation: SessionObservation) => void): void;
882
- /** 全部会话的摘要(含归档的、子会话),最近活动的在前。 */
1409
+ /** 全部会话的摘要(含归档的、子会话),最近活动的在前。只为找一个会话时用 `get`。 */
883
1410
  list(): Promise<SessionSummary[]>;
1411
+ /** 一个会话的摘要;没有这个会话(删掉了、id 不对)时是 undefined。给钩子之外拿不到 `SessionScope` 的地方(`serve` 的方法、事件回调)用。 */
1412
+ get(sessionId: string): Promise<SessionSummary | undefined>;
884
1413
  /** 一个会话存下的全部消息,是会话里的原文,不受压缩影响。没有这个会话时抛错。 */
885
1414
  messages(sessionId: string): Promise<Message[]>;
1415
+ /** 会话里登记过的后台任务(`ToolRun.track`),结束了的也在,按登记的先后;没有这个会话、没登记过时是空数组。 */
1416
+ jobs(sessionId: string): BackgroundJobInfo[];
1417
+ /** 叫一项后台任务停下(`force` 是强杀);没有这一项、已经结束时返回 false,停不掉时抛错。 */
1418
+ stopJob(sessionId: string, jobId: string, force: boolean): boolean;
1419
+ /** 一项后台任务的尾部输出,见 `BackgroundJobOutput`;没有这一项时抛错。 */
1420
+ jobOutput(sessionId: string, jobId: string): Promise<BackgroundJobOutput>;
1421
+ /** 会话里还没触发的唤醒(`ToolRun.schedule`),按下一次触发的先后;没有时是空数组。 */
1422
+ wakeups(sessionId: string): SessionWakeup[];
1423
+ /** 取消一个还没触发的唤醒;已经触发了、不存在时返回 false。 */
1424
+ cancelWakeup(sessionId: string, id: string): boolean;
886
1425
  /**
887
1426
  * 现在就压缩这个会话(人敲了 `/compact`):照常经钩子 context.compact(带 `force` 和 `manual`),转录里画一行
888
1427
  * `/compact` 和它的进度、结果。任何会话都行(压缩不替人说话,只改模型看到的历史)。会话正在跑、正在压、没有可用的模型、
@@ -942,18 +1481,50 @@ export interface SessionAccess {
942
1481
  }): Promise<SessionSummary>;
943
1482
  /**
944
1483
  * 开一个新会话并发出第一句话(定时任务到点了、人在插件界面上点了「在新会话里执行」)。新会话和人在侧栏新建的一样:
945
- * 进侧栏、不记在插件名下(没有 `owner`),人能接着聊,插件之后不能再往里发、也停不了删不了它。第一句话照常经
1484
+ * 进侧栏、不记在插件名下(没有 `owner`),人能接着聊,插件之后不能再往里发、也停不了删不了它(`steward` 开出来的除外:
1485
+ * 本插件能往里转话、替人答卡,见 `SessionLaunch.steward`)。第一句话照常经
946
1486
  * `prompt.submit`,可能被拦下(这一轮按完成收场)。只能开新的;对已有会话只有 `reportBack` 往开出它的那个会话交回一次。
947
1487
  * 建好就返回:`session` 是新会话的摘要,`done` 在这第一轮结束时给出它为什么结束(之后人接着聊的几轮不算)。
948
- * `idle` 在它第一次真闲下来时兑现:第一轮结束,它派出去的后台运行都回来了、送回的结果唤起的几轮也做完了,它经 `ToolRun.letter`
949
- * 问出去的请求(`request: true`)也都有了答复、答复唤起的几轮也做完了。请求有了答复:经回执的 `answer` 回了信,或对方读到以后
950
- * 寄回一封带 `answer` 的信(答掉它读到的全部);对方删掉、寄信的插件停用了也不再等。
1488
+ * `idle` 在它第一次真闲下来时兑现:第一轮结束,它派出去的后台运行都回来了、送回的结果唤起的几轮也做完了,登记着的一次性后台任务
1489
+ * 都结束了(被人停掉、撤回而作废的不算在等;挂着的监视、开发服务器、约好的唤醒和 cron 不等,见 `ParentStatus`),它经 `ToolRun.letter`
1490
+ * 问出去的请求(`request: true`)也都有了答复、答复唤起的几轮也做完了。请求总会有答复(见 `LetterReceipt`):对方答了,
1491
+ * 或者本体在对方没答时代答。
951
1492
  * 项目、目录、模型、思考档位、`spawnedFrom` 不对时抛错,message 是给人看的话(按界面语言)。
952
1493
  */
953
1494
  launch(options: SessionLaunch): Promise<LaunchedSession>;
1495
+ /**
1496
+ * 往本插件代管的会话里转一句话(外面的人经插件发来的消息):排进这个会话的队(和人在输入框里排的同一条,落盘),
1497
+ * 会话闲着就当场开一轮,正在跑就排着,不插进这一轮。气泡标着来自本插件(`UserMessage.origin` 是 `plugin`,`fromPlugin` 是本插件),
1498
+ * 模型读到的前面多一句本体写的来处说明:这是代管插件转来的,不是人在 Plume 的输入框里打的。权限、审批照这个会话的设置,不因来处而变。
1499
+ * `started` 是排上那一刻它就开始了。会话不在、不是本插件代管的(没代管过、人停止了代管)、是子会话(子会话里的一轮归建它的插件发起)、
1500
+ * 已归档、话为空时抛错。
1501
+ * 回复和收场经 `observe`(`response`、`turnEnd`)和 `messages` 读;哪些转出去由插件自己定。
1502
+ */
1503
+ post(sessionId: string, message: string): Promise<{
1504
+ started: boolean;
1505
+ }>;
1506
+ /**
1507
+ * 替人答本插件代管的会话里弹着的那张审批卡(`approval` 事件、`SessionView.approval` 里第一张,按 `toolCallId` 认):`once` 放行这一次,
1508
+ * `reject` 拒绝;对等人给数据的卡(`HumanRequest`)`reject` 是不答。和人在桌面上点卡同一条路,提权也算:人和插件谁先答算谁的,
1509
+ * 另一边的卡随之收起。「以后不再问」改的是人的设置,插件答不了。卡上的东西怎么转给外面的人、答的人是不是该答的人,由插件负责确认。
1510
+ * 挂在代管会话下面的子会话(模型派的子智能体、人开的侧边聊天,不管是哪个插件建的)弹的卡也能这样答,`sessionId` 传子会话的 id
1511
+ * (`approval` 事件带的那个);看的是父会话此刻是不是本插件代管着。
1512
+ * 答上了返回 true;那张卡已经不在(答过了、过期了、这一轮停了)、不是这一张时返回 false。会话不是本插件代管的(子会话:父会话不是)时抛错,
1513
+ * 人停止代管以后也抛错:那时还弹着的卡只能人答。
1514
+ * 这次调用的结果里记着是哪个插件代答的(`ToolResultMessage.answeredBy`),模型读到的结果后面也写明这一点。
1515
+ */
1516
+ respond(sessionId: string, toolCallId: string, decision: "once" | "reject"): Promise<boolean>;
1517
+ /**
1518
+ * 替人交回本插件代管的会话(或它下面的子会话)里弹着的那张等人给数据的卡(`ToolRun.request`:提问、表单、计划)要的数据,同 `AgentService.answer`。
1519
+ * `value` 由工具自己核。能答哪些会话、返回值、抛错和记下代答者同 `respond`。
1520
+ */
1521
+ answer(sessionId: string, toolCallId: string, value: unknown): Promise<boolean>;
954
1522
  /** 可用模型目录,只含名称、窗口和思考档位。 */
955
1523
  models(): Promise<SessionModel[]>;
956
- /** 开始自己的子会话的一轮,立即交回句柄;首次运行固定实际模型与思考档位,续跑沿用。父轮正常结束不取消,明确停止父会话则取消。 */
1524
+ /**
1525
+ * 开始自己的子会话的一轮,立即交回句柄;首次运行固定实际模型与思考档位,续跑沿用。父轮正常结束不取消,明确停止父会话则取消。
1526
+ * 这一次运行结束时,本体停掉这个子会话还在跑的后台任务(`ToolRun.track`):子会话收不到后台送达,留下来就没人管了。
1527
+ */
957
1528
  start(sessionId: string, message: string | SessionMessage): Promise<SessionRun>;
958
1529
  /** 等待自己的后台运行结束,失败和停止也正常交回原因。 */
959
1530
  wait(runId: string): Promise<TurnStop>;
@@ -1225,6 +1796,8 @@ export interface PluginContext {
1225
1796
  register(tool: ToolDefinition): () => void;
1226
1797
  /** 现在登记着的全部工具名(各插件的,同名只算一个),按登记先后。给要让人挑工具的插件用。 */
1227
1798
  names(): string[];
1799
+ /** 同 `ToolRun.tools()`:生效的工具、注册它的插件、它声明的类别。在钩子里按类别找工具(「记清单的那个叫什么」)时用。 */
1800
+ list(): RegisteredToolInfo[];
1228
1801
  };
1229
1802
  /** order 小的在外层,默认 0;相同时按注册先后。 */
1230
1803
  hook<N extends HookName>(name: N, layer: HookLayer<N>, options?: {
@@ -1243,6 +1816,14 @@ export interface PluginContext {
1243
1816
  readonly model: ModelAccess;
1244
1817
  /** 读写会话的媒体目录,见 `MediaAccess`。 */
1245
1818
  readonly media: MediaAccess;
1819
+ /** 把插件要存的密钥加密成一段文本、再解回来,见 `PluginSecrets`。本体提供,别的插件换不掉。 */
1820
+ readonly secrets: PluginSecrets;
1821
+ /** 资源地址(`scheme://路径`):登记自己的 scheme、存大块原文换地址,见 `PluginResources`。本体提供,别的插件换不掉。 */
1822
+ readonly resources: PluginResources;
1823
+ /** 在宿主上读工作区仓库,不执行仓库能控制的程序,见 `PluginWorkspace`。本体提供,别的插件换不掉。 */
1824
+ readonly workspace: PluginWorkspace;
1825
+ /** 本体记下的改动快照汇总和撤销,见 `PluginChanges`。本体提供,别的插件换不掉。 */
1826
+ readonly changes: PluginChanges;
1246
1827
  /** 读会话,见 `SessionAccess`。 */
1247
1828
  readonly sessions: SessionAccess;
1248
1829
  /** 驱动界面入口画的网页(`<webview>`),见 `WebPages`。 */
@@ -1320,43 +1901,19 @@ export interface InstalledPlugin {
1320
1901
  * 仓库外的插件给自己的服务用声明合并加一项:`declare module "@plume/contract" { interface Services { … } }`。
1321
1902
  */
1322
1903
  export interface Services {
1323
- /** 与内置文件工具共用读取授权、项目根和会话批准记录。 */
1324
- "file.read": {
1325
- authorize(run: ToolRun, path: string): Promise<{
1326
- ok: true;
1327
- absolute: string;
1328
- } | {
1329
- ok: false;
1330
- message: string;
1331
- }>;
1332
- };
1333
- "background.jobs": BackgroundJobsService;
1334
- "session.wakeups": SessionWakeupsService;
1335
- sandbox: SandboxService;
1336
- "file.changes": FileChangesService;
1337
- "project.files": ProjectFilesService;
1338
- resources: ResourcesService;
1339
1904
  "session.worktree": SessionWorktreeService;
1340
- secrets: SecretsService;
1905
+ "session.plan": SessionPlanService;
1341
1906
  }
1342
1907
  /**
1343
- * 服务 `secrets`:把插件要存的密钥(令牌、密码、请求头里的凭据)加密成一段文本,插件把它写回自己的配置文件。
1344
- * 内置插件 plume.secrets 提供:AES-256-GCM,钥匙在它的数据目录。
1345
- *
1346
- * 只管加解密,不替插件存:密文跟着它所属的那条配置走,删掉配置就是删掉密钥,不用另外清。
1347
- * 挡的是配置文件被同步、拷走、贴进问题报告时带出明文;挡不住同一台机器上同一个用户的程序——插件都跑在同一个进程里,
1348
- * 谁拿到服务都解得开谁的密文。
1908
+ * 服务 `session.plan`:一个会话照着做的、人批准过的计划。内置插件 plume.plan 提供。
1909
+ * 要把「这段活原本批的是什么」带到别处(审阅这个会话的改动)时用它,不读交计划那次调用的结果。
1349
1910
  */
1350
- export interface SecretsService {
1351
- /** 加密一个值,返回带固定前缀的文本(`isSealed` 认得出),能放进 JSON 之类的任何文本里。 */
1352
- seal(value: string): Promise<string>;
1911
+ export interface SessionPlanService {
1353
1912
  /**
1354
- * 解回来。不是 `seal` 出来的、或者这把钥匙打不开(钥匙丢了换了新的、密文来自别的机器)时是 null,当成「要重新填」。
1355
- * 钥匙文件在却读不出来时抛错:这时不能当成「没了」,否则调用方把空值写回去,等钥匙又读得出来时已经没东西可解了。
1913
+ * 这个会话照着做的那份计划:它自己最近批准、没交给新会话执行的那份;没有的话,它是计划「在新会话里执行」开出来的,
1914
+ * 就是交给它的那份。正文连同批准时附上的注意事项,和交给模型的一样。没有是 null;没有这个会话时抛错。
1356
1915
  */
1357
- unseal(sealed: string): Promise<string | null>;
1358
- /** 这段文本是不是 `seal` 出来的样子:手写进配置的明文和密文分得开。 */
1359
- isSealed(value: string): boolean;
1916
+ approved(sessionId: string): Promise<string | null>;
1360
1917
  }
1361
1918
  /**
1362
1919
  * 服务 `session.worktree`:让开出来的新会话在它自己的 git 工作树和分支里跑,不动项目的检出。内置插件 plume.git 提供。
@@ -1374,16 +1931,81 @@ export interface SessionWorktreeService {
1374
1931
  toolCallId: string;
1375
1932
  }): () => void;
1376
1933
  }
1377
- /** 服务 `project.files`:项目里该看哪些文件。内置插件 plume.tools 提供,它的 glob、grep 用的也是这一份。 */
1378
- export interface ProjectFilesService {
1934
+ /**
1935
+ * `ctx.workspace`:在宿主进程里读工作区仓库。工作区是沙箱可写的,仓库的配置(`.git/config`、`.gitattributes`)也是:直接跑 git 会替沙箱里的命令
1936
+ * 执行它写进去的程序(`core.fsmonitor`、过滤器、钩子),所以本体在一个读不到仓库配置的隔离视图里问 git。插件要列项目文件就用它,不自己跑 `git ls-files`。
1937
+ */
1938
+ export interface PluginWorkspace {
1939
+ /**
1940
+ * `root` 之下 git 认为该看的文件(已跟踪且还在磁盘上的,加上未跟踪且没被忽略的;隐藏的也算),绝对路径,以 `root` 开头。
1941
+ * 不在 git 仓库里、或 git 答不上来时是 null,由调用方自己走目录。不问人:要不要读 `root` 由调用方先经 `ToolRun.files.read` 判。
1942
+ */
1943
+ files(root: string): Promise<string[] | null>;
1379
1944
  /**
1380
- * `root` 之下 git 认为该看的文件(已跟踪且还在磁盘上的,加上未跟踪且没被忽略的),绝对路径,以 `root` 开头。
1381
- * 不在 git 仓库里、或 git 答不上来时是 null,由调用方自己走目录。问 git 时不执行仓库能控制的程序(`core.fsmonitor`、过滤器、钩子)。
1945
+ * 在同一个隔离视图里跑一组 git 命令,结束即删:HEAD、引用、分支的上游和远端地址都照着真实仓库抄好,分支、ahead / behind、
1946
+ * 日志上的装饰和在真实仓库里跑一样,读不到仓库自己的配置(`core.fsmonitor`、过滤器、`diff.external`、钩子)、也不会连网。
1947
+ * 一组读共用一个视图:搭一个要先跑几条命令,每条命令各搭一个就是每次刷新几十个进程。
1948
+ *
1949
+ * `root` 不在检出里时 `work` 不跑,交回 null。只读:暂存、提交、推送、切分支这些人点了才做的事仍由插件自己在真实仓库上跑,
1950
+ * 那样钩子、凭据助手正是人要的。除了要写 stdin 的那一个(`gitWithInput`,`cat-file --batch`),
1951
+ * 要按二进制读的用 `gitBuffer`:`git show HEAD:logo.png` 回的不是文字。
1952
+ */
1953
+ git<T>(root: string, work: (view: PluginGitView) => Promise<T>): Promise<T | null>;
1954
+ }
1955
+ /**
1956
+ * `ctx.workspace.git` 给的视图。路径、pathspec 相对 `cwd`,和在该目录直接跑 git 一样。
1957
+ * 读的结果没有替调用方决定是不是文字,失败时抛错(和 git 自己一样:非零退出就算失败,stderr 上那些不是错误的话不管)。
1958
+ */
1959
+ export interface PluginGitView {
1960
+ /** 调用方给的目录:命令在这里跑。 */
1961
+ readonly cwd: string;
1962
+ git(args: string[]): Promise<string>;
1963
+ gitBuffer(args: string[]): Promise<Uint8Array>;
1964
+ gitWithInput(args: string[], input: string): Promise<Uint8Array>;
1965
+ /**
1966
+ * 没人点的那种获取(面板露面时、之后每五分钟):在视图里 `fetch --prune`,再把变了的引用搬回真实仓库。
1967
+ * 别人的改动被拒时整批不搬(每条引用带着抄快照时的旧值)。浅克隆的边界变了就不搬,留给人点的那次获取。
1968
+ *
1969
+ * `cancelled` 是调用方叫停的,不算失败;`timedOut` 是到了时限被杀;其余失败带 git 自己的话(`stderr`),原话交给调用方去说。
1382
1970
  */
1383
- list(root: string): Promise<string[] | null>;
1971
+ fetch(options: {
1972
+ signal?: AbortSignal;
1973
+ timeoutMs: number;
1974
+ }): Promise<PluginGitFetch>;
1384
1975
  }
1385
- /** 沙箱的模式:只读、能写项目目录、不设限。 */
1976
+ /** 见 `PluginGitView.fetch`。 */
1977
+ export type PluginGitFetch = {
1978
+ ok: true;
1979
+ } | {
1980
+ ok: false;
1981
+ cancelled?: boolean;
1982
+ timedOut?: boolean;
1983
+ stderr?: string;
1984
+ };
1985
+ /**
1986
+ * 沙箱的模式:只读、能写项目目录、不设限。权限模式对应哪一档由本体定:请求批准只读,帮我批准能写项目目录和临时目录,
1987
+ * 完全访问不设限;`ToolExecution.readOnly` 一律只读。macOS 用 seatbelt,Windows 用受限令牌,Linux 没有后端。
1988
+ */
1386
1989
  export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access";
1990
+ /** 命令跑在哪个 shell、哪一档沙箱,见 `ToolRun.shell()`、`ModelContext.shell`。 */
1991
+ export interface ShellInfo {
1992
+ /** 命令行按什么语法写。Git Bash 是 `posix`。 */
1993
+ kind: "posix" | "powershell";
1994
+ /** 告诉模型、告诉人这是哪个 shell 时用的名字(「zsh」「Git Bash」「Windows PowerShell 5.1」)。 */
1995
+ label: string;
1996
+ /** 会话宣布的那一档。Windows 上受约束的命令跑在 PowerShell 里,完全访问跑在 Git Bash 里,别的平台每档都是同一个 shell。 */
1997
+ sandbox: SandboxMode;
1998
+ }
1999
+ /** `ToolRun.exec` 起的一条命令。 */
2000
+ export interface RunProcess extends SandboxProcess {
2001
+ /** 实际跑在哪一档(提权批过的就是更宽的那一档)。 */
2002
+ readonly mode: SandboxMode;
2003
+ /**
2004
+ * 这段输出像不像被沙箱拒了(只读的文件系统、没有权限):像的话交回给模型的那句说明(说出实际跑的那一档、说清这是策略拒绝而不是命令出错),
2005
+ * 原样放进结果,模型可以申请提权重试;不像交回 undefined。`danger-full-access` 下总是 undefined。
2006
+ */
2007
+ denied(output: string): string | undefined;
2008
+ }
1387
2009
  /** 一条已经启动的命令。 */
1388
2010
  export interface SandboxProcess {
1389
2011
  readonly pid?: number;
@@ -1399,25 +2021,6 @@ export interface SandboxProcess {
1399
2021
  /** 连它起的子进程一起停。 */
1400
2022
  kill(signal?: "SIGTERM" | "SIGKILL"): void;
1401
2023
  }
1402
- /** 服务 `sandbox`:在沙箱里跑 shell 命令。内置插件 plume.sandbox 提供(macOS 用 seatbelt,Windows 用受限令牌;Linux 没有后端)。 */
1403
- export interface SandboxService {
1404
- /**
1405
- * 用用户的 shell 起一条命令,环境向登录 shell 要(从图标启动时也找得到用户装的工具)。`mode` 不给就不进沙箱;
1406
- * 给了而这台机器给不了那样的沙箱时抛错。
1407
- *
1408
- * `shellMode` 是按哪一档选 shell:会话宣布的模式,系统提示词里告诉模型的 shell 就是按它选的;不给时按 `mode`。
1409
- * 只在 Windows 上有区别(受约束的跑 PowerShell,完全访问跑 Git Bash):提权的命令传更宽的 `mode` 和原来的 `shellMode`,
1410
- * 它仍是照被告知的那个 shell 写的。
1411
- */
1412
- run(command: string, options: {
1413
- cwd: string;
1414
- mode?: SandboxMode;
1415
- shellMode?: SandboxMode;
1416
- env?: Record<string, string>;
1417
- }): SandboxProcess;
1418
- /** 这段输出像不像被沙箱拒了(只读的文件系统、没有权限):像的话模型可以申请提权重试。 */
1419
- looksDenied(output: string): boolean;
1420
- }
1421
2024
  /** 逻辑入口 `dist/index.mjs` 的默认导出。 */
1422
2025
  export interface Plugin {
1423
2026
  name: string;
@@ -1426,5 +2029,11 @@ export interface Plugin {
1426
2029
  * 提供方换了或停了,插件先停用,再按现在的提供方重新加载(缺了就回到等待)。
1427
2030
  */
1428
2031
  inject?: readonly (keyof Services)[];
2032
+ /**
2033
+ * `"all"`:`ctx.sessions.observe` 收全部会话的观察(每个会话的模型请求和回复、弹着的卡、运行状态),不只和本插件有关的那些
2034
+ * (见 `SessionAccess.observe`)。诊断轨迹、一轮收场后跑用户的钩子、给每个会话写摘要这类插件要它;插件市场的详情页对装着的插件标出这一条。
2035
+ * 不写就是只看和自己有关的会话。内置插件也一样要写。
2036
+ */
2037
+ observe?: "all";
1429
2038
  apply(ctx: PluginContext): void | Promise<void>;
1430
2039
  }