@springbrand/agent-runtime 0.1.3-alpha.0 → 0.1.3-alpha.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.
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,27 @@ 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
36
  import { basePiToolCandidates } from "./pi/tool/base";
37
+ import {
38
+ browserQuickActionPiToolCandidates,
39
+ codeExecutionPiToolCandidate,
40
+ } from "./pi/tool/core";
41
+ import {
42
+ sandboxPiToolCandidates,
43
+ workspacePiToolCandidates,
44
+ } from "./pi/tool/workspace-sandbox";
45
+ import { schedulePiToolCandidates } from "./pi/tool/schedule";
46
+ import { skillPiToolCandidates } from "./pi/tool/skill";
47
+ import { subagentPiToolCandidates } from "./pi/tool/subagent";
34
48
  import { createWebSearch } from "./pi/tool/web-search";
35
49
  import { resolvePiModel } from "./pi/runtime-adapter/models";
36
50
 
37
51
  /**
38
- * 本文件实现 Plugin 的准备、贡献、校验和原子候选装配。
52
+ * 本文件实现 Plugin 的准备、声明合并、校验和原子候选装配。
39
53
  *
40
54
  * @remarks
41
55
  * 核心术语见包入口 `index.ts`,这里不重复定义。
@@ -70,332 +84,152 @@ export type PluginKind =
70
84
  export type { RuntimeDegradation } from "./kernel/degradation";
71
85
 
72
86
  /**
73
- * loader 读取到的值和本次降级信息一起返回。
87
+ * 提供 Runtime 核心运行参数。
74
88
  *
75
89
  * @remarks
76
- * `definePlugin` 在准备阶段读取它。
90
+ * `profile` Plugin 在准备结果中返回它。
77
91
  *
78
- * 值与诊断一起返回,可以避免 loader 直接修改 Runtime。
92
+ * 这里只放业务可配置字段,冻结和最终结构由 Runtime 负责。
79
93
  */
