@springbrand/agent-runtime 0.1.3-alpha.0 → 0.1.3-alpha.2

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/src/plugins.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import type { SkillSource } from "agents/skills";
2
2
  import type {
3
+ RuntimeCodeExecutionPort,
3
4
  RuntimeMemoryPort,
4
5
  RuntimePlatformPort,
5
6
  RuntimeProviderPort,
6
7
  RuntimeSandboxPort,
7
8
  RuntimeSchedulePort,
9
+ RuntimeSkillSourceBinding,
8
10
  RuntimeSubagentPort,
9
11
  RuntimeTurnEventsPort,
10
12
  RuntimeBindings,
@@ -27,15 +29,30 @@ import {
27
29
  } from "./lib/execution-level";
28
30
  import {
29
31
  createPiRuntimeAssembly,
32
+ type PiToolSurface,
30
33
  type PiRuntimeAssembly,
31
34
  } from "./pi/assembly/snapshot";
32
35
  import type { PiToolCandidate } from "./pi/tool/compiler";
33
- import { basePiToolCandidates } from "./pi/tool/base";
36
+ import {
37
+ basePiToolCandidates,
38
+ memoryPiToolCandidate,
39
+ } from "./pi/tool/base";
40
+ import {
41
+ browserQuickActionPiToolCandidates,
42
+ codeExecutionPiToolCandidate,
43
+ } from "./pi/tool/core";
44
+ import {
45
+ sandboxPiToolCandidates,
46
+ workspacePiToolCandidates,
47
+ } from "./pi/tool/workspace-sandbox";
48
+ import { schedulePiToolCandidates } from "./pi/tool/schedule";
49
+ import { skillPiToolCandidates } from "./pi/tool/skill";
50
+ import { subagentPiToolCandidates } from "./pi/tool/subagent";
34
51
  import { createWebSearch } from "./pi/tool/web-search";
35
52
  import { resolvePiModel } from "./pi/runtime-adapter/models";
36
53
 
37
54
  /**
38
- * 本文件实现 Plugin 的准备、贡献、校验和原子候选装配。
55
+ * 本文件实现 Plugin 的准备、声明合并、校验和原子候选装配。
39
56
  *
40
57
  * @remarks
41
58
  * 核心术语见包入口 `index.ts`,这里不重复定义。
@@ -70,332 +87,152 @@ export type PluginKind =
70
87
  export type { RuntimeDegradation } from "./kernel/degradation";
71
88
 
72
89
  /**
73
- * loader 读取到的值和本次降级信息一起返回。
90
+ * 提供 Runtime 核心运行参数。
74
91
  *
75
92
  * @remarks
76
- * `definePlugin` 在准备阶段读取它。
93
+ * `profile` Plugin 在准备结果中返回它。
77
94
  *
78
- * 值与诊断一起返回,可以避免 loader 直接修改 Runtime。
95
+ * 这里只放业务可配置字段,冻结和最终结构由 Runtime 负责。
79
96
  */
80
- export interface PluginLoadResult<T> {
81
- readonly value: T;
82
- readonly degradations: readonly RuntimeDegradation[];
97
+ export interface RuntimeProfileContribution {
98
+ readonly model: string;
99
+ readonly thinking: ThinkingEffort;
100
+ readonly systemPrompt?: string;
101
+ readonly executionLevel: ExecutionLevel;
83
102
  }
84
103
 
85
104
  /**
86
- * 表示一个尚未准备的 Runtime Plugin
87
- *
88
- * @remarks
89
- * 应用把它放进 `AgentConfig.plugins`。
90
- *
91
- * Runtime 只调用 `prepare`,不会接触 Plugin 捕获的业务依赖。
105
+ * 描述一个已准备 Skill
92
106
  */
93
- export interface AgentPlugin {
94
- readonly kind: PluginKind;
95
- /**
96
- * 读取这个 Plugin 的配置,并生成一个还未贡献的准备结果。
97
- *
98
- * @remarks
99
- * Runtime 在生成候选结果时调用,应用只需把 Plugin 放进 `AgentConfig`。
100
- *
101
- * 读取与贡献分开,是为了并行读取外部数据后再按稳定顺序合并。
102
- */
103
- prepare(): Promise<PreparedPlugin>;
107
+ export interface RuntimeSkillContribution {
108
+ readonly name: string;
109
+ readonly description: string;
110
+ readonly source: SkillSource;
111
+ readonly script?: Partial<RuntimeSkillScriptPolicy>;
104
112
  }
105
113
 
106
114
  /**
107
- * 表示 loader 已完成、但还没有写入候选结果的 Plugin
108
- *
109
- * @remarks
110
- * Runtime 按固定 kind 顺序调用 `contribute`。
111
- *
112
- * 准备和贡献分开,才能并行读取数据又保持确定的合并顺序。
115
+ * 描述一个已准备 Extension
113
116
  */
114
- export interface PreparedPlugin {
115
- readonly kind: PluginKind;
116
- readonly degradations: readonly RuntimeDegradation[];
117
- /**
118
- * 把已读取的值写进 Runtime 提供的受限候选上下文。
119
- *
120
- * @remarks
121
- * Runtime 在全部 loader 完成后,按 `PluginKind` 固定顺序调用。
122
- *
123
- * 这一阶段只能使用受限上下文,不能直接安装 Snapshot。
124
- */
125
- contribute(
126
- ctx: RuntimeContributionContext,
127
- ): void | Promise<void>;
117
+ export interface RuntimeExtensionContribution {
118
+ readonly name: string;
119
+ readonly extension: RuntimeExtensionConfig;
128
120
  }
129
121
 
130
122
  /**
131
- * 描述一个 Plugin 应该怎样读取数据并贡献能力。
132
- *
133
- * @remarks
134
- * 应用把该对象传给 `definePlugin`。
123
+ * 描述 Agent 对最终 Tool Surface 的可见性约束。
135
124
  *
136
- * `loader` 负责读取,`contribute` 负责组装,二者不要互相越权。
125
+ * Host 只提供策略;Runtime 在唯一 Tool Surface seam 中对所有
126
+ * Port 和 Resource 生成的候选项统一应用。
137
127
  */
138
- export interface AgentPluginSpec<T> {
139
- readonly kind: PluginKind;
140
- /**
141
- * 在准备阶段读取本 Plugin 所需的数据。
142
- *
143
- * @remarks
144
- * `definePlugin` 包装出的 `prepare` 会在 Runtime 装配开始时调用它。
145
- *
146
- * 它只返回数据和降级信息,不应直接修改正在运行的 Runtime。
147
- */
148
- loader(): Promise<PluginLoadResult<T>>;
149
- /**
150
- * 在贡献阶段把已读取的数据交给 Runtime。
151
- *
152
- * @remarks
153
- * Runtime 准备完所有 Plugin 后,通过 `PreparedPlugin.contribute` 调用它。
154
- *
155
- * 它必须通过 `RuntimeContributionContext` 写入候选结果,以便统一检查重复与跨能力约束。
156
- */
157
- contribute(
158
- loaded: T,
159
- ctx: RuntimeContributionContext,
160
- ): void | Promise<void>;
128
+ export interface RuntimeToolSurfacePolicy {
129
+ readonly denyPolicy?: RuntimeDenyPolicy;
130
+ readonly allowsTool?: (name: string) => boolean;
131
+ readonly allowsExtension?: (extension: RuntimeExtensionConfig) => boolean;
161
132
  }
162
133
 
163
- /**
164
- * 汇总一次 Runtime 装配要使用的全部 Plugin。
165
- *
166
- * @remarks
167
- * `RuntimeAgentDefinition.createConfig` 在首次加载或显式重载时返回它。
168
- *
169
- * 它只保存可信函数对象,不用于持久化或跨进程序列化。
170
- */
171
- export interface AgentConfig {
172
- readonly plugins: readonly AgentPlugin[];
134
+ interface PluginResultByKind {
135
+ readonly scope: {
136
+ readonly commitGuards: readonly (() => Promise<void>)[];
137
+ };
138
+ readonly profile: { readonly profile: RuntimeProfileContribution };
139
+ readonly provider: { readonly provider: RuntimeProviderPort };
140
+ readonly platform: {
141
+ readonly platform: RuntimePlatformPort;
142
+ };
143
+ readonly workspace: {
144
+ readonly workspace?: WorkspacePort;
145
+ readonly codeExecution?: RuntimeCodeExecutionPort;
146
+ };
147
+ readonly sandbox: {
148
+ readonly sandbox?: RuntimeSandboxPort;
149
+ };
150
+ readonly memory: {
151
+ readonly memoryProfile: RuntimeMemoryProfile;
152
+ readonly memoryPort?: RuntimeMemoryPort;
153
+ };
154
+ readonly tool: {
155
+ readonly hostTools: readonly PiToolCandidate[];
156
+ readonly policy?: RuntimeToolSurfacePolicy;
157
+ };
158
+ readonly skill: {
159
+ readonly skills: readonly RuntimeSkillContribution[];
160
+ };
161
+ readonly connector: {
162
+ readonly connectors: readonly RuntimeMcpServer[];
163
+ };
164
+ readonly extension: {
165
+ readonly extensions: readonly RuntimeExtensionContribution[];
166
+ };
167
+ readonly schedule: {
168
+ readonly schedule: RuntimeSchedulePort;
169
+ };
170
+ readonly subagent: {
171
+ readonly enabledSubagents: readonly string[];
172
+ readonly subagents?: RuntimeSubagentPort;
173
+ };
174
+ readonly "turn-events": {
175
+ readonly turnEvents: RuntimeTurnEventsPort;
176
+ };
173
177
  }
174
178
 
175
- /**
176
- * 提供 Runtime 核心运行参数。
177
- *
178
- * @remarks
179
- * `profile` Plugin 在贡献阶段调用 `configureProfile` 时传入。
180
- *
181
- * 这里只放业务可配置字段,冻结和最终结构由 Runtime 负责。
182
- */
183
- export interface RuntimeProfileContribution {
184
- readonly model: string;
185
- readonly thinking: ThinkingEffort;
186
- readonly systemPrompt?: string;
187
- readonly executionLevel: ExecutionLevel;
179
+ export type PluginPreparation<K extends PluginKind> = Readonly<
180
+ PluginResultByKind[K] & {
181
+ readonly degradations: readonly RuntimeDegradation[];
182
+ }
183
+ >;
184
+
185
+ export type PreparedPlugin<K extends PluginKind = PluginKind> =
186
+ K extends PluginKind
187
+ ? Readonly<PluginPreparation<K> & { readonly kind: K }>
188
+ : never;
189
+
190
+ interface AgentPluginContract<K extends PluginKind> {
191
+ readonly kind: K;
192
+ prepare(): Promise<PreparedPlugin<K>>;
193
+ }
194
+
195
+ export type AgentPlugin<K extends PluginKind = PluginKind> =
196
+ K extends PluginKind ? AgentPluginContract<K> : never;
197
+
198
+ export interface AgentPluginSpec<K extends PluginKind> {
199
+ readonly kind: K;
200
+ prepare(): Promise<PluginPreparation<K>>;
188
201
  }
189
202
 
190
203
  /**
191
- * 限定 Plugin 可以写入候选 Runtime 的内容。
192
- *
193
- * @remarks
194
- * Plugin 的 `contribute` 回调使用这些方法。
195
- *
196
- * Runtime 只暴露绑定和增量添加操作,避免 Plugin 取得 Builder 的可变引用。
204
+ * 汇总一次 Runtime 装配要使用的全部 Plugin。
197
205
  */
198
- export interface RuntimeContributionContext {
199
- /**
200
- * 写入唯一的模型、提示词、审批和权限配置。
201
- *
202
- * @remarks
203
- * profile Plugin 在贡献阶段调用一次。
204
- *
205
- * Runtime 拒绝第二份配置,避免候选结果同时有两个运行参数来源。
206
- */
207
- configureProfile(profile: RuntimeProfileContribution): void;
208
- /**
209
- * 写入部署提供的模型端点与凭据。
210
- *
211
- * @remarks
212
- * provider Plugin 在贡献阶段调用一次。
213
- *
214
- * Runtime 保留单一 Provider 选择,并在生成候选结果时检查默认模型、端点和凭据。
215
- */
216
- bindProvider(provider: RuntimeProviderPort): void;
217
- /**
218
- * 写入唯一的 Cloudflare 平台端口。
219
- *
220
- * @remarks
221
- * platform Plugin 在贡献阶段调用一次。
222
- *
223
- * Loader 和出口函数作为一个整体绑定,不允许从多个平台配置静默拼接。
224
- */
225
- bindPlatform(platform: RuntimePlatformPort): void;
226
- /**
227
- * 写入可选的工作区端口。
228
- *
229
- * @remarks
230
- * workspace Plugin 在工作区可用时调用一次。
231
- *
232
- * Runtime 只允许一个当前工作区;未绑定时依赖它的能力必须降级或在校验时失败。
233
- */
234
- bindWorkspace(workspace: WorkspacePort): void;
235
- /**
236
- * 写入可选的 Linux Sandbox 执行端口。
237
- *
238
- * @remarks
239
- * sandbox Plugin 在部署和 Agent 配置都启用该能力时调用。
240
- *
241
- * Sandbox 必须和持久工作区成对出现,这个跨能力约束在提交前统一校验。
242
- */
243
- bindSandbox(
244
- sandbox: RuntimeSandboxPort,
245
- ): void;
246
- /**
247
- * 写入内存参数,并在启用时提供内存读写端口。
248
- *
249
- * @remarks
250
- * memory Plugin 在贡献阶段调用一次,即使功能关闭也要传入 profile。
251
- *
252
- * 独立记录“已配置”状态,才能区分未贡献与明确关闭。
253
- */
254
- configureMemory(
255
- profile: RuntimeMemoryProfile,
256
- port?: RuntimeMemoryPort,
257
- ): void;
258
- /**
259
- * 添加一个已经过装配期授权、并保留审批元数据的 Pi Tool 候选。
260
- *
261
- * @remarks
262
- * tool Plugin 为当前 Agent 真正可见的 Tool 逐个调用。
263
- *
264
- * Runtime 在装配期拒绝空名和重名,避免 Pi 开始 Turn 后才发现能力冲突。
265
- */
266
- addPiTool(candidate: PiToolCandidate): void;
267
- /**
268
- * 写入唯一的 Turn 期拒绝清单。
269
- *
270
- * @remarks
271
- * tool Plugin 在完成实际可见能力挑选后调用一次。
272
- *
273
- * 能力的有无在注入前已经决定,这里只保留 Turn 期仍需执行的拒绝规则。
274
- */
275
- configureDenyPolicy(
276
- policy?: RuntimeDenyPolicy,
277
- ): void;
278
- /**
279
- * 添加一个已授权 Skill 的持久化 catalog、内容来源及脚本权限。
280
- *
281
- * @remarks
282
- * skill Plugin 为已解析的外部 Skill 逐个调用。
283
- *
284
- * catalog、来源与权限一起登记,同时和其他 Skill 来源共享同一名称空间。
285
- */
286
- addSkillSource(
287
- name: string,
288
- description: string,
289
- source: SkillSource,
290
- script?: Partial<RuntimeSkillScriptPolicy>,
291
- ): void;
292
- /**
293
- * 按名称添加一个 MCP Connector。
294
- *
295
- * @remarks
296
- * connector Plugin 为当前 Agent 已授权的连接逐个调用。
297
- *
298
- * Runtime 在装配期统一规范化名称和 URL,并拒绝空值或重名。
299
- */
300
- addConnector(server: RuntimeMcpServer): void;
301
- /**
302
- * 按名称添加一个已验证的 Runtime Extension。
303
- *
304
- * @remarks
305
- * extension Plugin 为当前部署的扩展逐个调用。
306
- *
307
- * 登记名必须和 manifest 名一致,否则配置选择可能指向错误的执行对象。
308
- */
309
- addExtension(name: string, extension: RuntimeExtensionConfig): void;
310
- /**
311
- * 写入唯一的定时任务端口。
312
- *
313
- * @remarks
314
- * schedule Plugin 在贡献阶段调用一次。
315
- *
316
- * 本端口只描述回到当前 Session 的调用边界;Cloudflare 的实际唤醒和回调路由 Host 与 Agents SDK 管理。
317
- */
318
- bindSchedule(schedule: RuntimeSchedulePort): void;
319
- /**
320
- * 标记一个 Subagent 类型在本次 Runtime 中启用。
321
- *
322
- * @remarks
323
- * subagent Plugin 通过部署检查后为每种可用类型调用。
324
- *
325
- * Runtime 只把已启用名称交给 Pi,并在提交前确认执行端口存在。
326
- */
327
- enableSubagent(name: string): void;
328
- /**
329
- * 写入唯一的 Subagent 执行端口。
330
- *
331
- * @remarks
332
- * subagent Plugin 在至少启用一种类型时调用一次。
333
- *
334
- * 类型选择和实际执行能力分开登记,缺少端口时由统一校验拒绝候选结果。
335
- */
336
- bindSubagents(subagents: RuntimeSubagentPort): void;
337
- /**
338
- * 写入唯一的 Turn 完成事件端口。
339
- *
340
- * @remarks
341
- * turn-events Plugin 在贡献阶段调用一次。
342
- *
343
- * 单一端口保证应用投影只有一个顺序来源,避免同一完成事件被重复写入。
344
- */
345
- bindTurnEvents(turnEvents: RuntimeTurnEventsPort): void;
346
- /**
347
- * 添加一个最终提交前必须通过的异步检查。
348
- *
349
- * @remarks
350
- * 会在 loader 执行期间过期的 scope Plugin 在贡献时添加它。
351
- *
352
- * Runtime 延迟到全部候选内容就绪后再运行 guard,防止旧业务条件被提交。
353
- */
354
- addCommitGuard(guard: () => Promise<void>): void;
355
- /**
356
- * 记录一项不阻止启动的能力降级。
357
- *
358
- * @remarks
359
- * Runtime 在贡献每个 PreparedPlugin 前,把它的降级信息逐项写入。
360
- *
361
- * 记录时会复制并冻结顶层对象,避免已提交诊断被 Plugin 后续改写。
362
- */
363
- reportDegradation(degradation: RuntimeDegradation): void;
206
+ export interface AgentConfig {
207
+ readonly plugins: readonly AgentPlugin[];
364
208
  }
365
209
 
366
210
  /**
367
- * loader 与贡献回调包装成 Runtime 可准备的 Plugin。
368
- *
369
- * @remarks
370
- * 应用在创建 `AgentConfig` 时调用它,不需要手写 `prepare`。
211
+ * 定义一个只准备声明式结果的 Runtime Plugin。
371
212
  *
372
- * 包装层会冻结准备结果,避免读取完成后被调用方改写。
213
+ * Runtime 内部负责排序、合并、冲突检查和提交。
373
214
  */
374
- export function definePlugin<T>(
375
- spec: AgentPluginSpec<T>,
376
- ): AgentPlugin {
215
+ export function definePlugin<K extends PluginKind>(
216
+ spec: AgentPluginSpec<K>,
217
+ ): AgentPlugin<K> {
377
218
  return Object.freeze({
378
219
  kind: spec.kind,
379
220
 
380
- // 作用:读取本 Plugin 的数据,并把结果变成不可变的准备对象。
381
- // 调用:Runtime 在候选装配开始时调用,应用不应直接调用。
382
- // 原因:此处只准备数据,不贡献能力,才能安全地与其他 loader 并行。
383
- async prepare(): Promise<PreparedPlugin> {
384
- const loaded = await spec.loader();
221
+ async prepare(): Promise<PreparedPlugin<K>> {
222
+ const prepared = await spec.prepare();
385
223
  const degradations = Object.freeze(
386
- loaded.degradations.map((degradation) =>
224
+ prepared.degradations.map((degradation) =>
387
225
  Object.freeze({ ...degradation }),
388
226
  ),
389
227
  );
390
228
 
391
229
  return Object.freeze({
230
+ ...prepared,
392
231
  kind: spec.kind,
393
232
  degradations,
394
- contribute: (ctx: RuntimeContributionContext) =>
395
- spec.contribute(loaded.value, ctx),
396
- });
233
+ }) as PreparedPlugin<K>;
397
234
  },
398
- });
235
+ }) as AgentPlugin<K>;
399
236
  }
400
237
 
401
238
  // #endregion
@@ -451,7 +288,6 @@ const DISABLED_MEMORY: RuntimeMemoryProfile = Object.freeze({
451
288
  enabled: false,
452
289
  memoryTokens: 2_000,
453
290
  preferencesTokens: 500,
454
- compactAfterTokens: 100_000,
455
291
  });
456
292
 
457
293
  // 作用:把外部名称整理成非空字符串。
@@ -479,22 +315,140 @@ function assertMemoryProfile(profile: RuntimeMemoryProfile): void {
479
315
  }
480
316
  }
481
317
 
318
+ interface ToolSurfaceInput {
319
+ readonly platform: RuntimePlatformPort;
320
+ readonly workspace?: WorkspacePort;
321
+ readonly memory?: {
322
+ readonly port: RuntimeMemoryPort;
323
+ readonly profile: RuntimeMemoryProfile;
324
+ };
325
+ readonly codeExecution?: RuntimeCodeExecutionPort;
326
+ readonly sandbox?: RuntimeSandboxPort;
327
+ readonly hostTools: readonly PiToolCandidate[];
328
+ readonly skills: readonly RuntimeSkillSourceBinding[];
329
+ readonly schedule?: RuntimeSchedulePort;
330
+ readonly subagents?: RuntimeSubagentPort;
331
+ readonly enabledSubagents: readonly string[];
332
+ readonly webSearch: Parameters<typeof basePiToolCandidates>[0];
333
+ readonly extensions: readonly RuntimeExtensionConfig[];
334
+ readonly policy?: RuntimeToolSurfacePolicy;
335
+ }
336
+
337
+ interface ToolSurface {
338
+ readonly surface: PiToolSurface;
339
+ readonly extensions: readonly RuntimeExtensionConfig[];
340
+ readonly degradations: readonly RuntimeDegradation[];
341
+ }
342
+
343
+ // 唯一 Tool Surface seam:只读取已授权 Port、Resource 和 Agent policy,
344
+ // 统一生成可见 Tool,并在一处做名称、执行档位与冲突校验。
345
+ async function createToolSurface(
346
+ input: ToolSurfaceInput,
347
+ ): Promise<ToolSurface> {
348
+ const deny = new Set(input.policy?.denyPolicy?.deny ?? []);
349
+ const allowsTool = input.policy?.allowsTool;
350
+ const allowsExtension = input.policy?.allowsExtension;
351
+ const visible = (candidate: PiToolCandidate) =>
352
+ candidate.authorized &&
353
+ !deny.has(candidate.tool.name) &&
354
+ allowsTool?.(candidate.tool.name) !== false;
355
+ const browserCandidates = input.platform.browser
356
+ ? browserQuickActionPiToolCandidates(input.platform.browser)
357
+ : [];
358
+ const workspaceCandidates = input.workspace
359
+ ? workspacePiToolCandidates(input.workspace)
360
+ : [];
361
+ const executionCandidates = input.workspace && input.codeExecution
362
+ ? [codeExecutionPiToolCandidate(input.codeExecution)]
363
+ : [];
364
+ const sandboxCandidates = input.sandbox
365
+ ? sandboxPiToolCandidates(input.sandbox)
366
+ : [];
367
+ const subagentCandidates = subagentPiToolCandidates(
368
+ input.subagents,
369
+ input.enabledSubagents,
370
+ );
371
+ const scheduleCandidates = input.schedule
372
+ ? schedulePiToolCandidates(input.schedule)
373
+ : [];
374
+ const baseCandidates = basePiToolCandidates(input.webSearch);
375
+ const memoryCandidates = input.memory
376
+ ? [memoryPiToolCandidate(input.memory.port, input.memory.profile)]
377
+ : [];
378
+ const scriptCandidates = [
379
+ ...browserCandidates,
380
+ ...workspaceCandidates,
381
+ ...executionCandidates,
382
+ ...sandboxCandidates,
383
+ ...input.hostTools,
384
+ ...scheduleCandidates,
385
+ ...subagentCandidates,
386
+ ...baseCandidates,
387
+ ...memoryCandidates,
388
+ ].filter(visible);
389
+ const staticCandidates = [
390
+ ...browserCandidates,
391
+ ...workspaceCandidates,
392
+ ...executionCandidates,
393
+ ...sandboxCandidates,
394
+ ...input.hostTools,
395
+ ...(await skillPiToolCandidates(input.skills, {
396
+ loader: input.platform.loader,
397
+ ...(input.workspace ? { workspace: input.workspace } : {}),
398
+ tools: scriptCandidates,
399
+ })),
400
+ ...scheduleCandidates,
401
+ ...subagentCandidates,
402
+ ...baseCandidates,
403
+ ...memoryCandidates,
404
+ ];
405
+
406
+ return {
407
+ surface: Object.freeze({
408
+ finalize(authorizedCandidates: readonly PiToolCandidate[]) {
409
+ const tools = new Map<string, PiToolCandidate>();
410
+
411
+ for (const candidate of [
412
+ ...staticCandidates,
413
+ ...authorizedCandidates,
414
+ ]) {
415
+ const name = requiredName(candidate.tool.name, "Pi Tool");
416
+ if (!visible(candidate)) continue;
417
+ if (!EXECUTION_LEVELS.includes(candidate.requiredExecutionLevel)) {
418
+ throw new Error(`Pi Tool ${name} execution level is invalid`);
419
+ }
420
+ if (tools.has(name)) {
421
+ throw new Error(`Duplicate Runtime Pi Tool: ${name}`);
422
+ }
423
+ tools.set(name, Object.freeze({ ...candidate }));
424
+ }
425
+
426
+ return Object.freeze([...tools.values()]);
427
+ },
428
+ }),
429
+ extensions: input.extensions.filter(
430
+ (extension) => allowsExtension?.(extension) !== false,
431
+ ),
432
+ degradations: [],
433
+ };
434
+ }
435
+
482
436
  // #endregion
483
437
 
484
- // #region RuntimeBuilder 贡献接口
438
+ // #region RuntimeBuilder 合并
485
439
 
486
- class RuntimeBuilder implements RuntimeContributionContext {
440
+ class RuntimeBuilder {
487
441
  private profile?: RuntimeProfileContribution;
488
442
  private provider?: RuntimeProviderPort;
489
443
  private platform?: RuntimePlatformPort;
490
444
  private workspace?: WorkspacePort;
445
+ private codeExecution?: RuntimeCodeExecutionPort;
491
446
  private sandbox?: RuntimeSandboxPort;
447
+ private schedule?: RuntimeSchedulePort;
492
448
  private memoryProfile: RuntimeMemoryProfile = DISABLED_MEMORY;
493
449
  private memory?: RuntimeMemoryPort;
494
- private memoryConfigured = false;
495
- private denyPolicy?: RuntimeDenyPolicy;
496
- private denyPolicyConfigured = false;
497
- private readonly piTools = new Map<string, PiToolCandidate>();
450
+ private readonly hostTools: PiToolCandidate[] = [];
451
+ private toolPolicy?: RuntimeToolSurfacePolicy;
498
452
  private readonly skillSources = new Map<
499
453
  string,
500
454
  {
@@ -504,156 +458,88 @@ class RuntimeBuilder implements RuntimeContributionContext {
504
458
  script: RuntimeSkillScriptPolicy;
505
459
  }
506
460
  >();
507
- private readonly skillDefinitions = new Set<string>();
508
461
  private readonly connectors = new Map<string, RuntimeMcpServer>();
509
462
  private readonly extensions = new Map<string, RuntimeExtensionConfig>();
510
- private schedule?: RuntimeSchedulePort;
511
463
  private readonly enabledSubagents = new Set<string>();
512
464
  private subagents?: RuntimeSubagentPort;
513
465
  private turnEvents?: RuntimeTurnEventsPort;
514
466
  private readonly commitGuards: Array<() => Promise<void>> = [];
515
467
  private readonly degradations: RuntimeDegradation[] = [];
516
468
 
517
- // 作用:给 Plugin 一组受限的候选写入方法。
518
- // 调用:候选装配开始后创建一次,再交给每个 contribute 回调。
519
- // 原因:返回冻结门面,防止 Plugin 保留或修改 Builder 自身状态。
520
- contributionContext(): RuntimeContributionContext {
521
- return Object.freeze({
522
- configureProfile: this.configureProfile.bind(this),
523
- bindProvider: this.bindProvider.bind(this),
524
- bindPlatform: this.bindPlatform.bind(this),
525
- bindWorkspace: this.bindWorkspace.bind(this),
526
- bindSandbox: this.bindSandbox.bind(this),
527
- configureMemory: this.configureMemory.bind(this),
528
- addPiTool: this.addPiTool.bind(this),
529
- configureDenyPolicy:
530
- this.configureDenyPolicy.bind(this),
531
- addSkillSource: this.addSkillSource.bind(this),
532
- addConnector: this.addConnector.bind(this),
533
- addExtension: this.addExtension.bind(this),
534
- bindSchedule: this.bindSchedule.bind(this),
535
- enableSubagent: this.enableSubagent.bind(this),
536
- bindSubagents: this.bindSubagents.bind(this),
537
- bindTurnEvents: this.bindTurnEvents.bind(this),
538
- addCommitGuard: this.addCommitGuard.bind(this),
539
- reportDegradation:
540
- this.reportDegradation.bind(this),
541
- });
542
- }
543
-
544
- // 作用:记录本次装配唯一的运行参数。
545
- // 调用:profile Plugin 在贡献阶段调用一次。
546
- // 原因:重复配置通常意味着装配歧义,因此立即拒绝。
547
- configureProfile(profile: RuntimeProfileContribution): void {
548
- if (this.profile) {
549
- throw new Error("Runtime profile was contributed more than once");
550
- }
551
- if (!EXECUTION_LEVELS.includes(profile.executionLevel)) {
552
- throw new Error("Runtime execution level is invalid");
553
- }
554
- this.profile = profile;
555
- }
556
-
557
- // 作用:记录本次装配唯一的模型 Provider。
558
- // 调用:provider Plugin 在贡献阶段调用一次。
559
- // 原因:默认模型和端点必须从同一份配置校验,不能静默合并。
560
- bindProvider(provider: RuntimeProviderPort): void {
561
- if (this.provider) {
562
- throw new Error("Runtime Provider was contributed more than once");
563
- }
564
- this.provider = provider;
565
- }
566
-
567
- // 作用:记录本次装配唯一的平台能力。
568
- // 调用:platform Plugin 在贡献阶段调用一次。
569
- // 原因:Loader 与出口端口必须来自同一个平台配置,不能静默拼接。
570
- bindPlatform(platform: RuntimePlatformPort): void {
571
- if (this.platform) {
572
- throw new Error("Runtime platform was contributed more than once");
573
- }
574
- this.platform = platform;
575
- }
576
-
577
- // 作用:记录本次装配可选的工作区。
578
- // 调用:workspace Plugin 在成功加载工作区后调用。
579
- // 原因:工作区只有一个当前目录语义,重复绑定会产生不确定行为。
580
- bindWorkspace(workspace: WorkspacePort): void {
581
- if (this.workspace) {
582
- throw new Error("Runtime workspace was contributed more than once");
583
- }
584
- this.workspace = workspace;
585
- }
586
-
587
- // 作用:记录本次候选唯一的 Linux Sandbox 执行端口。
588
- // 调用:sandbox Plugin 在部署与 Agent 双开关都通过后调用。
589
- // 原因:一个 Chat 只能指向一个由 Host 选定的 Sandbox,重复绑定必须失败。
590
- bindSandbox(
591
- sandbox: RuntimeSandboxPort,
592
- ): void {
593
- if (this.sandbox) {
594
- throw new Error("Runtime Sandbox was contributed more than once");
595
- }
596
- this.sandbox = sandbox;
597
- }
598
-
599
- // 作用:记录内存预算和可选读写端口。
600
- // 调用:memory Plugin 在贡献阶段调用一次。
601
- // 原因:即使内存关闭也要记录“已经配置”,以便拒绝第二份冲突配置。
602
- configureMemory(
603
- profile: RuntimeMemoryProfile,
604
- port?: RuntimeMemoryPort,
605
- ): void {
606
- if (this.memoryConfigured) {
607
- throw new Error("Runtime memory was contributed more than once");
608
- }
609
- this.memoryConfigured = true;
610
- this.memoryProfile = profile;
611
- this.memory = port;
612
- }
613
-
614
- // 作用:把一个已授权的 Pi Tool 加入候选 Agent 输入。
615
- // 调用:tool Plugin 在装配期为实际可见的能力逐个调用。
616
- // 原因:Pi Tool 自带名称,同名能力必须在进入 Agent 前失败。
617
- addPiTool(candidate: PiToolCandidate): void {
618
- const name = requiredName(candidate.tool.name, "Pi Tool");
619
- if (!EXECUTION_LEVELS.includes(candidate.requiredExecutionLevel)) {
620
- throw new Error(`Pi Tool ${name} execution level is invalid`);
469
+ merge(prepared: PreparedPlugin): void {
470
+ for (const degradation of prepared.degradations) {
471
+ this.reportDegradation(degradation);
472
+ }
473
+
474
+ switch (prepared.kind) {
475
+ case "scope":
476
+ this.commitGuards.push(...prepared.commitGuards);
477
+ break;
478
+ case "profile":
479
+ if (!EXECUTION_LEVELS.includes(prepared.profile.executionLevel)) {
480
+ throw new Error("Runtime execution level is invalid");
481
+ }
482
+ this.profile = prepared.profile;
483
+ break;
484
+ case "provider":
485
+ this.provider = prepared.provider;
486
+ break;
487
+ case "platform":
488
+ this.platform = prepared.platform;
489
+ break;
490
+ case "workspace":
491
+ this.workspace = prepared.workspace;
492
+ this.codeExecution = prepared.codeExecution;
493
+ break;
494
+ case "sandbox":
495
+ this.sandbox = prepared.sandbox;
496
+ break;
497
+ case "memory":
498
+ this.memoryProfile = prepared.memoryProfile;
499
+ this.memory = prepared.memoryPort;
500
+ break;
501
+ case "tool":
502
+ this.hostTools.push(...prepared.hostTools);
503
+ this.toolPolicy = prepared.policy;
504
+ break;
505
+ case "skill":
506
+ for (const skill of prepared.skills) this.addSkill(skill);
507
+ break;
508
+ case "connector":
509
+ for (const connector of prepared.connectors) {
510
+ this.addConnector(connector);
511
+ }
512
+ break;
513
+ case "extension":
514
+ for (const { name, extension } of prepared.extensions) {
515
+ this.addExtension(name, extension);
516
+ }
517
+ break;
518
+ case "schedule":
519
+ this.schedule = prepared.schedule;
520
+ break;
521
+ case "subagent":
522
+ for (const name of prepared.enabledSubagents) {
523
+ this.enabledSubagents.add(requiredName(name, "Subagent"));
524
+ }
525
+ this.subagents = prepared.subagents;
526
+ break;
527
+ case "turn-events":
528
+ this.turnEvents = prepared.turnEvents;
529
+ break;
621
530
  }
622
- if (this.piTools.has(name)) {
623
- throw new Error(`Duplicate Runtime Pi Tool: ${name}`);
624
- }
625
- this.piTools.set(name, candidate);
626
531
  }
627
532
 
628
- // 作用:记录 Turn 期拒绝清单。
629
- // 调用:tool Plugin 完成 Action 挑选后调用一次。
630
- // 原因:空策略也是有效配置,所以用独立标记判断是否重复贡献。
631
- configureDenyPolicy(
632
- policy?: RuntimeDenyPolicy,
633
- ): void {
634
- if (this.denyPolicyConfigured) {
635
- throw new Error(
636
- "Runtime deny policy was contributed more than once",
637
- );
533
+ private addSkill(skill: RuntimeSkillContribution): void {
534
+ const normalized = requiredName(skill.name, "Skill");
535
+ if (this.skillSources.has(normalized)) {
536
+ throw new Error(`Duplicate Runtime Skill: ${normalized}`);
638
537
  }
639
- this.denyPolicyConfigured = true;
640
- this.denyPolicy = policy;
641
- }
642
-
643
- // 作用:加入一个 Skill 的持久化 catalog、内容来源及其独立脚本权限。
644
- // 调用:skill Plugin 为已绑定的 Resource 逐个调用。
645
- // 原因:catalog、来源和权限一起登记,装配无需读取来源且权限不会在 Skill 之间漂移。
646
- addSkillSource(
647
- name: string,
648
- description: string,
649
- source: SkillSource,
650
- script: Partial<RuntimeSkillScriptPolicy> = {},
651
- ): void {
652
- const normalized = this.addSkillDefinition(name);
653
538
  const normalizedDescription = requiredName(
654
- description,
539
+ skill.description,
655
540
  `Runtime Skill "${normalized}" description`,
656
541
  );
542
+ const script = skill.script ?? {};
657
543
  const network = script.network ?? "none";
658
544
  if (network !== "none" && network !== "full") {
659
545
  throw new Error(
@@ -680,7 +566,7 @@ class RuntimeBuilder implements RuntimeContributionContext {
680
566
  this.skillSources.set(normalized, Object.freeze({
681
567
  name: normalized,
682
568
  description: normalizedDescription,
683
- source,
569
+ source: skill.source,
684
570
  script: Object.freeze({
685
571
  network,
686
572
  workspace,
@@ -689,10 +575,7 @@ class RuntimeBuilder implements RuntimeContributionContext {
689
575
  }));
690
576
  }
691
577
 
692
- // 作用:把一个 MCP Connector 加入候选连接表。
693
- // 调用:connector Plugin 为每个已授权连接逐个调用。
694
- // 原因:名称和 URL 在提交前规范化,避免运行时才发现空地址或重名。
695
- addConnector(server: RuntimeMcpServer): void {
578
+ private addConnector(server: RuntimeMcpServer): void {
696
579
  const name = requiredName(server.name, "Connector");
697
580
  if (this.connectors.has(name)) {
698
581
  throw new Error(`Duplicate Runtime Connector: ${name}`);
@@ -706,10 +589,10 @@ class RuntimeBuilder implements RuntimeContributionContext {
706
589
  }));
707
590
  }
708
591
 
709
- // 作用:把一个 Runtime Extension 加入候选扩展表。
710
- // 调用:extension Plugin 为每个已部署扩展逐个调用。
711
- // 原因:注册名必须等于 manifest 名,防止配置选择和实际执行对象错位。
712
- addExtension(name: string, extension: RuntimeExtensionConfig): void {
592
+ private addExtension(
593
+ name: string,
594
+ extension: RuntimeExtensionConfig,
595
+ ): void {
713
596
  const normalized = requiredName(name, "Extension");
714
597
  if (this.extensions.has(normalized)) {
715
598
  throw new Error(`Duplicate Runtime Extension: ${normalized}`);
@@ -722,54 +605,7 @@ class RuntimeBuilder implements RuntimeContributionContext {
722
605
  this.extensions.set(normalized, extension);
723
606
  }
724
607
 
725
- // 作用:记录定时任务调用端口。
726
- // 调用:schedule Plugin 在贡献阶段调用一次。
727
- // 原因:定时任务必须回到当前会话所有者,不能选择多个路由。
728
- bindSchedule(schedule: RuntimeSchedulePort): void {
729
- if (this.schedule) {
730
- throw new Error("Runtime Schedule was contributed more than once");
731
- }
732
- this.schedule = schedule;
733
- }
734
-
735
- // 作用:标记一个 Subagent 类型在本次运行中可用。
736
- // 调用:subagent Plugin 完成部署检查后逐个调用。
737
- // 原因:只记录已部署类型,避免模型拿到无法执行的名称。
738
- enableSubagent(name: string): void {
739
- this.enabledSubagents.add(requiredName(name, "Subagent"));
740
- }
741
-
742
- // 作用:记录 Subagent 的实际执行端口。
743
- // 调用:subagent Plugin 至少启用一种类型时调用一次。
744
- // 原因:启用列表与执行能力分开校验,缺端口时在提交前失败。
745
- bindSubagents(subagents: RuntimeSubagentPort): void {
746
- if (this.subagents) {
747
- throw new Error("Runtime Subagents were contributed more than once");
748
- }
749
- this.subagents = subagents;
750
- }
751
-
752
- // 作用:记录 Turn 完成后的应用投影端口。
753
- // 调用:turn-events Plugin 在贡献阶段调用一次。
754
- // 原因:完成事件只有一个顺序来源,重复绑定可能重复写业务状态。
755
- bindTurnEvents(turnEvents: RuntimeTurnEventsPort): void {
756
- if (this.turnEvents) {
757
- throw new Error("Runtime Turn Events were contributed more than once");
758
- }
759
- this.turnEvents = turnEvents;
760
- }
761
-
762
- // 作用:登记一个提交前必须再次通过的业务检查。
763
- // 调用:容易在加载期间过期的 scope Plugin 添加检查时调用。
764
- // 原因:检查延迟到最终 commit 前,可阻止过期候选覆盖当前 Runtime。
765
- addCommitGuard(guard: () => Promise<void>): void {
766
- this.commitGuards.push(guard);
767
- }
768
-
769
- // 作用:把一项可继续运行的能力缺失加入诊断。
770
- // 调用:Runtime 汇总每个 PreparedPlugin 的降级信息时调用。
771
- // 原因:复制并冻结对象,避免 Plugin 在提交后改写诊断。
772
- reportDegradation(degradation: RuntimeDegradation): void {
608
+ private reportDegradation(degradation: RuntimeDegradation): void {
773
609
  this.degradations.push(Object.freeze({ ...degradation }));
774
610
  }
775
611
 
@@ -777,10 +613,10 @@ class RuntimeBuilder implements RuntimeContributionContext {
777
613
 
778
614
  // #region RuntimeBuilder 校验与候选生成
779
615
 
780
- // 作用:校验全部贡献,并生成一个不可变候选 Runtime。
781
- // 调用:所有 Plugin 按固定顺序完成 contribute 后调用一次。
616
+ // 作用:校验全部声明,并生成一个不可变候选 Runtime。
617
+ // 调用:所有 Plugin 按固定顺序完成合并后调用一次。
782
618
  // 原因:所有交叉约束都在这里通过后才返回候选,旧 Snapshot 不会半更新。
783
- build(): RuntimeCandidate {
619
+ async build(): Promise<RuntimeCandidate> {
784
620
  if (!this.profile) {
785
621
  throw new Error("Runtime profile Plugin did not configure a profile");
786
622
  }
@@ -834,7 +670,7 @@ class RuntimeBuilder implements RuntimeContributionContext {
834
670
  model: this.profile.model.trim(),
835
671
  thinking: this.profile.thinking,
836
672
  systemPrompt: this.profile.systemPrompt,
837
- denyPolicy: this.denyPolicy,
673
+ denyPolicy: this.toolPolicy?.denyPolicy,
838
674
  enabledSubagents: [...this.enabledSubagents],
839
675
  mcpServers: [...this.connectors.values()],
840
676
  executionLevel: this.profile.executionLevel,
@@ -864,23 +700,32 @@ class RuntimeBuilder implements RuntimeContributionContext {
864
700
  }
865
701
  const resolvedModel = resolvePiModel(provider, profile.model);
866
702
  const endpoint = endpoints[0]!;
867
- const webSearch = endpoint.protocol === "openrouter-chat"
868
- ? undefined
869
- : createWebSearch({
870
- endpoint,
871
- model: profile.model,
872
- maxTokens: resolvedModel.maxTokens,
873
- reasoning: resolvedModel.reasoning,
874
- });
875
- if (!webSearch) {
876
- this.reportDegradation({
877
- capability: "web_search",
878
- reason: "unavailable",
879
- detail: "Native web search is unavailable for openrouter-chat",
880
- });
881
- }
882
- for (const candidate of basePiToolCandidates(webSearch)) {
883
- this.addPiTool(candidate);
703
+ const webSearch = createWebSearch({
704
+ endpoint,
705
+ model: profile.model,
706
+ maxTokens: resolvedModel.maxTokens,
707
+ reasoning: resolvedModel.reasoning,
708
+ });
709
+ const skillSources = [...this.skillSources.values()];
710
+ const toolSurface = await createToolSurface({
711
+ platform: this.platform,
712
+ ...(this.workspace ? { workspace: this.workspace } : {}),
713
+ ...(this.memoryProfile.enabled && this.memory
714
+ ? { memory: { port: this.memory, profile: this.memoryProfile } }
715
+ : {}),
716
+ ...(this.codeExecution ? { codeExecution: this.codeExecution } : {}),
717
+ ...(this.sandbox ? { sandbox: this.sandbox } : {}),
718
+ hostTools: this.hostTools,
719
+ skills: skillSources,
720
+ ...(this.schedule ? { schedule: this.schedule } : {}),
721
+ ...(this.subagents ? { subagents: this.subagents } : {}),
722
+ enabledSubagents: [...this.enabledSubagents],
723
+ webSearch,
724
+ extensions: [...this.extensions.values()],
725
+ ...(this.toolPolicy ? { policy: this.toolPolicy } : {}),
726
+ });
727
+ for (const degradation of toolSurface.degradations) {
728
+ this.reportDegradation(degradation);
884
729
  }
885
730
  const bindings: RuntimeBindings = Object.freeze({
886
731
  provider,
@@ -890,9 +735,7 @@ class RuntimeBuilder implements RuntimeContributionContext {
890
735
  ? { memory: this.memory }
891
736
  : {}),
892
737
  skills: Object.freeze({
893
- sources: Object.freeze([
894
- ...this.skillSources.values(),
895
- ]),
738
+ sources: Object.freeze(skillSources),
896
739
  }),
897
740
  ...(this.turnEvents ? { turnEvents: this.turnEvents } : {}),
898
741
  });
@@ -902,9 +745,9 @@ class RuntimeBuilder implements RuntimeContributionContext {
902
745
  pi: createPiRuntimeAssembly({
903
746
  profile,
904
747
  provider,
905
- toolCandidates: [...this.piTools.values()],
748
+ toolSurface: toolSurface.surface,
906
749
  mcpServers: profile.mcpServers,
907
- extensions: [...this.extensions.values()],
750
+ extensions: toolSurface.extensions,
908
751
  }),
909
752
  degradations: Object.freeze([
910
753
  ...this.degradations,
@@ -919,21 +762,10 @@ class RuntimeBuilder implements RuntimeContributionContext {
919
762
  });
920
763
  }
921
764
 
922
- // 作用:登记一个 Skill 名称并返回规范化结果。
923
- // 调用:Host 文档和外部 Skill 来源写入前调用。
924
- // 原因:所有 Skill 来源共享一个命名空间,重名必须在装配时暴露。
925
- private addSkillDefinition(name: string): string {
926
- const normalized = requiredName(name, "Skill");
927
- if (this.skillDefinitions.has(normalized)) {
928
- throw new Error(`Duplicate Runtime Skill: ${normalized}`);
929
- }
930
- this.skillDefinitions.add(normalized);
931
- return normalized;
932
- }
933
765
  }
934
766
 
935
767
  // 作用:检查 Plugin kind 合法、唯一,并包含所有必需能力。
936
- // 调用:任何 loader 启动前由 `prepareRuntimeCandidate` 调用。
768
+ // 调用:任何 prepare 启动前由 `prepareRuntimeCandidate` 调用。
937
769
  // 原因:先校验结构可避免为注定失败的配置执行外部读取。
938
770
  function validatePluginKinds(
939
771
  plugins: readonly AgentPlugin[],
@@ -968,7 +800,7 @@ function validatePluginKinds(
968
800
  * @remarks
969
801
  * `AgentRuntimeKernel` 在初始化或重载时调用它。
970
802
  *
971
- * loader 并行执行,贡献按固定 kind 顺序执行,任何失败都不会产生 commit。
803
+ * prepare 并行执行,声明按固定 kind 顺序合并,任何失败都不会产生 commit。
972
804
  *
973
805
  * 该函数有意不从包入口导出,提交生命周期只能由 Kernel 控制。
974
806
  *
@@ -994,16 +826,11 @@ export async function prepareRuntimeCandidate(
994
826
  prepared.map((plugin) => [plugin.kind, plugin]),
995
827
  );
996
828
  const builder = new RuntimeBuilder();
997
- const contributionContext =
998
- builder.contributionContext();
999
829
 
1000
830
  for (const kind of PLUGIN_ORDER) {
1001
831
  const plugin = byKind.get(kind);
1002
832
  if (!plugin) continue;
1003
- for (const degradation of plugin.degradations) {
1004
- builder.reportDegradation(degradation);
1005
- }
1006
- await plugin.contribute(contributionContext);
833
+ builder.merge(plugin);
1007
834
  }
1008
835
 
1009
836
  return builder.build();
@@ -1015,7 +842,7 @@ export async function prepareRuntimeCandidate(
1015
842
  * @remarks
1016
843
  * `AgentRuntimeKernel.initConfig` 在首次加载或显式重载时调用它。
1017
844
  *
1018
- * `commit` 只在全部 loader、贡献、校验和 guard 成功后调用一次。
845
+ * `commit` 只在全部准备、合并、校验和 guard 成功后调用一次。
1019
846
  *
1020
847
  * @internal
1021
848
  */