@springbrand/agent-runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/package.json +28 -0
  2. package/src/db/approval.repo.ts +291 -0
  3. package/src/db/ext-context.repo.ts +34 -0
  4. package/src/db/index.ts +83 -0
  5. package/src/db/message-ui.repo.ts +39 -0
  6. package/src/db/milestone.repo.ts +96 -0
  7. package/src/db/runtime-event-outbox.repo.ts +89 -0
  8. package/src/db/schema.ts +164 -0
  9. package/src/db/settlement.repo.ts +104 -0
  10. package/src/db/steer.repo.ts +73 -0
  11. package/src/db/submission.repo.ts +323 -0
  12. package/src/index.ts +133 -0
  13. package/src/kernel/approval-lifecycle.ts +552 -0
  14. package/src/kernel/bindings.ts +898 -0
  15. package/src/kernel/degradation.ts +15 -0
  16. package/src/kernel/extensions.ts +108 -0
  17. package/src/kernel/profile.ts +116 -0
  18. package/src/kernel/public-contracts.ts +17 -0
  19. package/src/kernel/receipts.ts +124 -0
  20. package/src/kernel/recoverable-chat-agent.ts +899 -0
  21. package/src/kernel/state.ts +76 -0
  22. package/src/kernel/submission-lifecycle.ts +600 -0
  23. package/src/layers/context/budget/gate.ts +88 -0
  24. package/src/layers/orchestration/subagents/agent-types/contract.ts +78 -0
  25. package/src/layers/orchestration/subagents/agent-types/extract/index.ts +47 -0
  26. package/src/layers/orchestration/subagents/agent-types/fanout/index.ts +53 -0
  27. package/src/layers/orchestration/subagents/agent-types/registry.ts +16 -0
  28. package/src/layers/orchestration/temporary-agent/core.ts +152 -0
  29. package/src/layers/orchestration/temporary-agent/runner.ts +133 -0
  30. package/src/layers/orchestration/temporary-agent/workspace.ts +154 -0
  31. package/src/lib/artifacts.ts +54 -0
  32. package/src/lib/egress.ts +44 -0
  33. package/src/lib/execution-level.ts +27 -0
  34. package/src/lib/extension-name.ts +18 -0
  35. package/src/lib/host-actions.ts +57 -0
  36. package/src/lib/mcp.ts +86 -0
  37. package/src/lib/model-catalog.ts +7 -0
  38. package/src/lib/prompt.ts +139 -0
  39. package/src/lib/telemetry-dev.ts +44 -0
  40. package/src/pi/assembly/context.ts +510 -0
  41. package/src/pi/assembly/extensions.ts +661 -0
  42. package/src/pi/assembly/index.ts +19 -0
  43. package/src/pi/assembly/snapshot.ts +200 -0
  44. package/src/pi/message/contract.ts +8 -0
  45. package/src/pi/message/conversion.ts +73 -0
  46. package/src/pi/message/index.ts +3 -0
  47. package/src/pi/message/projection.ts +604 -0
  48. package/src/pi/runtime-adapter/assembly.ts +552 -0
  49. package/src/pi/runtime-adapter/execution.ts +683 -0
  50. package/src/pi/runtime-adapter/index.ts +232 -0
  51. package/src/pi/runtime-adapter/models.ts +243 -0
  52. package/src/pi/runtime-adapter/recovery.ts +805 -0
  53. package/src/pi/runtime-adapter/transcript.ts +825 -0
  54. package/src/pi/session/index.ts +24 -0
  55. package/src/pi/session/storage.ts +353 -0
  56. package/src/pi/tool/ai-adapter.ts +100 -0
  57. package/src/pi/tool/base.ts +110 -0
  58. package/src/pi/tool/compiler.ts +444 -0
  59. package/src/pi/tool/core-host.ts +48 -0
  60. package/src/pi/tool/core.ts +251 -0
  61. package/src/pi/tool/index.ts +32 -0
  62. package/src/pi/tool/mcp.ts +319 -0
  63. package/src/pi/tool/schedule.ts +198 -0
  64. package/src/pi/tool/skill.ts +455 -0
  65. package/src/pi/tool/subagent.ts +148 -0
  66. package/src/pi/tool/web-search/api.ts +1292 -0
  67. package/src/pi/tool/web-search/index.ts +2 -0
  68. package/src/pi/tool/web-search/web-search.ts +127 -0
  69. package/src/pi/tool/workspace-sandbox.ts +664 -0
  70. package/src/pi/turn/approval.ts +181 -0
  71. package/src/pi/turn/index.ts +62 -0
  72. package/src/pi/turn/tool-recovery.ts +792 -0
  73. package/src/plugins.ts +1024 -0
  74. package/src/runtime-agent.ts +654 -0
  75. package/src/runtime.ts +2880 -0
