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

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.
Files changed (79) hide show
  1. package/package.json +12 -3
  2. package/src/adapter/cloudflare/index.ts +56 -0
  3. package/src/adapter/cloudflare/resources/runtime-resources.ts +89 -0
  4. package/src/adapter/cloudflare/sandbox/adapter.ts +1513 -0
  5. package/src/adapter/cloudflare/sandbox/id.ts +23 -0
  6. package/src/adapter/cloudflare/sandbox/policy.ts +15 -0
  7. package/src/adapter/cloudflare/subagent/definition.ts +574 -0
  8. package/src/adapter/cloudflare/subagent/runner.ts +175 -0
  9. package/src/adapter/cloudflare/subagent/tools.ts +254 -0
  10. package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
  11. package/src/adapter/cloudflare/universal-agent/preparation.ts +277 -0
  12. package/src/adapter/cloudflare/universal-agent/tools.ts +80 -0
  13. package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
  14. package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
  15. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
  16. package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
  17. package/src/agent-tool-runtime.ts +152 -0
  18. package/src/db/agent-tool.repo.ts +27 -0
  19. package/src/db/index.ts +33 -0
  20. package/src/db/interaction.repo.ts +185 -0
  21. package/src/db/schema.ts +25 -1
  22. package/src/db/submission.repo.ts +63 -1
  23. package/src/index.ts +61 -27
  24. package/src/kernel/approval-lifecycle.ts +41 -6
  25. package/src/kernel/bindings.ts +99 -12
  26. package/src/kernel/extensions.ts +1 -1
  27. package/src/kernel/interaction-lifecycle.ts +395 -0
  28. package/src/kernel/profile.ts +3 -4
  29. package/src/kernel/public-contracts.ts +2 -0
  30. package/src/kernel/recoverable-chat-agent.ts +104 -6
  31. package/src/kernel/runtime-assembly-view.ts +37 -0
  32. package/src/kernel/runtime-assembly.ts +41 -0
  33. package/src/kernel/runtime-config.ts +4 -0
  34. package/src/kernel/runtime-load.ts +191 -0
  35. package/src/kernel/state.ts +12 -1
  36. package/src/kernel/submission-lifecycle.ts +33 -2
  37. package/src/layers/orchestration/temporary-agent/core.ts +12 -1
  38. package/src/layers/orchestration/temporary-agent/runner.ts +1 -67
  39. package/src/lib/mcp.ts +7 -3
  40. package/src/lib/prompt.ts +1 -1
  41. package/src/lib/telemetry-dev.ts +7 -4
  42. package/src/pi/assembly/context.ts +4 -6
  43. package/src/pi/assembly/extensions.ts +11 -22
  44. package/src/pi/assembly/snapshot.ts +17 -9
  45. package/src/pi/message/contract.ts +7 -0
  46. package/src/pi/message/conversion.ts +9 -1
  47. package/src/pi/runtime-adapter/assembly.ts +42 -97
  48. package/src/pi/runtime-adapter/execution.ts +216 -21
  49. package/src/pi/runtime-adapter/index.ts +24 -8
  50. package/src/pi/runtime-adapter/models.ts +382 -35
  51. package/src/pi/runtime-adapter/recovery.ts +188 -1
  52. package/src/pi/runtime-adapter/transcript.ts +61 -3
  53. package/src/pi/tool/ai-adapter.ts +58 -1
  54. package/src/pi/tool/base.ts +190 -12
  55. package/src/pi/tool/compiler.ts +39 -33
  56. package/src/pi/tool/core-host.ts +28 -30
  57. package/src/pi/tool/core.ts +37 -124
  58. package/src/pi/tool/gateway.ts +54 -0
  59. package/src/pi/tool/index.ts +2 -0
  60. package/src/pi/tool/mcp.ts +98 -70
  61. package/src/pi/tool/schedule.ts +86 -20
  62. package/src/pi/tool/skill.ts +126 -420
  63. package/src/pi/tool/subagent.ts +14 -2
  64. package/src/pi/tool/web-fetch.ts +281 -0
  65. package/src/pi/tool/web-search/api.ts +34 -18
  66. package/src/pi/tool/web-search/web-search.ts +0 -1
  67. package/src/pi/tool/workspace-revision.ts +64 -0
  68. package/src/pi/tool/workspace-sandbox.ts +105 -263
  69. package/src/pi/turn/index.ts +20 -0
  70. package/src/pi/turn/interaction.ts +181 -0
  71. package/src/pi/turn/tool-recovery.ts +244 -1
  72. package/src/runtime-agent-context.ts +112 -0
  73. package/src/runtime-agent.ts +568 -321
  74. package/src/runtime-assembler.ts +797 -0
  75. package/src/runtime-definition.ts +175 -0
  76. package/src/runtime.ts +840 -208
  77. package/src/tool-registry.ts +143 -0
  78. package/src/workspace-versioning.ts +46 -0
  79. package/src/plugins.ts +0 -1033