80
- export interface PluginLoadResult<T> {
81
- readonly value: T;
82
- readonly degradations: readonly RuntimeDegradation[];
94
+ export interface RuntimeProfileContribution {
95
+ readonly model: string;
96
+ readonly thinking: ThinkingEffort;
97
+ readonly systemPrompt?: string;
98
+ readonly executionLevel: ExecutionLevel;
83
99
  }
84
100
 
85
101
  /**
86
- * 表示一个尚未准备的 Runtime Plugin
87
- *
88
- * @remarks
89
- * 应用把它放进 `AgentConfig.plugins`。
90
- *
91
- * Runtime 只调用 `prepare`,不会接触 Plugin 捕获的业务依赖。
102
+ * 描述一个已准备 Skill
92
103
  */
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>;
104
+ export interface RuntimeSkillContribution {
105
+ readonly name: string;
106
+ readonly description: string;
107
+ readonly source: SkillSource;
108
+ readonly script?: Partial<RuntimeSkillScriptPolicy>;
104
109
  }
105
110
 
106
111
  /**
107
- * 表示 loader 已完成、但还没有写入候选结果的 Plugin
108
- *
109
- * @remarks
110
- * Runtime 按固定 kind 顺序调用 `contribute`。
111
- *
112
- * 准备和贡献分开,才能并行读取数据又保持确定的合并顺序。
112
+ * 描述一个已准备 Extension
113
113
  */
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>;
114
+ export interface RuntimeExtensionContribution {
115
+ readonly name: string;
116
+ readonly extension: RuntimeExtensionConfig;
128
117
  }
129
118
 
130
119
  /**
131
- * 描述一个 Plugin 应该怎样读取数据并贡献能力。
132
- *
133
- * @remarks
134
- * 应用把该对象传给 `definePlugin`。
120
+ * 描述 Agent 对最终 Tool Surface 的可见性约束。
135
121
  *
136
- * `loader` 负责读取,`contribute` 负责组装,二者不要互相越权。
122
+ * Host 只提供策略;Runtime 在唯一 Tool Surface seam 中对所有
123
+ * Port 和 Resource 生成的候选项统一应用。
137
124
  */
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>;
125
+ export interface RuntimeToolSurfacePolicy {
126
+ readonly denyPolicy?: RuntimeDenyPolicy;
127
+ readonly allowsTool?: (name: string) => boolean;
128
+ readonly allowsExtension?: (extension: RuntimeExtensionConfig) => boolean;
161
129
  }
162
130
 
163
- /**
164
- * 汇总一次 Runtime 装配要使用的全部 Plugin。
165
- *
166
- * @remarks
167
- * `RuntimeAgentDefinition.createConfig` 在首次加载或显式重载时返回它。
168
- *
169
- * 它只保存可信函数对象,不用于持久化或跨进程序列化。
170
- */
171
- export interface AgentConfig {
172
- readonly plugins: readonly AgentPlugin[];
131
+ interface PluginResultByKind {
132
+ readonly scope: {
133
+ readonly commitGuards: readonly (() => Promise<void>)[];
134
+ };
135
+ readonly profile: { readonly profile: RuntimeProfileContribution };
136
+ readonly provider: { readonly provider: RuntimeProviderPort };
137
+ readonly platform: {
138
+ readonly platform: RuntimePlatformPort;
139
+ };
140
+ readonly workspace: {
141
+ readonly workspace?: WorkspacePort;
142
+ readonly codeExecution?: RuntimeCodeExecutionPort;
143
+ };
144
+ readonly sandbox: {
145
+ readonly sandbox?: RuntimeSandboxPort;
146
+ };
147
+ readonly memory: {
148
+ readonly memoryProfile: RuntimeMemoryProfile;
149
+ readonly memoryPort?: RuntimeMemoryPort;
150
+ };
151
+ readonly tool: {
152
+ readonly hostTools: readonly PiToolCandidate[];
153
+ readonly policy?: RuntimeToolSurfacePolicy;
154
+ };
155
+ readonly skill: {
156
+ readonly skills: readonly RuntimeSkillContribution[];
157
+ };
158
+ readonly connector: {
159
+ readonly connectors: readonly RuntimeMcpServer[];
160
+ };
161
+ readonly extension: {
162
+ readonly extensions: readonly RuntimeExtensionContribution[];
163
+ };
164
+ readonly schedule: {
165
+ readonly schedule: RuntimeSchedulePort;
166
+ };
167
+ readonly subagent: {
168
+ readonly enabledSubagents: readonly string[];
169
+ readonly subagents?: RuntimeSubagentPort;
170
+ };
171
+ readonly "turn-events": {
172
+ readonly turnEvents: RuntimeTurnEventsPort;
173
+ };
173
174
  }
174
175
 
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;
176
+ export type PluginPreparation<K extends PluginKind> = Readonly<
177
+ PluginResultByKind[K] & {
178
+ readonly degradations: readonly RuntimeDegradation[];
179
+ }
180
+ >;
181
+
182
+ export type PreparedPlugin<K extends PluginKind = PluginKind> =
183
+ K extends PluginKind
184
+ ? Readonly<PluginPreparation<K> & { readonly kind: K }>
185
+ : never;
186
+
187
+ interface AgentPluginContract<K extends PluginKind> {
188
+ readonly kind: K;
189
+ prepare(): Promise<PreparedPlugin<K>>;
190
+ }
191
+
192
+ export type AgentPlugin<K extends PluginKind = PluginKind> =
193
+ K extends PluginKind ? AgentPluginContract<K> : never;
194
+
195
+ export interface AgentPluginSpec<K extends PluginKind> {
196
+ readonly kind: K;
197
+ prepare(): Promise<PluginPreparation<K>>;
188
198
  }
189
199
 
190
200
  /**
191
- * 限定 Plugin 可以写入候选 Runtime 的内容。
192
- *
193
- * @remarks
194
- * Plugin 的 `contribute` 回调使用这些方法。
195
- *
196
- * Runtime 只暴露绑定和增量添加操作,避免 Plugin 取得 Builder 的可变引用。
201
+ * 汇总一次 Runtime 装配要使用的全部 Plugin。
197
202
  */
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;
203
+ export interface AgentConfig {
204
+ readonly plugins: readonly AgentPlugin[];
364
205
  }
365
206
 
366
207
  /**
367
- * loader 与贡献回调包装成 Runtime 可准备的 Plugin。
368
- *
369
- * @remarks
370
- * 应用在创建 `AgentConfig` 时调用它,不需要手写 `prepare`。
208
+ * 定义一个只准备声明式结果的 Runtime Plugin。
371
209
  *
372
- * 包装层会冻结准备结果,避免读取完成后被调用方改写。
210
+ * Runtime 内部负责排序、合并、冲突检查和提交。
373
211
  */
374
- export function definePlugin<T>(
375
- spec: AgentPluginSpec<T>,
376
- ): AgentPlugin {
212
+ export function definePlugin<K extends PluginKind>(
213
+ spec: AgentPluginSpec<K>,
214
+ ): AgentPlugin<K> {
377
215
  return Object.freeze({
378
216
  kind: spec.kind,
379
217
 
380
- // 作用:读取本 Plugin 的数据,并把结果变成不可变的准备对象。
381
- // 调用:Runtime 在候选装配开始时调用,应用不应直接调用。
382
- // 原因:此处只准备数据,不贡献能力,才能安全地与其他 loader 并行。
383
- async prepare(): Promise<PreparedPlugin> {
384
- const loaded = await spec.loader();
218
+ async prepare(): Promise<PreparedPlugin<K>> {
219
+ const prepared = await spec.prepare();
385
220
  const degradations = Object.freeze(
386
- loaded.degradations.map((degradation) =>
221
+ prepared.degradations.map((degradation) =>
387
222
  Object.freeze({ ...degradation }),
388
223
  ),
389
224
  );
390
225
 
391
226
  return Object.freeze({
227
+ ...prepared,
392
228
  kind: spec.kind,
393
229
  degradations,
394
- contribute: (ctx: RuntimeContributionContext) =>
395
- spec.contribute(loaded.value, ctx),
396
- });
230
+ }) as PreparedPlugin<K>;
397
231
  },
398
- });
232
+ }) as AgentPlugin<K>;
399
233
  }
400
234
 
401
235
  // #endregion
@@ -451,7 +285,6 @@ const DISABLED_MEMORY: RuntimeMemoryProfile = Object.freeze({
451
285
  enabled: false,
452
286
  memoryTokens: 2_000,
453
287
  preferencesTokens: 500,
454
- compactAfterTokens: 100_000,
455
288
  });
456
289
 
457
290
  // 作用:把外部名称整理成非空字符串。
@@ -479,22 +312,107 @@ function assertMemoryProfile(profile: RuntimeMemoryProfile): void {
479
312
  }
480
313
  }