@@ -0,0 +1,552 @@
1
+ import type {
2
+ RuntimeBindings,
3
+ RuntimePlatformPort,
4
+ } from "../../kernel/bindings";
5
+ import type { RuntimeDegradation } from "../../kernel/degradation";
6
+ import type {
7
+ RuntimeExtensionPermissions,
8
+ } from "../../kernel/extensions";
9
+ import type { RuntimeProfile } from "../../kernel/profile";
10
+ import type { ExecutionLevel } from "../../lib/execution-level";
11
+ import type { RuntimeSnapshot } from "../../plugins";
12
+ import type { PiRuntimeAssembly } from "../assembly";
13
+ import {
14
+ assemblePiSystemContext,
15
+ assemblePiExtensions,
16
+ loadPiExtension,
17
+ type PiExtensionAssembly,
18
+ type PiExtensionContextContribution,
19
+ type PiLoadedExtension,
20
+ } from "../assembly";
21
+ import {
22
+ compilePiTools,
23
+ createPiMcpToolCandidates,
24
+ listExtensionsPiToolCandidate,
25
+ type PiMcpHost,
26
+ type PiToolCandidate,
27
+ } from "../tool";
28
+
29
+ /**
30
+ * 本文件负责准备 Pi 的扩展与工具,并为每次提交固定可恢复的运行描述。
31
+ *
32
+ * @remarks
33
+ * 核心术语见本目录入口 `./index.ts`,这里不重复定义。
34
+ */
35
+
36
+ // #region 装配契约
37
+
38
+ // 只保留加载扩展和收集工具所需的快照字段。
39
+ // preparePiAssembly 在 Runtime 配置阶段接收这个收窄后的视图。
40
+ // 边界保持精简,避免装配代码依赖无关的运行状态。
41
+ interface PiAssemblySnapshot {
42
+ readonly pi: Pick<
43
+ PiRuntimeAssembly,
44
+ "extensions" | "toolCandidates"
45
+ >;
46
+ readonly profile: Pick<
47
+ RuntimeProfile,
48
+ "denyPolicy" | "enabledExtensions" | "mcpServers"
49
+ >;
50
+ readonly bindings: {
51
+ readonly platform: Pick<RuntimePlatformPort, "loader">;
52
+ readonly workspace?: RuntimeBindings["workspace"];
53
+ };
54
+ }
55
+
56
+ // 汇集装配扩展和 MCP 工具所需的输入。
57
+ // preparePiRuntime 在配置期间把这些值交给 preparePiAssembly。
58
+ // Host 绑定由 Worker 持有,所以创建函数不能下沉到本模块。
59
+ interface PreparePiAssemblyOptions {
60
+ readonly snapshot: PiAssemblySnapshot;
61
+ readonly mcpHost: PiMcpHost;
62
+ // 为一个扩展生成带权限参数的 Host Fetcher。
63
+ // loadPiExtension 在扩展需要 Host 能力时调用它。
64
+ // Worker 掌握真实绑定,装配层只保存这个窄函数,不能自行扩大权限。
65
+ readonly createExtensionHostBinding: (
66
+ permissions: RuntimeExtensionPermissions,
67
+ ownContextLabels: readonly string[],
68
+ ) => Fetcher;
69
+ }
70
+
71
+ // 暴露后续准备和固定阶段会读取的扩展装配结果。
72
+ // preparePiRuntime 读取工具与降级,pinPiRuntime 读取上下文贡献。
73
+ // 访问器直接代理扩展装配,避免维护会漂移的副本。
74
+ interface PreparedPiAssembly {
75
+ readonly loaded: readonly PiLoadedExtension[];
76
+ readonly context: readonly PiExtensionContextContribution[];
77
+ readonly degradations: PiExtensionAssembly["degradations"];
78
+ // 生成当前可供 Pi 编译的完整工具候选列表。
79
+ // preparePiAssembly 用它预检,preparePiRuntime 再读取并冻结最终列表。
80
+ // MCP 就绪状态由 Host 提供,因此这里不能只拼接初始化时的静态数组。
81
+ toolCandidates(): PiToolCandidate[];
82
+ }
83
+
84
+ /**
85
+ * 提供准备 Pi 所需的已校验 Runtime 快照和 Host 端口。
86
+ *
87
+ * @remarks
88
+ * `PiRuntimeAdapter.prepare` 在加载 Runtime 配置时传入它。
89
+ *
90
+ * 扩展绑定仍由 Host 创建,因为 Cloudflare 绑定属于 Worker 的能力边界。
91
+ *
92
+ * @see https://developers.cloudflare.com/workers/runtime-apis/bindings/
93
+ */
94
+ export interface PreparePiRuntimeOptions {
95
+ readonly snapshot: RuntimeSnapshot;
96
+ readonly mcpHost: PiMcpHost;
97
+ /**
98
+ * 为一个扩展创建受限的 Host 绑定。
99
+ *
100
+ * @remarks
101
+ * Runtime 配置只会在加载需要 Host 能力的已启用扩展时调用它。
102
+ *
103
+ * 权限和自有上下文标签必须显式传入,Host 才能执行同一条授权边界。
104
+ */
105
+ readonly createExtensionHostBinding: (
106
+ permissions: RuntimeExtensionPermissions,
107
+ ownContextLabels: readonly string[],
108
+ ) => Fetcher;
109
+ }
110
+
111
+ /**
112
+ * 表示已经准备好、但尚未固定到某次提交的 Pi 配置。
113
+ *
114
+ * @remarks
115
+ * Runtime 在配置完成后保存这个句柄,并在提交准入或创建 Turn 时传回。
116
+ *
117
+ * 公开形状只给出版本描述和加载降级,调用方不会直接拿到可变的装配状态。
118
+ */
119
+ export interface PreparedPiRuntime {
120
+ readonly revisionDescriptor: string;
121
+ readonly degradations: readonly RuntimeDegradation[];
122
+ }
123
+
124
+ /**
125
+ * 提供把一次提交固定到当前 Runtime 所需的值。
126
+ *
127
+ * @remarks
128
+ * Runtime 在写入提交前的准入阶段把它交给 `PiRuntimeAdapter.pin`。
129
+ *
130
+ * 扩展没有可读上下文时不需要读取函数,所以该端口保持可选。
131
+ */
132
+ export interface PinPiRuntimeOptions {
133
+ readonly prepared: PreparedPiRuntime;
134
+ readonly baseRevision: string;
135
+ /**
136
+ * 读取一个扩展要写入固定系统提示词的上下文。
137
+ *
138
+ * @remarks
139
+ * pinPiRuntime 在准入阶段为已加载扩展声明的上下文贡献调用它。
140
+ *
141
+ * 本模块不持有扩展上下文存储,所以读取端口必须留在 Host。
142
+ */
143
+ readonly readExtensionContext?: (
144
+ contribution: PiExtensionContextContribution,
145
+ ) => Promise<string | null>;
146
+ }
147
+
148
+ /**
149
+ * 返回已经固定到一次提交的描述和上下文降级。
150
+ *
151
+ * @remarks
152
+ * Runtime 在准入时持久化描述,并在创建 Turn 时传回。
153
+ *
154
+ * 固定这些值可让恢复继续使用当时已经准入的配置。
155
+ */
156
+ export interface PinnedPiRuntime {
157
+ readonly descriptor: string;
158
+ readonly degradations: readonly RuntimeDegradation[];
159
+ }
160
+
161
+ /**
162
+ * 描述一条已存提交可以恢复到哪份 Pi 配置。
163
+ *
164
+ * @remarks
165
+ * `PreparedPiTurnAdapter` 在重建 Turn 前读取这个 JSON 结构。
166
+ *
167
+ * 版本号和基础版本校验用于阻止恢复落到不兼容的 Runtime 状态。
168
+ */
169
+ export interface PinnedPiRuntimeDescriptor {
170
+ readonly version: 1;
171
+ readonly baseRevision: string;
172
+ readonly model: string;
173
+ readonly provider: string;
174
+ readonly thinking: RuntimeSnapshot["pi"]["thinkingLevel"];
175
+ readonly systemPrompt: string;
176
+ readonly executionLevel: ExecutionLevel;
177
+ readonly deny: readonly string[];
178
+ readonly tools: readonly ReturnType<typeof describeTools>[number][];
179
+ readonly [key: string]: unknown;
180
+ }
181
+
182
+ /**
183
+ * 保存创建 Pi Turn 所需的内部准备结果。
184
+ *
185
+ * @remarks
186
+ * readPreparedPiRuntime 只向持有原适配器身份标记的代码返回它。
187
+ *
188
+ * 工具候选与快照放在一起,确保后续 Turn 使用准备阶段最终冻结的同一份列表。
189
+ */
190
+ export interface PreparedPiRuntimeState {
191
+ readonly snapshot: RuntimeSnapshot;
192
+ readonly assembly: PreparedPiAssembly;
193
+ readonly candidates: readonly PiToolCandidate[];
194
+ }
195
+
196
+ // #endregion
197
+
198
+ // #region 版本描述与准备句柄
199
+
200
+ const IDEMPOTENT_TOOL_NAMES = new Set([
201
+ "activate_skill",
202
+ "browser_extract",
203
+ "browser_links",
204
+ "browser_markdown",
205
+ "browser_scrape",
206
+ "bind_resource",
207
+ "find",
208
+ "get_time",
209
+ "grep",
210
+ "list",
211
+ "list_resources",
212
+ "list_extensions",
213
+ "list_schedules",
214
+ "read",
215
+ "read_skill_resource",
216
+ "sandbox_process_logs",
217
+ "web_search",
218
+ "unbind_resource",
219
+ ]);
220
+
221
+ /**
222
+ * 判断工具在执行结果不确定时能否安全重试。
223
+ *
224
+ * @remarks
225
+ * Turn 执行在记录第一次尝试前调用它,恢复流程随后使用这条持久化结论。
226
+ *
227
+ * 白名单故意保持保守,未知工具和新工具都会默认按不可重复执行处理。
228
+ */
229
+ export function piToolRetryPolicy(
230
+ candidate: PiToolCandidate,
231
+ ): "idempotent" | "non-idempotent" {
232
+ return IDEMPOTENT_TOOL_NAMES.has(candidate.tool.name)
233
+ ? "idempotent"
234
+ : "non-idempotent";
235
+ }
236
+
237
+ // 把工具候选整理成版本描述里要保存的稳定字段。
238
+ // describeRuntime 在准备 Runtime 配置时调用它。
239
+ // 按名称排序可避免仅因候选注册顺序变化就产生新版本。
240
+ function describeTools(candidates: readonly PiToolCandidate[]) {
241
+ return candidates
242
+ .map((candidate) => ({
243
+ name: candidate.tool.name,
244
+ owner: candidate.owner,
245
+ label: candidate.tool.label,
246
+ description:
247
+ candidate.tool.description ?? candidate.tool.label,
248
+ parameters: candidate.tool.parameters,
249
+ requiredExecutionLevel: candidate.requiredExecutionLevel,
250
+ ...(candidate.source ? { source: candidate.source } : {}),
251
+ retry: piToolRetryPolicy(candidate),
252
+ }))
253
+ .sort((left, right) => left.name.localeCompare(right.name));
254
+ }
255
+
256
+ // 把本模块认定会影响版本的准备结果序列化出来。
257
+ // preparePiRuntime 创建一次,Runtime 接受快照前再对结果求哈希。
258
+ // 集合排序保证等价输入稳定;增删字段会改变版本哈希和恢复兼容性。
259
+ function describeRuntime(
260
+ snapshot: RuntimeSnapshot,
261
+ candidates: readonly PiToolCandidate[],
262
+ loadedExtensions: readonly PiLoadedExtension[],
263
+ ): string {
264
+ return JSON.stringify({
265
+ version: 1,
266
+ model: snapshot.pi.model.id,
267
+ provider: snapshot.pi.model.provider,
268
+ thinking: snapshot.pi.thinkingLevel,
269
+ systemPrompt: snapshot.pi.systemPrompt,
270
+ executionLevel: snapshot.profile.executionLevel,
271
+ deny: [...(snapshot.profile.denyPolicy?.deny ?? [])].sort(),
272
+ memory: { ...snapshot.profile.memory },
273
+ tools: describeTools(candidates),
274
+ skills: snapshot.bindings.skills.sources
275
+ .map((binding) => ({
276
+ name: binding.name,
277
+ description: binding.description,
278
+ script: {
279
+ network: binding.script.network,
280
+ workspace: binding.script.workspace,
281
+ tools: [...binding.script.tools].sort(),
282
+ },
283
+ }))
284
+ .sort((left, right) => left.name.localeCompare(right.name)),
285
+ extensions: loadedExtensions
286
+ .map((extension) => ({
287
+ name: extension.name,
288
+ version: extension.version,
289
+ }))
290
+ .sort((left, right) => left.name.localeCompare(right.name)),
291
+ subagents: [...snapshot.profile.enabledSubagents].sort(),
292
+ mcpServers: snapshot.profile.mcpServers
293
+ .map((server) => ({ name: server.name, url: server.url }))
294
+ .sort((left, right) => left.name.localeCompare(right.name)),
295
+ });
296
+ }
297
+
298
+ class PreparedRuntime implements PreparedPiRuntime {
299
+ // 把公开版本数据与受身份保护的内部准备状态放在同一个句柄里。
300
+ // 只有 preparePiRuntime 会构造它,并在返回前冻结实例。
301
+ // owner 使用对象身份区分 PiRuntimeAdapter,避免不同适配器的句柄被混用。
302
+ constructor(
303
+ readonly revisionDescriptor: string,
304
+ readonly degradations: readonly RuntimeDegradation[],
305
+ private readonly owner: object,
306
+ private readonly state: PreparedPiRuntimeState,
307
+ ) {}
308
+
309
+ // 在调用方证明适配器身份后返回内部准备状态。
310
+ // readPreparedPiRuntime 在固定配置或创建 Turn 前调用它。
311
+ // 公开接口没有可信的来源字段,所以这里必须比较对象身份。
312
+ read(owner: object): PreparedPiRuntimeState {
313
+ if (owner !== this.owner) {
314
+ throw new Error(
315
+ "Prepared Pi Runtime belongs to another PiRuntimeAdapter",
316
+ );
317
+ }
318
+ return this.state;
319
+ }
320
+ }
321
+
322
+ /**
323
+ * 为创建它的 PiRuntimeAdapter 打开准备句柄。
324
+ *
325
+ * @remarks
326
+ * 固定配置和创建 Turn 都会先调用它,再读取快照、装配结果或工具候选。
327
+ *
328
+ * 具体类检查与 owner 检查一起拒绝伪造句柄和跨适配器复用。
329
+ */
330
+ export function readPreparedPiRuntime(
331
+ prepared: PreparedPiRuntime,
332
+ owner: object,
333
+ ): PreparedPiRuntimeState {
334
+ if (!(prepared instanceof PreparedRuntime)) {
335
+ throw new Error("Prepared Pi Runtime handle is invalid");
336
+ }
337
+ return prepared.read(owner);
338
+ }
339
+
340
+ /**
341
+ * 校验已存的固定描述是否仍与当前准备好的 Runtime 兼容。
342
+ *
343
+ * @remarks
344
+ * `PreparedPiTurnAdapter` 在新建或恢复 Pi Turn 前调用它。
345
+ *
346
+ * 只有固定阶段写入的提示词允许不同,其余准备字段及基础版本必须匹配。
347
+ */
348
+ export function readPinnedPiRuntime(
349
+ prepared: PreparedPiRuntime,
350
+ owner: object,
351
+ descriptorBody: string,
352
+ baseRevision: string,
353
+ ): {
354
+ readonly state: PreparedPiRuntimeState;
355
+ readonly descriptor: PinnedPiRuntimeDescriptor;
356
+ } {
357
+ const state = readPreparedPiRuntime(prepared, owner);
358
+ let value: unknown;
359
+ try {
360
+ value = JSON.parse(descriptorBody);
361
+ } catch {
362
+ throw new Error("Pinned Runtime assembly descriptor is invalid");
363
+ }
364
+ if (!value || typeof value !== "object") {
365
+ throw new Error("Pinned Runtime assembly descriptor is invalid");
366
+ }
367
+ const descriptor = value as PinnedPiRuntimeDescriptor;
368
+ const expected = JSON.parse(prepared.revisionDescriptor) as Record<
369
+ string,
370
+ unknown
371
+ >;
372
+ const actual = descriptor as unknown as Record<string, unknown>;
373
+ const incompatible = Object.keys(expected).some((key) =>
374
+ key !== "systemPrompt" &&
375
+ JSON.stringify(actual[key]) !== JSON.stringify(expected[key])
376
+ );
377
+ if (
378
+ descriptor.version !== 1 ||
379
+ descriptor.baseRevision !== baseRevision ||
380
+ typeof descriptor.systemPrompt !== "string" ||
381
+ incompatible
382
+ ) {
383
+ throw new Error(
384
+ "Pinned Runtime revision is unavailable for Pi recovery",
385
+ );
386
+ }
387
+ return { state, descriptor };
388
+ }
389
+
390
+ // #endregion
391
+
392
+ // #region 扩展与工具装配
393
+
394
+ class PreparedAssembly implements PreparedPiAssembly {
395
+ // 把准备快照、已加载扩展和 MCP Host 放在一个装配对象里。
396
+ // preparePiAssembly 在扩展加载完成后构造它。
397
+ // 这里保留源对象而不复制列表,让每个访问器都读取同一份结果。
398
+ constructor(
399
+ private readonly snapshot: PiAssemblySnapshot,
400
+ private readonly extensions: PiExtensionAssembly,
401
+ private readonly mcpHost: PiMcpHost,
402
+ ) {}
403
+
404
+ // 返回成功加载的扩展。
405
+ // preparePiRuntime 在生成版本描述时读取这个列表。
406
+ // 直接代理扩展装配,避免维护可能漂移的第二份列表。
407
+ get loaded(): readonly PiLoadedExtension[] {
408
+ return this.extensions.loaded;
409
+ }
410
+
411
+ // 返回已加载扩展声明的上下文贡献。
412
+ // pinPiRuntime 在提交准入时把它们交给系统上下文装配器。
413
+ // 上下文值在固定阶段才读取,所以不能提前写死在准备阶段的提示词中。
414
+ get context(): readonly PiExtensionContextContribution[] {
415
+ return this.extensions.context;
416
+ }
417
+
418
+ // 返回准备阶段被降级处理、而不是继续抛出的扩展失败。
419
+ // preparePiRuntime 把它们与快照降级合并,用于报告 Runtime 加载结果。
420
+ // 直接代理可保留扩展装配器生成的原始降级记录。
421
+ get degradations(): PiExtensionAssembly["degradations"] {
422
+ return this.extensions.degradations;
423
+ }
424
+
425
+ // 汇总内置、扩展列表、扩展和当前已就绪的 MCP 工具。
426
+ // 准备流程先调用它做校验,再冻结一份列表供后续 Turn 使用。
427
+ // 每次重新生成是因为 MCP 候选来自 Host 当前的连接状态。
428
+ toolCandidates(): PiToolCandidate[] {
429
+ return [
430
+ ...this.snapshot.pi.toolCandidates,
431
+ ...this.extensions.candidates,
432
+ listExtensionsPiToolCandidate(this.extensions.loaded),
433
+ ...createPiMcpToolCandidates(
434
+ this.mcpHost,
435
+ this.snapshot.profile.mcpServers,
436
+ ),
437
+ ];
438
+ }
439
+ }
440
+
441
+ // 加载已启用扩展,并确认汇总后的工具可以正常编译。
442
+ // preparePiRuntime 为每份待配置的 Runtime 快照调用它一次。
443
+ // 工作区能力在共享加载边界校验,单个扩展加载失败则保留为降级结果。
444
+ async function preparePiAssembly(
445
+ options: PreparePiAssemblyOptions,
446
+ ): Promise<PreparedPiAssembly> {
447
+ const { snapshot } = options;
448
+ const extensionNames = new Set(
449
+ snapshot.pi.extensions.map((extension) =>
450
+ extension.manifest.name.trim()
451
+ ),
452
+ );
453
+ const extensions = await assemblePiExtensions({
454
+ extensions: snapshot.pi.extensions,
455
+ published: extensionNames,
456
+ enabled: new Set(snapshot.profile.enabledExtensions),
457
+ authorized: extensionNames,
458
+ load: async (extension) => {
459
+ const workspacePermission =
460
+ extension.manifest.permissions?.workspace ?? "none";
461
+ if (workspacePermission !== "none" && !snapshot.bindings.workspace) {
462
+ throw new Error(
463
+ "Extension workspace permission requires a Runtime Workspace",
464
+ );
465
+ }
466
+ return loadPiExtension(extension, {
467
+ loader: snapshot.bindings.platform.loader,
468
+ createHostBinding: options.createExtensionHostBinding,
469
+ });
470
+ },
471
+ });
472
+ const prepared = new PreparedAssembly(
473
+ snapshot,
474
+ extensions,
475
+ options.mcpHost,
476
+ );
477
+ // 待确认:MCP 就绪状态可能在下一次 toolCandidates() 前改变,
478
+ // 因而最终冻结的候选可能与这里校验的集合不同。
479
+ compilePiTools(prepared.toolCandidates(), {
480
+ deny: snapshot.profile.denyPolicy?.deny,
481
+ settle: () => undefined,
482
+ });
483
+ return prepared;
484
+ }
485
+
486
+ // #endregion
487
+
488
+ // #region Runtime 准备与提交固定
489
+
490
+ /**
491
+ * 为一份已校验的 Runtime 快照准备扩展和工具状态。
492
+ *
493
+ * @remarks
494
+ * `PiRuntimeAdapter.prepare` 在 Runtime 配置阶段、快照生效前调用它。
495
+ *
496
+ * 冻结句柄会保存最终候选列表,对外只提供版本描述和降级结果。
497
+ */
498
+ export async function preparePiRuntime(
499
+ options: PreparePiRuntimeOptions,
500
+ owner: object,
501
+ ): Promise<PreparedPiRuntime> {
502
+ const assembly = await preparePiAssembly(options);
503
+ const candidates = assembly.toolCandidates();
504
+ return Object.freeze(new PreparedRuntime(
505
+ describeRuntime(options.snapshot, candidates, assembly.loaded),
506
+ Object.freeze([
507
+ ...options.snapshot.degradations,
508
+ ...assembly.degradations,
509
+ ]),
510
+ owner,
511
+ Object.freeze({
512
+ snapshot: options.snapshot,
513
+ assembly,
514
+ candidates: Object.freeze([...candidates]),
515
+ }),
516
+ ));
517
+ }
518
+
519
+ /**
520
+ * 把请求时的上下文和执行档位固定到一次提交的描述中。
521
+ *
522
+ * @remarks
523
+ * Runtime 在持久化提交并启动 Turn 前的准入阶段调用它。
524
+ *
525
+ * 此时重建上下文可避免把动态记忆和扩展上下文写进 Pi 消息,同时让恢复继续使用已准入的结果。
526
+ */
527
+ export async function pinPiRuntime(
528
+ options: PinPiRuntimeOptions,
529
+ owner: object,
530
+ ): Promise<PinnedPiRuntime> {
531
+ const state = readPreparedPiRuntime(options.prepared, owner);
532
+ const { snapshot, assembly } = state;
533
+ const context = await assemblePiSystemContext({
534
+ baseSystemPrompt: snapshot.pi.systemPrompt,
535
+ memory: snapshot.profile.memory,
536
+ memoryPort: snapshot.bindings.memory,
537
+ workspace: snapshot.bindings.workspace,
538
+ skills: snapshot.bindings.skills.sources,
539
+ extensionContexts: assembly.context,
540
+ readExtensionContext: options.readExtensionContext,
541
+ });
542
+ return Object.freeze({
543
+ descriptor: JSON.stringify({
544
+ ...JSON.parse(options.prepared.revisionDescriptor),
545
+ systemPrompt: context.systemPrompt,
546
+ baseRevision: options.baseRevision,
547
+ }),
548
+ degradations: context.degradations,
549
+ });
550
+ }
551
+
552
+ // #endregion