@@ -0,0 +1,797 @@
1
+ import type { SkillSource } from "agents/skills";
2
+ import type {
3
+ RuntimeCodeExecutionPort,
4
+ RuntimeGatewayPort,
5
+ RuntimeMemoryPort,
6
+ RuntimePlatformPort,
7
+ RuntimeProviderPort,
8
+ RuntimeSandboxPort,
9
+ RuntimeSchedulePort,
10
+ RuntimeSkillSourceBinding,
11
+ RuntimeSubagentPort,
12
+ RuntimeTurnEventsPort,
13
+ RuntimeBindings,
14
+ RuntimeSkillScriptPolicy,
15
+ WorkspacePort,
16
+ } from "./kernel/bindings";
17
+ import {
18
+ freezeRuntimeProfile,
19
+ type RuntimeDenyPolicy,
20
+ type RuntimeMcpServer,
21
+ type RuntimeMemoryProfile,
22
+ type RuntimeProfile,
23
+ type ThinkingEffort,
24
+ } from "./kernel/profile";
25
+ import type { RuntimeExtensionConfig } from "./kernel/extensions";
26
+ import type { RuntimeDegradation } from "./kernel/degradation";
27
+ import {
28
+ EXECUTION_LEVELS,
29
+ type ExecutionLevel,
30
+ } from "./lib/execution-level";
31
+ import {
32
+ createPiRuntimeAssembly,
33
+ type PiToolSurface,
34
+ type PiRuntimeAssembly,
35
+ } from "./pi/assembly/snapshot";
36
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
37
+ import type { PiToolCandidate } from "./pi/tool/compiler";
38
+ import type {
39
+ AssembleRuntimeSnapshotInput,
40
+ RuntimeAgentHooks,
41
+ RuntimeSettings,
42
+ RuntimeExtensionContribution,
43
+ RuntimeProfileContribution,
44
+ RuntimeSkillContribution,
45
+ RuntimeToolSurfacePolicy,
46
+ } from "./runtime-definition";
47
+ import type { ToolRegistry } from "./tool-registry";
48
+ import { withRuntimeLoadTimeout } from "./kernel/runtime-load";
49
+ import {
50
+ basePiToolCandidates,
51
+ memoryPiToolCandidate,
52
+ } from "./pi/tool/base";
53
+ import {
54
+ browserQuickActionPiToolCandidates,
55
+ } from "./pi/tool/core";
56
+ import { skillPiToolCandidates } from "./pi/tool/skill";
57
+ import { createWebSearch } from "./pi/tool/web-search";
58
+ import { resolvePiModel } from "./pi/runtime-adapter/models";
59
+
60
+ /** Runtime Assembler:校验一份扁平输入并生成可原子提交的 Snapshot。 */
61
+
62
+ export type { RuntimeDegradation } from "./kernel/degradation";
63
+ export type {
64
+ RuntimeExtensionContribution,
65
+ RuntimeProfileContribution,
66
+ RuntimeSkillContribution,
67
+ RuntimeToolSurfacePolicy,
68
+ } from "./runtime-definition";
69
+
70
+ /** Host Planner 与 Runtime Assembler 之间的一次性内部 seam。 */
71
+ export interface RuntimeAssemblyInput {
72
+ readonly profile: RuntimeProfileContribution;
73
+ readonly memoryProfile: RuntimeMemoryProfile;
74
+ readonly toolPolicy?: RuntimeToolSurfacePolicy;
75
+ readonly enabledSubagents: readonly string[];
76
+ readonly provider: RuntimeProviderPort;
77
+ readonly platform: RuntimePlatformPort;
78
+ readonly workspace?: WorkspacePort;
79
+ readonly codeExecution?: RuntimeCodeExecutionPort;
80
+ readonly sandbox?: RuntimeSandboxPort;
81
+ readonly memory?: RuntimeMemoryPort;
82
+ readonly hostTools: readonly PiToolCandidate[];
83
+ readonly skills: readonly RuntimeSkillContribution[];
84
+ readonly connectors: readonly RuntimeMcpServer[];
85
+ readonly gateway?: RuntimeGatewayPort;
86
+ readonly extensions: readonly RuntimeExtensionContribution[];
87
+ readonly subagents?: RuntimeSubagentPort;
88
+ readonly schedule?: RuntimeSchedulePort;
89
+ readonly turnEvents?: RuntimeTurnEventsPort;
90
+ readonly commitGuards: readonly (() => Promise<void>)[];
91
+ readonly degradations: readonly RuntimeDegradation[];
92
+ }
93
+
94
+ // #region 内部候选契约
95
+
96
+ /**
97
+ * 保存一次已经完成校验、可供 Kernel 使用的 Runtime 结果。
98
+ *
99
+ * @internal
100
+ */
101
+ export interface RuntimeSnapshot {
102
+ readonly profile: RuntimeProfile;
103
+ readonly bindings: RuntimeBindings;
104
+ readonly pi: PiRuntimeAssembly;
105
+ readonly degradations: readonly RuntimeDegradation[];
106
+ }
107
+
108
+ /**
109
+ * 保存待提交的 Snapshot 和提交前检查。
110
+ *
111
+ * @internal
112
+ */
113
+ export interface RuntimeCandidate {
114
+ readonly snapshot: RuntimeSnapshot;
115
+ readonly commitGuards: readonly (() => Promise<void>)[];
116
+ readonly hooks?: RuntimeAgentHooks<Cloudflare.Env, unknown, never, never>;
117
+ readonly turnEvents?: RuntimeTurnEventsPort;
118
+ }
119
+
120
+ const DISABLED_MEMORY: RuntimeMemoryProfile = Object.freeze({
121
+ enabled: false,
122
+ memoryTokens: 2_000,
123
+ preferencesTokens: 500,
124
+ });
125
+
126
+ // 作用:把外部名称整理成非空字符串。
127
+ // 调用:Builder 在添加具名 Tool、Skill、Connector 或 Extension 时调用。
128
+ // 原因:统一在入口拒绝空名称,后续 Map 和 Set 才有稳定键值。
129
+ function requiredName(value: string, label: string): string {
130
+ const name = value.trim();
131
+ if (!name) throw new Error(`${label} name must not be empty`);
132
+ return name;
133
+ }
134
+
135
+ // 作用:检查内存预算都是可用的非负数字。
136
+ // 调用:Builder 在生成候选 Snapshot 前调用。
137
+ // 原因:把检查放在提交前,失败时旧 Snapshot 仍保持不变。
138
+ function assertMemoryProfile(profile: RuntimeMemoryProfile): void {
139
+ for (const [name, value] of Object.entries(profile)) {
140
+ if (name === "enabled") continue;
141
+ if (
142
+ typeof value !== "number" ||
143
+ !Number.isFinite(value) ||
144
+ value < 0
145
+ ) {
146
+ throw new Error(`Runtime memory ${name} must be a non-negative number`);
147
+ }
148
+ }
149
+ }
150
+
151
+ interface ToolSurfaceInput {
152
+ readonly platform: RuntimePlatformPort;
153
+ readonly workspace?: WorkspacePort;
154
+ readonly memory?: {
155
+ readonly port: RuntimeMemoryPort;
156
+ readonly profile: RuntimeMemoryProfile;
157
+ };
158
+ readonly hostTools: readonly PiToolCandidate[];
159
+ readonly skills: readonly RuntimeSkillSourceBinding[];
160
+ readonly enabledSubagents: readonly string[];
161
+ readonly webSearch: Parameters<typeof basePiToolCandidates>[0];
162
+ readonly extensions: readonly RuntimeExtensionConfig[];
163
+ readonly policy?: RuntimeToolSurfacePolicy;
164
+ }
165
+
166
+ interface ToolSurface {
167
+ readonly surface: PiToolSurface;
168
+ readonly extensions: readonly RuntimeExtensionConfig[];
169
+ readonly degradations: readonly RuntimeDegradation[];
170
+ }
171
+
172
+ // 唯一 Tool Surface seam:只读取已配置 Port、Resource 和 Agent policy,
173
+ // 统一生成可见 Tool,并在一处做名称、执行档位与冲突校验。
174
+ async function createToolSurface(
175
+ input: ToolSurfaceInput,
176
+ ): Promise<ToolSurface> {
177
+ const deny = new Set(input.policy?.denyPolicy?.deny ?? []);
178
+ const allowsTool = input.policy?.allowsTool;
179
+ const allowsExtension = input.policy?.allowsExtension;
180
+ const visible = (candidate: PiToolCandidate) =>
181
+ !deny.has(candidate.tool.name) &&
182
+ allowsTool?.(candidate.tool.name) !== false;
183
+ const browserCandidates = input.platform.browser
184
+ ? browserQuickActionPiToolCandidates(input.platform.browser)
185
+ : [];
186
+ const baseCandidates = basePiToolCandidates(input.webSearch);
187
+ const memoryCandidates = input.memory
188
+ ? [memoryPiToolCandidate(input.memory.port, input.memory.profile)]
189
+ : [];
190
+ const scriptCandidates = [
191
+ ...browserCandidates,
192
+ ...input.hostTools,
193
+ ...baseCandidates,
194
+ ...memoryCandidates,
195
+ ].filter(visible);
196
+ const staticCandidates = [
197
+ ...browserCandidates,
198
+ ...input.hostTools,
199
+ ...(await skillPiToolCandidates(input.skills, {
200
+ loader: input.platform.loader,
201
+ ...(input.workspace ? { workspace: input.workspace } : {}),
202
+ tools: scriptCandidates,
203
+ })),
204
+ ...baseCandidates,
205
+ ...memoryCandidates,
206
+ ];
207
+
208
+ return {
209
+ surface: Object.freeze({
210
+ finalize(candidates: readonly PiToolCandidate[]) {
211
+ const tools = new Map<string, PiToolCandidate>();
212
+
213
+ for (const candidate of [
214
+ ...staticCandidates,
215
+ ...candidates,
216
+ ]) {
217
+ const name = requiredName(candidate.tool.name, "Pi Tool");
218
+ if (!visible(candidate)) continue;
219
+ if (!EXECUTION_LEVELS.includes(candidate.requiredExecutionLevel)) {
220
+ throw new Error(`Pi Tool ${name} execution level is invalid`);
221
+ }
222
+ if (tools.has(name)) {
223
+ throw new Error(`Duplicate Runtime Pi Tool: ${name}`);
224
+ }
225
+ tools.set(name, Object.freeze({ ...candidate }));
226
+ }
227
+
228
+ return Object.freeze([...tools.values()]);
229
+ },
230
+ }),
231
+ extensions: input.extensions.filter(
232
+ (extension) => allowsExtension?.(extension) !== false,
233
+ ),
234
+ degradations: [],
235
+ };
236
+ }
237
+
238
+ // #endregion
239
+
240
+ // #region RuntimeBuilder 合并
241
+
242
+ class RuntimeBuilder {
243
+ private profile?: RuntimeProfileContribution;
244
+ private provider?: RuntimeProviderPort;
245
+ private platform?: RuntimePlatformPort;
246
+ private gateway?: RuntimeGatewayPort;
247
+ private workspace?: WorkspacePort;
248
+ private codeExecution?: RuntimeCodeExecutionPort;
249
+ private sandbox?: RuntimeSandboxPort;
250
+ private schedule?: RuntimeSchedulePort;
251
+ private memoryProfile: RuntimeMemoryProfile = DISABLED_MEMORY;
252
+ private memory?: RuntimeMemoryPort;
253
+ private readonly hostTools: PiToolCandidate[] = [];
254
+ private toolPolicy?: RuntimeToolSurfacePolicy;
255
+ private readonly skillSources = new Map<
256
+ string,
257
+ {
258
+ name: string;
259
+ description: string;
260
+ source: SkillSource;
261
+ script: RuntimeSkillScriptPolicy;
262
+ }
263
+ >();
264
+ private readonly connectors = new Map<string, RuntimeMcpServer>();
265
+ private readonly extensions = new Map<string, RuntimeExtensionConfig>();
266
+ private readonly enabledSubagents = new Set<string>();
267
+ private subagents?: RuntimeSubagentPort;
268
+ private turnEvents?: RuntimeTurnEventsPort;
269
+ private readonly commitGuards: Array<() => Promise<void>> = [];
270
+ private readonly degradations: RuntimeDegradation[] = [];
271
+
272
+ merge(input: RuntimeAssemblyInput): void {
273
+ for (const degradation of input.degradations) {
274
+ this.reportDegradation(degradation);
275
+ }
276
+ if (!EXECUTION_LEVELS.includes(input.profile.executionLevel)) {
277
+ throw new Error("Runtime execution level is invalid");
278
+ }
279
+ this.profile = input.profile;
280
+ this.provider = input.provider;
281
+ this.platform = input.platform;
282
+ this.workspace = input.workspace;
283
+ this.codeExecution = input.codeExecution;
284
+ this.sandbox = input.sandbox;
285
+ this.memoryProfile = input.memoryProfile;
286
+ this.memory = input.memory;
287
+ this.hostTools.push(...input.hostTools);
288
+ this.toolPolicy = input.toolPolicy;
289
+ for (const skill of input.skills) this.addSkill(skill);
290
+ for (const connector of input.connectors) this.addConnector(connector);
291
+ this.gateway = input.gateway;
292
+ for (const { name, extension } of input.extensions) {
293
+ this.addExtension(name, extension);
294
+ }
295
+ this.schedule = input.schedule;
296
+ for (const name of input.enabledSubagents) {
297
+ this.enabledSubagents.add(requiredName(name, "Subagent"));
298
+ }
299
+ this.subagents = input.subagents;
300
+ this.turnEvents = input.turnEvents;
301
+ this.commitGuards.push(...input.commitGuards);
302
+ }
303
+
304
+ private addSkill(skill: RuntimeSkillContribution): void {
305
+ const normalized = requiredName(skill.name, "Skill");
306
+ if (this.skillSources.has(normalized)) {
307
+ throw new Error(`Duplicate Runtime Skill: ${normalized}`);
308
+ }
309
+ const normalizedDescription = requiredName(
310
+ skill.description,
311
+ `Runtime Skill "${normalized}" description`,
312
+ );
313
+ const script = skill.script ?? {};
314
+ const network = script.network ?? "none";
315
+ if (network !== "none" && network !== "full") {
316
+ throw new Error(
317
+ `Runtime Skill "${normalized}" has invalid network permission`,
318
+ );
319
+ }
320
+ const workspace = script.workspace ?? "none";
321
+ if (
322
+ workspace !== "none" &&
323
+ workspace !== "read" &&
324
+ workspace !== "read-write"
325
+ ) {
326
+ throw new Error(
327
+ `Runtime Skill "${normalized}" has invalid workspace permission`,
328
+ );
329
+ }
330
+ const tools = [
331
+ ...new Set(
332
+ (script.tools ?? []).map((tool) =>
333
+ requiredName(tool, `Runtime Skill "${normalized}" Tool`),
334
+ ),
335
+ ),
336
+ ];
337
+ this.skillSources.set(normalized, Object.freeze({
338
+ name: normalized,
339
+ description: normalizedDescription,
340
+ source: skill.source,
341
+ script: Object.freeze({
342
+ network,
343
+ workspace,
344
+ tools: Object.freeze(tools),
345
+ }),
346
+ }));
347
+ }
348
+
349
+ private addConnector(server: RuntimeMcpServer): void {
350
+ const name = requiredName(server.name, "Connector");
351
+ if (this.connectors.has(name)) {
352
+ throw new Error(`Duplicate Runtime Connector: ${name}`);
353
+ }
354
+ if (!server.url.trim()) {
355
+ throw new Error(`Runtime Connector "${name}" URL must not be empty`);
356
+ }
357
+ this.connectors.set(name, Object.freeze({
358
+ name,
359
+ url: server.url.trim(),
360
+ }));
361
+ }
362
+
363
+ private addExtension(
364
+ name: string,
365
+ extension: RuntimeExtensionConfig,
366
+ ): void {
367
+ const normalized = requiredName(name, "Extension");
368
+ if (this.extensions.has(normalized)) {
369
+ throw new Error(`Duplicate Runtime Extension: ${normalized}`);
370
+ }
371
+ if (extension.manifest.name !== normalized) {
372
+ throw new Error(
373
+ `Runtime Extension name mismatch: expected "${normalized}", got "${extension.manifest.name}"`,
374
+ );
375
+ }
376
+ this.extensions.set(normalized, extension);
377
+ }
378
+
379
+ private reportDegradation(degradation: RuntimeDegradation): void {
380
+ this.degradations.push(Object.freeze({ ...degradation }));
381
+ }
382
+
383
+ // #endregion
384
+
385
+ // #region RuntimeBuilder 校验与候选生成
386
+
387
+ // 作用:校验全部声明,并生成一个不可变候选 Runtime。
388
+ // 调用:扁平 Assembly Input 合并完成后调用一次。
389
+ // 原因:所有交叉约束都在这里通过后才返回候选,旧 Snapshot 不会半更新。
390
+ async build(): Promise<RuntimeCandidate> {
391
+ if (!this.profile) {
392
+ throw new Error("Runtime assembly did not configure a profile");
393
+ }
394
+ if (!this.platform) {
395
+ throw new Error("Runtime assembly did not bind a platform");
396
+ }
397
+ if (!this.provider) {
398
+ throw new Error("Runtime assembly did not bind a provider");
399
+ }
400
+ if (!this.profile.model.trim()) {
401
+ throw new Error("Runtime model must not be empty");
402
+ }
403
+ if (
404
+ !this.provider.defaultModel.trim() ||
405
+ this.provider.endpoints.length === 0 ||
406
+ !this.provider.endpoints.some((endpoint) =>
407
+ endpoint.models.includes(this.provider!.defaultModel)
408
+ ) ||
409
+ this.provider.endpoints.some((endpoint) =>
410
+ !endpoint.apiKey.trim() ||
411
+ !endpoint.baseURL.trim() ||
412
+ endpoint.models.length === 0
413
+ )
414
+ ) {
415
+ throw new Error("No usable Provider credential is configured");
416
+ }
417
+ if (!this.platform.loader) {
418
+ throw new Error("Runtime platform loader is required");
419
+ }
420
+ if (typeof this.platform.outbound !== "function") {
421
+ throw new Error("Runtime platform outbound port is required");
422
+ }
423
+
424
+ assertMemoryProfile(this.memoryProfile);
425
+ if (this.memoryProfile.enabled && (!this.workspace || !this.memory)) {
426
+ throw new Error(
427
+ "Enabled Runtime memory requires Workspace and Memory bindings",
428
+ );
429
+ }
430
+ const profile = freezeRuntimeProfile({
431
+ model: this.profile.model.trim(),
432
+ thinking: this.profile.thinking,
433
+ systemPrompt: this.profile.systemPrompt,
434
+ denyPolicy: this.toolPolicy?.denyPolicy,
435
+ enabledSubagents: [...this.enabledSubagents],
436
+ mcpServers: [...this.connectors.values()],
437
+ executionLevel: this.profile.executionLevel,
438
+ memory: this.memoryProfile,
439
+ });
440
+ const provider: RuntimeProviderPort = Object.freeze({
441
+ defaultModel: this.provider.defaultModel,
442
+ endpoints: Object.freeze(this.provider.endpoints.map(
443
+ (endpoint) => Object.freeze({
444
+ ...endpoint,
445
+ ...(endpoint.headers
446
+ ? { headers: Object.freeze({ ...endpoint.headers }) }
447
+ : {}),
448
+ models: Object.freeze([...endpoint.models]),
449
+ }),
450
+ )),
451
+ });
452
+ const endpoints = provider.endpoints.filter((endpoint) =>
453
+ endpoint.models.includes(profile.model)
454
+ );
455
+ if (endpoints.length !== 1) {
456
+ throw new Error(
457
+ endpoints.length === 0
458
+ ? `Model is not configured by this deployment: ${profile.model}`
459
+ : `Model is configured by multiple endpoints: ${profile.model}`,
460
+ );
461
+ }
462
+ const resolvedModel = resolvePiModel(provider, profile.model);
463
+ const endpoint = endpoints[0]!;
464
+ const webSearch = createWebSearch({
465
+ endpoint,
466
+ model: profile.model,
467
+ maxTokens: resolvedModel.maxTokens,
468
+ reasoning: resolvedModel.reasoning,
469
+ });
470
+ const skillSources = [...this.skillSources.values()];
471
+ const toolSurface = await createToolSurface({
472
+ platform: this.platform,
473
+ ...(this.workspace ? { workspace: this.workspace } : {}),
474
+ ...(this.memoryProfile.enabled && this.memory
475
+ ? { memory: { port: this.memory, profile: this.memoryProfile } }
476
+ : {}),
477
+ hostTools: this.hostTools,
478
+ skills: skillSources,
479
+ enabledSubagents: [...this.enabledSubagents],
480
+ webSearch,
481
+ extensions: [...this.extensions.values()],
482
+ ...(this.toolPolicy ? { policy: this.toolPolicy } : {}),
483
+ });
484
+ for (const degradation of toolSurface.degradations) {
485
+ this.reportDegradation(degradation);
486
+ }
487
+ const bindings: RuntimeBindings = Object.freeze({
488
+ provider,
489
+ platform: Object.freeze({ ...this.platform }),
490
+ ...(this.gateway ? { gateway: this.gateway } : {}),
491
+ ...(this.workspace ? { workspace: this.workspace } : {}),
492
+ ...(this.memoryProfile.enabled && this.memory
493
+ ? { memory: this.memory }
494
+ : {}),
495
+ skills: Object.freeze({
496
+ sources: Object.freeze(skillSources),
497
+ }),
498
+ ...(this.turnEvents ? { turnEvents: this.turnEvents } : {}),
499
+ });
500
+ const snapshot: RuntimeSnapshot = Object.freeze({
501
+ profile,
502
+ bindings,
503
+ pi: createPiRuntimeAssembly({
504
+ profile,
505
+ provider,
506
+ toolSurface: toolSurface.surface,
507
+ mcpServers: profile.mcpServers,
508
+ extensions: toolSurface.extensions,
509
+ }),
510
+ degradations: Object.freeze([
511
+ ...this.degradations,
512
+ ]),
513
+ });
514
+
515
+ return Object.freeze({
516
+ snapshot,
517
+ commitGuards: Object.freeze([
518
+ ...this.commitGuards,
519
+ ]),
520
+ });
521
+ }
522
+
523
+ }
524
+
525
+ /**
526
+ * 在不改动当前 Runtime 的前提下生成一个隔离候选。
527
+ *
528
+ * @remarks
529
+ * `AgentRuntimeKernel` 在初始化或重载时调用它。
530
+ *
531
+ * 任何合并、校验或 Tool Surface 构建失败都不会产生 commit。
532
+ *
533
+ * 该函数有意不从包入口导出,提交生命周期只能由 Kernel 控制。
534
+ *
535
+ * @internal
536
+ */
537
+ export async function prepareRuntimeCandidate(
538
+ input: RuntimeAssemblyInput,
539
+ ): Promise<RuntimeCandidate> {
540
+ const builder = new RuntimeBuilder();
541
+ builder.merge(input);
542
+ return builder.build();
543
+ }
544
+
545
+ /**
546
+ * 完成候选装配、提交前检查,并在最后一步安装 Snapshot。
547
+ *
548
+ * @remarks
549
+ * `AgentRuntimeKernel.initConfig` 在首次加载或显式重载时调用它。
550
+ *
551
+ * `commit` 只在全部准备、合并、校验和 guard 成功后调用一次。
552
+ *
553
+ * @internal
554
+ */
555
+ export async function assembleRuntime(
556
+ input: RuntimeAssemblyInput,
557
+ commit: (snapshot: RuntimeSnapshot) => void,
558
+ ): Promise<void> {
559
+ const candidate = await prepareRuntimeCandidate(input);
560
+ for (const guard of candidate.commitGuards) {
561
+ await guard();
562
+ }
563
+ commit(candidate.snapshot);
564
+ }
565
+
566
+ export function toolRegistryPiToolCandidates(
567
+ registry: ToolRegistry,
568
+ settings?: Pick<RuntimeSettings, "toolPolicy">,
569
+ ): PiToolCandidate[] {
570
+ const deny = new Set(settings?.toolPolicy?.denyPolicy?.deny ?? []);
571
+ const allowsTool = settings?.toolPolicy?.allowsTool;
572
+ const candidates: PiToolCandidate[] = [];
573
+ for (const [name, spec] of Object.entries(registry)) {
574
+ if (deny.has(name)) continue;
575
+ if (allowsTool?.(name) === false) continue;
576
+ const normalized = requiredName(name, "Platform Tool");
577
+ candidates.push(Object.freeze({
578
+ owner: "platform",
579
+ requiredExecutionLevel: spec.requiredExecutionLevel,
580
+ ...(spec.alwaysRequiresApproval
581
+ ? { alwaysRequiresApproval: true }
582
+ : {}),
583
+ tool: Object.freeze({
584
+ name: normalized,
585
+ label: spec.label,
586
+ description: spec.description,
587
+ parameters: spec.parameters,
588
+ async execute(
589
+ toolCallId: string,
590
+ input: unknown,
591
+ signal?: AbortSignal,
592
+ ) {
593
+ const result = await spec.execute(input, {
594
+ toolCallId,
595
+ signal: signal ?? new AbortController().signal,
596
+ });
597
+ if (
598
+ result != null &&
599
+ typeof result === "object" &&
600
+ "content" in result &&
601
+ Array.isArray((result as { content?: unknown }).content)
602
+ ) {
603
+ return result as import("@earendil-works/pi-agent-core").AgentToolResult<unknown>;
604
+ }
605
+ let text: string;
606
+ try {
607
+ text = JSON.stringify(result) ?? String(result);
608
+ } catch {
609
+ text = String(result);
610
+ }
611
+ return {
612
+ content: [{ type: "text" as const, text }],
613
+ details: result,
614
+ };
615
+ },
616
+ } as AgentTool<any, any>),
617
+ }));
618
+ }
619
+ return candidates;
620
+ }
621
+
622
+ function hooksToTurnEventsPort<
623
+ Env extends Cloudflare.Env,
624
+ Config,
625
+ Command,
626
+ Change,
627
+ >(
628
+ ctx: AssembleRuntimeSnapshotInput<Env, Config, Command, Change>["ctx"],
629
+ hooks?: RuntimeAgentHooks<Env, Config, Command, Change>,
630
+ ): RuntimeTurnEventsPort | undefined {
631
+ if (!hooks) return undefined;
632
+ if (
633
+ !hooks.onTurnEnd &&
634
+ !hooks.onModelUsage &&
635
+ !hooks.onToolStart &&
636
+ !hooks.onToolSettled &&
637
+ !hooks.onApproval &&
638
+ !hooks.onActivityChanged &&
639
+ !hooks.onSubmissionTerminal
640
+ ) {
641
+ return undefined;
642
+ }
643
+ return {
644
+ onResponse: hooks.onTurnEnd
645
+ ? (messages) => hooks.onTurnEnd!(ctx, messages)
646
+ : async () => undefined,
647
+ ...(hooks.onModelUsage ? { onModelUsage: hooks.onModelUsage } : {}),
648
+ ...(hooks.onToolStart ? { onToolStart: hooks.onToolStart } : {}),
649
+ ...(hooks.onToolSettled ? { onToolSettled: hooks.onToolSettled } : {}),
650
+ ...(hooks.onApproval ? { onApproval: hooks.onApproval } : {}),
651
+ ...(hooks.onActivityChanged
652
+ ? { onActivityChanged: hooks.onActivityChanged }
653
+ : {}),
654
+ ...(hooks.onSubmissionTerminal
655
+ ? { onSubmissionTerminal: hooks.onSubmissionTerminal }
656
+ : {}),
657
+ };
658
+ }
659
+
660
+ /**
661
+ * 从 Config、Tool 和已加载 Resource 装配 Runtime 候选 Snapshot。
662
+ *
663
+ * @internal
664
+ */
665
+ export async function assembleRuntimeSnapshot<
666
+ Env extends Cloudflare.Env,
667
+ Config,
668
+ Command = never,
669
+ Change = never,
670
+ >(
671
+ input: AssembleRuntimeSnapshotInput<Env, Config, Command, Change>,
672
+ ): Promise<RuntimeCandidate> {
673
+ const resources = input.resources;
674
+ const toolAssembly = input.tools;
675
+ const settings = resources.settings;
676
+ const provider = resources.provider;
677
+ let profile = settings.profile;
678
+ if (input.hooks?.profileOverrides) {
679
+ const overrides = await withRuntimeLoadTimeout(
680
+ "profile-overrides",
681
+ () => input.hooks!.profileOverrides!(
682
+ input.ctx,
683
+ input.config,
684
+ ),
685
+ );
686
+ if (overrides.systemPrompt !== undefined) {
687
+ profile = { ...profile, systemPrompt: overrides.systemPrompt };
688
+ }
689
+ if (overrides.executionLevel !== undefined) {
690
+ profile = { ...profile, executionLevel: overrides.executionLevel };
691
+ }
692
+ }
693
+
694
+ const hostTools = toolRegistryPiToolCandidates(
695
+ toolAssembly.tools,
696
+ settings,
697
+ );
698
+
699
+ const commitGuards: Array<() => Promise<void>> = [];
700
+ if (toolAssembly.commitGuard) commitGuards.push(toolAssembly.commitGuard);
701
+ if (input.hooks?.beforeCommit) {
702
+ for (const guard of input.hooks.beforeCommit) {
703
+ commitGuards.push(() => guard(input.ctx, input.config));
704
+ }
705
+ }
706
+
707
+ const platform: RuntimePlatformPort = input.hooks?.gateTool
708
+ ? Object.freeze({
709
+ ...resources.platform,
710
+ gateTool: (request: {
711
+ toolCallId: string;
712
+ toolName: string;
713
+ input: unknown;
714
+ requiredExecutionLevel: import("./lib/execution-level").ExecutionLevel;
715
+ signal: AbortSignal;
716
+ }) => input.hooks!.gateTool!(request),
717
+ })
718
+ : resources.platform;
719
+
720
+ const memoryProfile =
721
+ toolAssembly.memoryProfile ?? settings.memoryProfile;
722
+ const enabledSubagents =
723
+ toolAssembly.enabledSubagents ?? settings.enabledSubagents;
724
+ const turnEvents = hooksToTurnEventsPort(input.ctx, input.hooks);
725
+
726
+ const toolPolicy: RuntimeToolSurfacePolicy | undefined =
727
+ settings.toolPolicy || toolAssembly.surfacePolicy
728
+ ? {
729
+ ...(settings.toolPolicy?.denyPolicy
730
+ ? { denyPolicy: settings.toolPolicy.denyPolicy }
731
+ : {}),
732
+ ...(settings.toolPolicy?.allowsTool
733
+ ? { allowsTool: settings.toolPolicy.allowsTool }
734
+ : {}),
735
+ ...(settings.toolPolicy?.allowsExtension
736
+ ? { allowsExtension: settings.toolPolicy.allowsExtension }
737
+ : {}),
738
+ ...(toolAssembly.surfacePolicy?.allowsTool
739
+ ? { allowsTool: toolAssembly.surfacePolicy.allowsTool }
740
+ : {}),
741
+ ...(toolAssembly.surfacePolicy?.allowsExtension
742
+ ? { allowsExtension: toolAssembly.surfacePolicy.allowsExtension }
743
+ : {}),
744
+ }
745
+ : undefined;
746
+
747
+ const assemblyInput: RuntimeAssemblyInput = {
748
+ profile,
749
+ memoryProfile,
750
+ ...(toolPolicy ? { toolPolicy } : {}),
751
+ enabledSubagents: [...enabledSubagents],
752
+ provider,
753
+ platform,
754
+ ...(toolAssembly.bindings?.workspace
755
+ ? { workspace: toolAssembly.bindings.workspace }
756
+ : {}),
757
+ ...(memoryProfile.enabled && toolAssembly.bindings?.memory
758
+ ? { memory: toolAssembly.bindings.memory }
759
+ : {}),
760
+ hostTools,
761
+ skills: resources.skills,
762
+ connectors: resources.connectors.servers,
763
+ ...(resources.connectors.gateway
764
+ ? { gateway: resources.connectors.gateway }
765
+ : {}),
766
+ extensions: resources.extensions,
767
+ ...(turnEvents ? { turnEvents } : {}),
768
+ commitGuards: [],
769
+ degradations: [
770
+ ...(toolAssembly.degradations ?? []),
771
+ ...(resources.degradations ?? []),
772
+ ...resources.connectors.degradations,
773
+ ],
774
+ };
775
+
776
+ const candidate = await prepareRuntimeCandidate(assemblyInput);
777
+ return Object.freeze({
778
+ snapshot: candidate.snapshot,
779
+ commitGuards: Object.freeze([
780
+ ...candidate.commitGuards,
781
+ ...commitGuards,
782
+ ]),
783
+ ...(input.hooks
784
+ ? {
785
+ hooks: input.hooks as unknown as RuntimeAgentHooks<
786
+ Cloudflare.Env,
787
+ unknown,
788
+ never,
789
+ never
790
+ >,
791
+ }
792
+ : {}),
793
+ ...(turnEvents ? { turnEvents } : {}),
794
+ });
795
+ }
796
+
797
+ // #endregion