481
314
 
315
+ interface ToolSurfaceInput {
316
+ readonly platform: RuntimePlatformPort;
317
+ readonly workspace?: WorkspacePort;
318
+ readonly codeExecution?: RuntimeCodeExecutionPort;
319
+ readonly sandbox?: RuntimeSandboxPort;
320
+ readonly hostTools: readonly PiToolCandidate[];
321
+ readonly skills: readonly RuntimeSkillSourceBinding[];
322
+ readonly schedule?: RuntimeSchedulePort;
323
+ readonly subagents?: RuntimeSubagentPort;
324
+ readonly enabledSubagents: readonly string[];
325
+ readonly webSearch?: Parameters<typeof basePiToolCandidates>[0];
326
+ readonly extensions: readonly RuntimeExtensionConfig[];
327
+ readonly policy?: RuntimeToolSurfacePolicy;
328
+ }
329
+
330
+ interface ToolSurface {
331
+ readonly surface: PiToolSurface;
332
+ readonly extensions: readonly RuntimeExtensionConfig[];
333
+ readonly degradations: readonly RuntimeDegradation[];
334
+ }
335
+
336
+ // 唯一 Tool Surface seam:只读取已授权 Port、Resource 和 Agent policy,
337
+ // 统一生成可见 Tool,并在一处做名称、执行档位与冲突校验。
338
+ function createToolSurface(input: ToolSurfaceInput): ToolSurface {
339
+ const staticCandidates = [
340
+ ...(input.platform.browser
341
+ ? browserQuickActionPiToolCandidates(input.platform.browser)
342
+ : []),
343
+ ...(input.workspace ? workspacePiToolCandidates(input.workspace) : []),
344
+ ...(input.workspace && input.codeExecution
345
+ ? [codeExecutionPiToolCandidate(input.codeExecution)]
346
+ : []),
347
+ ...(input.sandbox ? sandboxPiToolCandidates(input.sandbox) : []),
348
+ ...input.hostTools,
349
+ ...skillPiToolCandidates(input.skills, {
350
+ loader: input.platform.loader,
351
+ }),
352
+ ...(input.schedule ? schedulePiToolCandidates(input.schedule) : []),
353
+ ...subagentPiToolCandidates(input.subagents, input.enabledSubagents),
354
+ ...basePiToolCandidates(input.webSearch),
355
+ ];
356
+ const deny = new Set(input.policy?.denyPolicy?.deny ?? []);
357
+ const allowsTool = input.policy?.allowsTool;
358
+ const allowsExtension = input.policy?.allowsExtension;
359
+
360
+ return {
361
+ surface: Object.freeze({
362
+ finalize(authorizedCandidates: readonly PiToolCandidate[]) {
363
+ const tools = new Map<string, PiToolCandidate>();
364
+
365
+ for (const candidate of [
366
+ ...staticCandidates,
367
+ ...authorizedCandidates,
368
+ ]) {
369
+ const name = requiredName(candidate.tool.name, "Pi Tool");
370
+ if (
371
+ !candidate.authorized ||
372
+ deny.has(name) ||
373
+ allowsTool?.(name) === false
374
+ ) continue;
375
+ if (!EXECUTION_LEVELS.includes(candidate.requiredExecutionLevel)) {
376
+ throw new Error(`Pi Tool ${name} execution level is invalid`);
377
+ }
378
+ if (tools.has(name)) {
379
+ throw new Error(`Duplicate Runtime Pi Tool: ${name}`);
380
+ }
381
+ tools.set(name, Object.freeze({ ...candidate }));
382
+ }
383
+
384
+ return Object.freeze([...tools.values()]);
385
+ },
386
+ }),
387
+ extensions: input.extensions.filter(
388
+ (extension) => allowsExtension?.(extension) !== false,
389
+ ),
390
+ degradations: input.webSearch
391
+ ? []
392
+ : [{
393
+ capability: "web_search",
394
+ reason: "unavailable",
395
+ detail: "Native web search is unavailable for openrouter-chat",
396
+ }],
397
+ };
398
+ }
399
+
482
400
  // #endregion
483
401
 
484
- // #region RuntimeBuilder 贡献接口
402
+ // #region RuntimeBuilder 合并
485
403
 
486
- class RuntimeBuilder implements RuntimeContributionContext {
404
+ class RuntimeBuilder {
487
405
  private profile?: RuntimeProfileContribution;
488
406
  private provider?: RuntimeProviderPort;
489
407
  private platform?: RuntimePlatformPort;
490
408
  private workspace?: WorkspacePort;
409
+ private codeExecution?: RuntimeCodeExecutionPort;
491
410
  private sandbox?: RuntimeSandboxPort;
411
+ private schedule?: RuntimeSchedulePort;
492
412
  private memoryProfile: RuntimeMemoryProfile = DISABLED_MEMORY;
493
413
  private memory?: RuntimeMemoryPort;
494
- private memoryConfigured = false;
495
- private denyPolicy?: RuntimeDenyPolicy;
496
- private denyPolicyConfigured = false;
497
- private readonly piTools = new Map<string, PiToolCandidate>();
414
+ private readonly hostTools: PiToolCandidate[] = [];
415
+ private toolPolicy?: RuntimeToolSurfacePolicy;
498
416
  private readonly skillSources = new Map<
499
417
  string,
500
418
  {
@@ -504,156 +422,88 @@ class RuntimeBuilder implements RuntimeContributionContext {
504
422
  script: RuntimeSkillScriptPolicy;
505
423
  }
506
424
  >();
507
- private readonly skillDefinitions = new Set<string>();
508
425
  private readonly connectors = new Map<string, RuntimeMcpServer>();
509
426
  private readonly extensions = new Map<string, RuntimeExtensionConfig>();
510
- private schedule?: RuntimeSchedulePort;
511
427
  private readonly enabledSubagents = new Set<string>();
512
428
  private subagents?: RuntimeSubagentPort;
513
429
  private turnEvents?: RuntimeTurnEventsPort;
514
430
  private readonly commitGuards: Array<() => Promise<void>> = [];
515
431
  private readonly degradations: RuntimeDegradation[] = [];
516
432
 
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");
433
+ merge(prepared: PreparedPlugin): void {
434
+ for (const degradation of prepared.degradations) {
435
+ this.reportDegradation(degradation);
436
+ }
437
+
438
+ switch (prepared.kind) {
439
+ case "scope":
440
+ this.commitGuards.push(...prepared.commitGuards);
441
+ break;
442
+ case "profile":
443
+ if (!EXECUTION_LEVELS.includes(prepared.profile.executionLevel)) {
444
+ throw new Error("Runtime execution level is invalid");
445
+ }
446
+ this.profile = prepared.profile;
447
+ break;
448
+ case "provider":
449
+ this.provider = prepared.provider;
450
+ break;
451
+ case "platform":
452
+ this.platform = prepared.platform;
453
+ break;
454
+ case "workspace":
455
+ this.workspace = prepared.workspace;
456
+ this.codeExecution = prepared.codeExecution;
457
+ break;
458
+ case "sandbox":
459
+ this.sandbox = prepared.sandbox;
460
+ break;
461
+ case "memory":
462
+ this.memoryProfile = prepared.memoryProfile;
463
+ this.memory = prepared.memoryPort;
464
+ break;
465
+ case "tool":
466
+ this.hostTools.push(...prepared.hostTools);
467
+ this.toolPolicy = prepared.policy;
468
+ break;
469
+ case "skill":
470
+ for (const skill of prepared.skills) this.addSkill(skill);
471
+ break;
472
+ case "connector":
473
+ for (const connector of prepared.connectors) {
474
+ this.addConnector(connector);
475
+ }
476
+ break;
477
+ case "extension":
478
+ for (const { name, extension } of prepared.extensions) {
479
+ this.addExtension(name, extension);
480
+ }
481
+ break;
482
+ case "schedule":
483
+ this.schedule = prepared.schedule;
484
+ break;
485
+ case "subagent":
486
+ for (const name of prepared.enabledSubagents) {
487
+ this.enabledSubagents.add(requiredName(name, "Subagent"));
488
+ }
489
+ this.subagents = prepared.subagents;
490
+ break;
491
+ case "turn-events":
492
+ this.turnEvents = prepared.turnEvents;
493
+ break;
550
494
  }
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
495
  }
566
496
 
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`);
621
- }
622
- if (this.piTools.has(name)) {
623
- throw new Error(`Duplicate Runtime Pi Tool: ${name}`);
624
- }
625
- this.piTools.set(name, candidate);
626
- }
627
-
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
- );
497
+ private addSkill(skill: RuntimeSkillContribution): void {
498
+ const normalized = requiredName(skill.name, "Skill");
499
+ if (this.skillSources.has(normalized)) {
500
+ throw new Error(`Duplicate Runtime Skill: ${normalized}`);
638
501
  }
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
502
  const normalizedDescription = requiredName(
654
- description,
503
+ skill.description,
655
504
  `Runtime Skill "${normalized}" description`,
656
505
  );
506
+ const script = skill.script ?? {};
657
507
  const network = script.network ?? "none";
658
508
  if (network !== "none" && network !== "full") {
659
509
  throw new Error(
@@ -680,7 +530,7 @@ class RuntimeBuilder implements RuntimeContributionContext {
680
530
  this.skillSources.set(normalized, Object.freeze({
681
531
  name: normalized,
682
532
  description: normalizedDescription,
683
- source,
533
+ source: skill.source,
684
534
  script: Object.freeze({
685
535
  network,
686
536
  workspace,
@@ -689,10 +539,7 @@ class RuntimeBuilder implements RuntimeContributionContext {
689
539
  }));
690
540
  }
691
541
 
692
- // 作用:把一个 MCP Connector 加入候选连接表。
693
- // 调用:connector Plugin 为每个已授权连接逐个调用。
694
- // 原因:名称和 URL 在提交前规范化,避免运行时才发现空地址或重名。
695
- addConnector(server: RuntimeMcpServer): void {
542
+ private addConnector(server: RuntimeMcpServer): void {
696
543
  const name = requiredName(server.name, "Connector");
697
544
  if (this.connectors.has(name)) {
698
545
  throw new Error(`Duplicate Runtime Connector: ${name}`);
@@ -706,10 +553,10 @@ class RuntimeBuilder implements RuntimeContributionContext {
706
553
  }));
707
554
  }
708
555
 
709
- // 作用:把一个 Runtime Extension 加入候选扩展表。
710
- // 调用:extension Plugin 为每个已部署扩展逐个调用。
711
- // 原因:注册名必须等于 manifest 名,防止配置选择和实际执行对象错位。
712
- addExtension(name: string, extension: RuntimeExtensionConfig): void {
556
+ private addExtension(
557
+ name: string,
558
+ extension: RuntimeExtensionConfig,
559
+ ): void {
713
560
  const normalized = requiredName(name, "Extension");
714
561
  if (this.extensions.has(normalized)) {
715
562
  throw new Error(`Duplicate Runtime Extension: ${normalized}`);
@@ -722,54 +569,7 @@ class RuntimeBuilder implements RuntimeContributionContext {
722
569
  this.extensions.set(normalized, extension);
723
570
  }
724
571
 
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 {
572
+ private reportDegradation(degradation: RuntimeDegradation): void {
773
573
  this.degradations.push(Object.freeze({ ...degradation }));
774
574
  }
775
575
 
@@ -777,8 +577,8 @@ class RuntimeBuilder implements RuntimeContributionContext {
777
577
 
778
578
  // #region RuntimeBuilder 校验与候选生成
779
579
 
780
- // 作用:校验全部贡献,并生成一个不可变候选 Runtime。
781
- // 调用:所有 Plugin 按固定顺序完成 contribute 后调用一次。
580
+ // 作用:校验全部声明,并生成一个不可变候选 Runtime。
581
+ // 调用:所有 Plugin 按固定顺序完成合并后调用一次。
782
582
  // 原因:所有交叉约束都在这里通过后才返回候选,旧 Snapshot 不会半更新。
783
583
  build(): RuntimeCandidate {
784
584
  if (!this.profile) {
@@ -834,7 +634,7 @@ class RuntimeBuilder implements RuntimeContributionContext {
834
634
  model: this.profile.model.trim(),
835
635
  thinking: this.profile.thinking,
836
636
  systemPrompt: this.profile.systemPrompt,
837
- denyPolicy: this.denyPolicy,
637
+ denyPolicy: this.toolPolicy?.denyPolicy,
838
638
  enabledSubagents: [...this.enabledSubagents],
839
639
  mcpServers: [...this.connectors.values()],
840
640
  executionLevel: this.profile.executionLevel,
@@ -872,15 +672,23 @@ class RuntimeBuilder implements RuntimeContributionContext {
872
672
  maxTokens: resolvedModel.maxTokens,
873
673
  reasoning: resolvedModel.reasoning,
874
674
  });
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);
675
+ const skillSources = [...this.skillSources.values()];
676
+ const toolSurface = createToolSurface({
677
+ platform: this.platform,
678
+ ...(this.workspace ? { workspace: this.workspace } : {}),
679
+ ...(this.codeExecution ? { codeExecution: this.codeExecution } : {}),
680
+ ...(this.sandbox ? { sandbox: this.sandbox } : {}),
681
+ hostTools: this.hostTools,
682
+ skills: skillSources,
683
+ ...(this.schedule ? { schedule: this.schedule } : {}),
684
+ ...(this.subagents ? { subagents: this.subagents } : {}),
685
+ enabledSubagents: [...this.enabledSubagents],
686
+ ...(webSearch ? { webSearch } : {}),
687
+ extensions: [...this.extensions.values()],
688
+ ...(this.toolPolicy ? { policy: this.toolPolicy } : {}),
689
+ });
690
+ for (const degradation of toolSurface.degradations) {
691
+ this.reportDegradation(degradation);
884
692
  }
885
693
  const bindings: RuntimeBindings = Object.freeze({
886
694
  provider,
@@ -890,9 +698,7 @@ class RuntimeBuilder implements RuntimeContributionContext {
890
698
  ? { memory: this.memory }
891
699
  : {}),
892
700
  skills: Object.freeze({
893
- sources: Object.freeze([
894
- ...this.skillSources.values(),
895
- ]),
701
+ sources: Object.freeze(skillSources),
896
702
  }),
897
703
  ...(this.turnEvents ? { turnEvents: this.turnEvents } : {}),
898
704
  });
@@ -902,9 +708,9 @@ class RuntimeBuilder implements RuntimeContributionContext {
902
708
  pi: createPiRuntimeAssembly({
903
709
  profile,
904
710
  provider,
905
- toolCandidates: [...this.piTools.values()],
711
+ toolSurface: toolSurface.surface,
906
712
  mcpServers: profile.mcpServers,
907
- extensions: [...this.extensions.values()],
713
+ extensions: toolSurface.extensions,
908
714
  }),
909
715
  degradations: Object.freeze([
910
716
  ...this.degradations,
@@ -919,21 +725,10 @@ class RuntimeBuilder implements RuntimeContributionContext {
919
725
  });
920
726
  }
921
727
 
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
728
  }
934
729
 
935
730
  // 作用:检查 Plugin kind 合法、唯一,并包含所有必需能力。
936
- // 调用:任何 loader 启动前由 `prepareRuntimeCandidate` 调用。
731
+ // 调用:任何 prepare 启动前由 `prepareRuntimeCandidate` 调用。
937
732
  // 原因:先校验结构可避免为注定失败的配置执行外部读取。
938
733
  function validatePluginKinds(
939
734
  plugins: readonly AgentPlugin[],
@@ -968,7 +763,7 @@ function validatePluginKinds(
968
763
  * @remarks
969
764
  * `AgentRuntimeKernel` 在初始化或重载时调用它。
970
765
  *
971
- * loader 并行执行,贡献按固定 kind 顺序执行,任何失败都不会产生 commit。
766
+ * prepare 并行执行,声明按固定 kind 顺序合并,任何失败都不会产生 commit。
972
767
  *
973
768
  * 该函数有意不从包入口导出,提交生命周期只能由 Kernel 控制。
974
769
  *
@@ -994,16 +789,11 @@ export async function prepareRuntimeCandidate(
994
789
  prepared.map((plugin) => [plugin.kind, plugin]),
995
790
  );
996
791
  const builder = new RuntimeBuilder();
997
- const contributionContext =
998
- builder.contributionContext();
999
792
 
1000
793
  for (const kind of PLUGIN_ORDER) {
1001
794
  const plugin = byKind.get(kind);
1002
795
  if (!plugin) continue;
1003
- for (const degradation of plugin.degradations) {
1004
- builder.reportDegradation(degradation);
1005
- }
1006
- await plugin.contribute(contributionContext);
796
+ builder.merge(plugin);
1007
797
  }
1008
798
 
1009
799
  return builder.build();
@@ -1015,7 +805,7 @@ export async function prepareRuntimeCandidate(
1015
805
  * @remarks
1016
806
  * `AgentRuntimeKernel.initConfig` 在首次加载或显式重载时调用它。
1017
807
  *
1018
- * `commit` 只在全部 loader、贡献、校验和 guard 成功后调用一次。
808
+ * `commit` 只在全部准备、合并、校验和 guard 成功后调用一次。
1019
809
  *
1020
810
  * @internal
1021
811
  */