@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
package/src/lib/prompt.ts CHANGED
@@ -46,7 +46,7 @@ export const BEHAVIOR =
46
46
  export const PLANNING =
47
47
  "Planning: For work with 3 or more distinct steps, or any non-trivial / multi-file change, call " +
48
48
  "update_plan with the complete plan (it fully replaces the previous one) so the user sees live " +
49
- "progress. Mark a step in_progress before starting it and completed the moment it's done; keep only " +
49
+ "progress. Mark a step in_progress before starting it and done the moment it's done; keep only " +
50
50
  "one step in_progress at a time and don't batch completions. Do not make a plan for a single trivial " +
51
51
  "step or a purely conversational reply — just do it.";
52
52
 
@@ -1,9 +1,12 @@
1
1
  /**
2
- * dev-only 观测降级:把 observability 事件打到 console,让 `wrangler dev` /
3
- * `wrangler tail` 里能实时看到埋点(否则 publish 到零订阅 channel = 静默 no-op)。
2
+ * 观测降级:把 observability 事件打到 console,让 `wrangler dev` / `wrangler tail` 里能实时看到埋点,
3
+ * 并且(生产上开了 `observability.logs` 时)进 Workers Logs 供事后查询。
4
+ * 不装它,事件就 publish 到零订阅 channel = 静默 no-op。
4
5
  *
5
- * 仅在 env `TELEMETRY_CONSOLE==="1"` 时由 onStart 装载(`.dev.vars` 里开,prod 不设 自动关)。
6
- * 生产不靠它:prod 所有 channel 事件自动转发 Tail Worker
6
+ * onStart 在 env `TELEMETRY_CONSOLE==="1"` 时装载 —— `.dev.vars` `wrangler.jsonc` vars 里都开着。
7
+ * 注意:上游注释宣称"生产上所有 channel 事件自动转发 Tail Worker",那条路径需要 `tail_consumers`,
8
+ * 本仓没有配,所以生产**不能**指望它;这个 console sink 就是生产的唯一消费面(2026-08-06 实测:
9
+ * 未开启前 Observability API 查 universal-agent 24h 事件 count=0)。
7
10
  *
8
11
  * 用 `agents/observability` 的类型化 `subscribe`(按 channel key 订阅),避免引 `node:diagnostics_channel`
9
12
  * 与 `@types/node`(tsconfig 只带 workers-types)。
@@ -116,7 +116,7 @@ export async function compactPiContext(input: Readonly<{
116
116
  const settings = DEFAULT_COMPACTION_SETTINGS;
117
117
  if (
118
118
  !settings.enabled ||
119
- estimateContextTokens(currentMessages).tokens <=
119
+ estimateContextTokens(currentMessages).tokens <
120
120
  input.compactAfterTokens
121
121
  ) {
122
122
  return {
@@ -367,7 +367,7 @@ async function readMemory(
367
367
  return { blocks, degradations };
368
368
  }
369
369
 
370
- // 作用:把已授权 Skill 的持久化 catalog 整理成提示词。
370
+ // 作用:把已配置 Skill 的持久化 catalog 整理成提示词。
371
371
  // 调用:系统上下文装配在需要告诉模型当前可用 Skill 时调用。
372
372
  // 原因:catalog 已在发布和绑定时固定,装配期不得为此访问远端内容来源。
373
373
  function renderSkillCatalog(
@@ -379,8 +379,8 @@ function renderSkillCatalog(
379
379
  if (bindings.length === 0) return null;
380
380
 
381
381
  return [
382
- "AUTHORIZED SKILLS",
383
- "Use an authorized Skill when its description matches. Read its instructions or files only through the authorized Skill tool.",
382
+ "AVAILABLE SKILLS",
383
+ "Use an available Skill when its description matches. Read its instructions or files only through the Skill tools.",
384
384
  "",
385
385
  ...bindings.map(
386
386
  ({ name, description }) => `- ${name}: ${description}`,
@@ -479,7 +479,6 @@ export async function assemblePiSystemContext(input: Readonly<{
479
479
  ) => Promise<string | null>;
480
480
  }>): Promise<Readonly<{
481
481
  systemPrompt: string;
482
- compactAfterTokens: number;
483
482
  degradations: readonly RuntimeDegradation[];
484
483
  }>> {
485
484
  const [memory, extensions] = await Promise.all([
@@ -499,7 +498,6 @@ export async function assemblePiSystemContext(input: Readonly<{
499
498
 
500
499
  return Object.freeze({
501
500
  systemPrompt: parts.join("\n\n"),
502
- compactAfterTokens: input.memory.compactAfterTokens,
503
501
  degradations: Object.freeze([
504
502
  ...memory.degradations,
505
503
  ...extensions.degradations,
@@ -11,6 +11,7 @@ import type {
11
11
  RuntimeExtensionPermissions,
12
12
  } from "../../kernel/extensions";
13
13
  import type { RuntimeDegradation } from "../../kernel/degradation";
14
+ import { withRuntimeLoadTimeout } from "../../kernel/runtime-load";
14
15
  import { sanitizeExtensionName } from "../../lib/extension-name";
15
16
  import type { PiToolCandidate } from "../tool/compiler";
16
17
 
@@ -42,9 +43,6 @@ export interface PiLoadedExtension {
42
43
 
43
44
  export interface AssemblePiExtensionsOptions {
44
45
  readonly extensions: readonly RuntimeExtensionConfig[];
45
- readonly published: ReadonlySet<string>;
46
- readonly enabled: ReadonlySet<string>;
47
- readonly authorized: ReadonlySet<string>;
48
46
  readonly load: (
49
47
  extension: RuntimeExtensionConfig,
50
48
  ) => Promise<LoadedRuntimeExtension>;
@@ -237,7 +235,7 @@ const EXTENSION_HOOK_NAMES = [
237
235
  * 通过 Cloudflare ExtensionManager 加载一个 Extension,并只暴露 Runtime 需要的描述和执行能力。
238
236
  *
239
237
  * @remarks
240
- * Runtime 准备阶段会对已授权 Extension 调用,资源发布的 smoke test 也会用它校验候选源码;调用方必须提供 WorkerLoader,并在权限需要时提供 Host 绑定工厂。
238
+ * Runtime 准备阶段会对已配置 Extension 调用,资源发布的 smoke test 也会用它校验候选源码;调用方必须提供 WorkerLoader,并在权限需要时提供 Host 绑定工厂。
241
239
  *
242
240
  * discovery 使用剔除权限的 manifest,有外部访问的执行 isolate 则延迟到首次 Tool 执行时创建。这是为了防止 `describe()` 在授权和审批前获得网络或 Host 能力,不能随意合并两条加载路径。
243
241
  *
@@ -267,9 +265,12 @@ export async function loadPiExtension(
267
265
  const { permissions: _ignored, ...manifestWithoutPermissions } =
268
266
  extension.manifest;
269
267
  try {
270
- await discovery.load(
271
- manifestWithoutPermissions as ExtensionManifest,
272
- extension.source,
268
+ await withRuntimeLoadTimeout(
269
+ `extension:${name}:discover`,
270
+ () => discovery.load(
271
+ manifestWithoutPermissions as ExtensionManifest,
272
+ extension.source,
273
+ ),
273
274
  );
274
275
  } catch (cause) {
275
276
  // Upstream assumes describe() yields an array and throws an opaque TypeError
@@ -507,20 +508,17 @@ export function extensionToolRequiredExecutionLevel(
507
508
  }
508
509
 
509
510
  /**
510
- * 把当前已发布、已启用且已授权的 Extension 装配成 Runtime 能力。
511
+ * 把当前已配置的 Extension 装配成 Runtime 能力。
511
512
  *
512
513
  * @remarks
513
- * Runtime 准备阶段和资源 smoke test 调用;调用方传入三个选择集合及一个已经确定加载策略的 `load`。
514
+ * Runtime 准备阶段和资源 smoke test 调用;调用方传入已选 Extension 及已确定加载策略的 `load`。
514
515
  *
515
- * 只有同时通过三道选择的 Extension 才会被加载,单个加载失败记为 degradation 而不影响其他项;不要绕过这个交集直接注册 Tool。
516
+ * 单个加载失败记为 degradation 而不影响其他项。
516
517
  *
517
518
  * 核心术语见包入口 `index.ts`。
518
519
  */
519
520
  export async function assemblePiExtensions({
520
521
  extensions,
521
- published,
522
- enabled,
523
- authorized,
524
522
  load,
525
523
  }: AssemblePiExtensionsOptions): Promise<PiExtensionAssembly> {
526
524
  const loadedExtensions: PiLoadedExtension[] = [];
@@ -530,14 +528,6 @@ export async function assemblePiExtensions({
530
528
 
531
529
  for (const extension of extensions) {
532
530
  const name = extension.manifest.name.trim();
533
- if (
534
- !published.has(name) ||
535
- !enabled.has(name) ||
536
- !authorized.has(name)
537
- ) {
538
- continue;
539
- }
540
-
541
531
  let loaded: LoadedRuntimeExtension;
542
532
  try {
543
533
  loaded = await load(extension);
@@ -608,7 +598,6 @@ export async function assemblePiExtensions({
608
598
  });
609
599
  candidates.push(Object.freeze({
610
600
  owner: `extension:${name}@${extension.manifest.version}`,
611
- authorized: true,
612
601
  requiredExecutionLevel,
613
602
  summary: description,
614
603
  tool,
@@ -3,6 +3,7 @@ import type {
3
3
  ThinkingLevel,
4
4
  } from "@earendil-works/pi-agent-core";
5
5
  import type { Api, Model } from "@earendil-works/pi-ai";
6
+ import { clampReasoning } from "@earendil-works/pi-ai/api/simple-options";
6
7
  import type {
7
8
  RuntimeMcpServer,
8
9
  RuntimeProfile,
@@ -16,13 +17,22 @@ import { assembleSystemPrompt } from "../../lib/prompt";
16
17
  import { resolvePiModel } from "../runtime-adapter/models";
17
18
  import type { PiToolCandidate } from "../tool/compiler";
18
19
 
20
+ /**
21
+ * 收集完动态 Extension/MCP 候选后,一次性生成最终 Tool Surface。
22
+ */
23
+ export interface PiToolSurface {
24
+ finalize(
25
+ candidates: readonly PiToolCandidate[],
26
+ ): readonly PiToolCandidate[];
27
+ }
28
+
19
29
  /** Immutable inputs consumed directly by Pi Agent Core. */
20
30
  export interface PiRuntimeAssembly {
21
31
  readonly model: Model<Api>;
22
32
  readonly thinkingLevel: ThinkingLevel;
23
33
  readonly systemPrompt: string;
24
34
  readonly messages: readonly AgentMessage[];
25
- readonly toolCandidates: readonly PiToolCandidate[];
35
+ readonly toolSurface: PiToolSurface;
26
36
  readonly mcpServers: readonly RuntimeMcpServer[];
27
37
  readonly extensions: readonly RuntimeExtensionConfig[];
28
38
  }
@@ -34,7 +44,7 @@ interface PiRuntimeAssemblyInput {
34
44
  >;
35
45
  readonly provider: RuntimeProviderPort;
36
46
  readonly messages?: readonly AgentMessage[];
37
- readonly toolCandidates: readonly PiToolCandidate[];
47
+ readonly toolSurface: PiToolSurface;
38
48
  readonly mcpServers: readonly RuntimeMcpServer[];
39
49
  readonly extensions: readonly RuntimeExtensionConfig[];
40
50
  }
@@ -70,11 +80,13 @@ function freezeModel(model: Model<Api>): Model<Api> {
70
80
 
71
81
  // 作用:把 Runtime 的思考强度名称转成 Pi Agent Core 接受的名称。
72
82
  // 调用:创建 Runtime 装配快照时对 profile 中的 `thinking` 调用。
73
- // 原因:只有 `none` Pi 的 `off` 命名不同,其他级别应保持原值。
83
+ // 原因:`none` 映射成 `off`;xhigh/max 用 clampReasoning 夹到 API 真正支持的最高档,
84
+ // 避免超出范围的等级在 provider 侧静默失败。
74
85
  function toPiThinkingLevel(
75
86
  thinking: ThinkingEffort,
76
87
  ): ThinkingLevel {
77
- return thinking === "none" ? "off" : thinking;
88
+ if (thinking === "none") return "off";
89
+ return clampReasoning(thinking) ?? "off";
78
90
  }
79
91
 
80
92
  // 作用:复制并冻结一个 Extension 配置的 manifest 和权限集合。
@@ -172,15 +184,11 @@ export function createPiRuntimeAssembly(
172
184
  input: PiRuntimeAssemblyInput,
173
185
  ): PiRuntimeAssembly {
174
186
  const messages = [...(input.messages ?? [])];
175
- const toolCandidates = input.toolCandidates.map(
176
- (candidate) => Object.freeze({ ...candidate }),
177
- );
178
187
  const mcpServers = input.mcpServers.map(
179
188
  (server) => Object.freeze({ ...server }),
180
189
  );
181
190
  const extensions = input.extensions.map(freezeExtension);
182
191
  Object.freeze(messages);
183
- Object.freeze(toolCandidates);
184
192
  Object.freeze(mcpServers);
185
193
  Object.freeze(extensions);
186
194
 
@@ -193,7 +201,7 @@ export function createPiRuntimeAssembly(
193
201
  input.profile.systemPrompt,
194
202
  ),
195
203
  messages,
196
- toolCandidates,
204
+ toolSurface: input.toolSurface,
197
205
  mcpServers,
198
206
  extensions,
199
207
  });
@@ -2,6 +2,13 @@ import type { UIMessage } from "ai";
2
2
 
3
3
  export type UIChatTrigger = "submit-message" | "regenerate-message";
4
4
 
5
+ /** A user-selected Runtime capability persisted with the visible UIMessage. */
6
+ export interface RequestedCapability {
7
+ readonly kind: "skill" | "plan";
8
+ readonly name: string;
9
+ readonly label: string;
10
+ }
11
+
5
12
  export interface UIChatRequestBody {
6
13
  readonly messages: readonly UIMessage[];
7
14
  readonly trigger: UIChatTrigger;
@@ -26,6 +26,7 @@ function attachmentText(
26
26
 
27
27
  function userContent(
28
28
  parts: ReadonlyArray<UIMessage["parts"][number]>,
29
+ privateContext?: string,
29
30
  ): UserMessage["content"] {
30
31
  const content: Exclude<UserMessage["content"], string> = [];
31
32
  for (const part of parts) {
@@ -46,16 +47,23 @@ function userContent(
46
47
  }
47
48
  }
48
49
  if (content.length === 0) throw new Error("User message content is required");
50
+ if (privateContext) {
51
+ content.unshift({
52
+ type: "text",
53
+ text: `[Agent private context]\n${privateContext}\n[/Agent private context]`,
54
+ });
55
+ }
49
56
  return content;
50
57
  }
51
58
 
52
59
  /** Convert an official user UIMessage into canonical Pi model input. */
53
60
  export function uiUserMessageToPi(
54
61
  message: UIMessage & { readonly role: "user" },
62
+ privateContext?: string,
55
63
  ): UserMessage {
56
64
  return {
57
65
  role: "user",
58
- content: userContent(message.parts),
66
+ content: userContent(message.parts, privateContext),
59
67
  timestamp: timestampOf(message),
60
68
  };
61
69
  }
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  RuntimeBindings,
3
+ RuntimeGatewaySession,
3
4
  RuntimePlatformPort,
4
5
  } from "../../kernel/bindings";
5
6
  import type { RuntimeDegradation } from "../../kernel/degradation";
@@ -8,7 +9,7 @@ import type {
8
9
  } from "../../kernel/extensions";
9
10
  import type { RuntimeProfile } from "../../kernel/profile";
10
11
  import type { ExecutionLevel } from "../../lib/execution-level";
11
- import type { RuntimeSnapshot } from "../../plugins";
12
+ import type { RuntimeSnapshot } from "../../runtime-assembler";
12
13
  import type { PiRuntimeAssembly } from "../assembly";
13
14
  import {
14
15
  assemblePiSystemContext,
@@ -19,7 +20,7 @@ import {
19
20
  type PiLoadedExtension,
20
21
  } from "../assembly";
21
22
  import {
22
- compilePiTools,
23
+ createPiGatewayToolCandidates,
23
24
  createPiMcpToolCandidates,
24
25
  listExtensionsPiToolCandidate,
25
26
  type PiMcpHost,
@@ -41,7 +42,7 @@ import {
41
42
  interface PiAssemblySnapshot {
42
43
  readonly pi: Pick<
43
44
  PiRuntimeAssembly,
44
- "extensions" | "toolCandidates"
45
+ "extensions" | "toolSurface"
45
46
  >;
46
47
  readonly profile: Pick<
47
48
  RuntimeProfile,
@@ -59,6 +60,7 @@ interface PiAssemblySnapshot {
59
60
  interface PreparePiAssemblyOptions {
60
61
  readonly snapshot: PiAssemblySnapshot;
61
62
  readonly mcpHost: PiMcpHost;
63
+ readonly gatewaySession?: RuntimeGatewaySession;
62
64
  // 为一个扩展生成带权限参数的 Host Fetcher。
63
65
  // loadPiExtension 在扩展需要 Host 能力时调用它。
64
66
  // Worker 掌握真实绑定,装配层只保存这个窄函数,不能自行扩大权限。
@@ -75,10 +77,7 @@ interface PreparedPiAssembly {
75
77
  readonly loaded: readonly PiLoadedExtension[];
76
78
  readonly context: readonly PiExtensionContextContribution[];
77
79
  readonly degradations: PiExtensionAssembly["degradations"];
78
- // 生成当前可供 Pi 编译的完整工具候选列表。
79
- // preparePiAssembly 用它预检,preparePiRuntime 再读取并冻结最终列表。
80
- // MCP 就绪状态由 Host 提供,因此这里不能只拼接初始化时的静态数组。
81
- toolCandidates(): PiToolCandidate[];
80
+ readonly candidates: readonly PiToolCandidate[];
82
81
  }
83
82
 
84
83
  /**
@@ -94,6 +93,8 @@ interface PreparedPiAssembly {
94
93
  export interface PreparePiRuntimeOptions {
95
94
  readonly snapshot: RuntimeSnapshot;
96
95
  readonly mcpHost: PiMcpHost;
96
+ readonly gatewaySession?: RuntimeGatewaySession;
97
+ readonly additionalDegradations?: readonly RuntimeDegradation[];
97
98
  /**
98
99
  * 为一个扩展创建受限的 Host 绑定。
99
100
  *
@@ -119,6 +120,7 @@ export interface PreparePiRuntimeOptions {
119
120
  export interface PreparedPiRuntime {
120
121
  readonly revisionDescriptor: string;
121
122
  readonly degradations: readonly RuntimeDegradation[];
123
+ readonly extensions: readonly { readonly name: string; readonly version: string }[];
122
124
  }
123
125
 
124
126
  /**
@@ -164,7 +166,7 @@ export interface PinnedPiRuntime {
164
166
  * @remarks
165
167
  * `PreparedPiTurnAdapter` 在重建 Turn 前读取这个 JSON 结构。
166
168
  *
167
- * 版本号和基础版本校验用于阻止恢复落到不兼容的 Runtime 状态。
169
+ * 基础版本作为准入时元数据保留,创建 Turn 时不与当前 Runtime 比较。
168
170
  */
169
171
  export interface PinnedPiRuntimeDescriptor {
170
172
  readonly version: 1;
@@ -204,6 +206,8 @@ const IDEMPOTENT_TOOL_NAMES = new Set([
204
206
  "browser_markdown",
205
207
  "browser_scrape",
206
208
  "bind_resource",
209
+ "delete",
210
+ "edit",
207
211
  "find",
208
212
  "get_time",
209
213
  "grep",
@@ -215,6 +219,7 @@ const IDEMPOTENT_TOOL_NAMES = new Set([
215
219
  "read_skill_resource",
216
220
  "sandbox_process_logs",
217
221
  "web_search",
222
+ "write",
218
223
  "unbind_resource",
219
224
  ]);
220
225
 
@@ -243,10 +248,12 @@ function describeTools(candidates: readonly PiToolCandidate[]) {
243
248
  name: candidate.tool.name,
244
249
  owner: candidate.owner,
245
250
  label: candidate.tool.label,
246
- description:
247
- candidate.tool.description ?? candidate.tool.label,
251
+ description: candidate.tool.description ?? candidate.tool.label,
248
252
  parameters: candidate.tool.parameters,
249
253
  requiredExecutionLevel: candidate.requiredExecutionLevel,
254
+ ...(candidate.alwaysRequiresApproval
255
+ ? { alwaysRequiresApproval: true }
256
+ : {}),
250
257
  ...(candidate.source ? { source: candidate.source } : {}),
251
258
  retry: piToolRetryPolicy(candidate),
252
259
  }))
@@ -255,7 +262,7 @@ function describeTools(candidates: readonly PiToolCandidate[]) {
255
262
 
256
263
  // 把本模块认定会影响版本的准备结果序列化出来。
257
264
  // preparePiRuntime 创建一次,Runtime 接受快照前再对结果求哈希。
258
- // 集合排序保证等价输入稳定;增删字段会改变版本哈希和恢复兼容性。
265
+ // 集合排序保证等价输入稳定;增删字段会改变版本哈希。
259
266
  function describeRuntime(
260
267
  snapshot: RuntimeSnapshot,
261
268
  candidates: readonly PiToolCandidate[],
@@ -302,6 +309,7 @@ class PreparedRuntime implements PreparedPiRuntime {
302
309
  constructor(
303
310
  readonly revisionDescriptor: string,
304
311
  readonly degradations: readonly RuntimeDegradation[],
312
+ readonly extensions: readonly { readonly name: string; readonly version: string }[],
305
313
  private readonly owner: object,
306
314
  private readonly state: PreparedPiRuntimeState,
307
315
  ) {}
@@ -338,18 +346,17 @@ export function readPreparedPiRuntime(
338
346
  }
339
347
 
340
348
  /**
341
- * 校验已存的固定描述是否仍与当前准备好的 Runtime 兼容。
349
+ * 读取已存的固定描述。
342
350
  *
343
351
  * @remarks
344
352
  * `PreparedPiTurnAdapter` 在新建或恢复 Pi Turn 前调用它。
345
353
  *
346
- * 只有固定阶段写入的提示词允许不同,其余准备字段及基础版本必须匹配。
354
+ * Prepared Runtime 提供当前执行能力,descriptor 仅保留 Submission 固定的 Turn 字段。
347
355
  */
348
356
  export function readPinnedPiRuntime(
349
357
  prepared: PreparedPiRuntime,
350
358
  owner: object,
351
359
  descriptorBody: string,
352
- baseRevision: string,
353
360
  ): {
354
361
  readonly state: PreparedPiRuntimeState;
355
362
  readonly descriptor: PinnedPiRuntimeDescriptor;
@@ -365,24 +372,11 @@ export function readPinnedPiRuntime(
365
372
  throw new Error("Pinned Runtime assembly descriptor is invalid");
366
373
  }
367
374
  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
375
  if (
378
376
  descriptor.version !== 1 ||
379
- descriptor.baseRevision !== baseRevision ||
380
- typeof descriptor.systemPrompt !== "string" ||
381
- incompatible
377
+ typeof descriptor.systemPrompt !== "string"
382
378
  ) {
383
- throw new Error(
384
- "Pinned Runtime revision is unavailable for Pi recovery",
385
- );
379
+ throw new Error("Pinned Runtime assembly descriptor is invalid");
386
380
  }
387
381
  return { state, descriptor };
388
382
  }
@@ -391,70 +385,15 @@ export function readPinnedPiRuntime(
391
385
 
392
386
  // #region 扩展与工具装配
393
387
 
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
- // 加载已启用扩展,并确认汇总后的工具可以正常编译。
388
+ // 加载已启用扩展,并把 Extension/MCP 候选交给唯一 Tool Surface 最终化。
442
389
  // preparePiRuntime 为每份待配置的 Runtime 快照调用它一次。
443
390
  // 工作区能力在共享加载边界校验,单个扩展加载失败则保留为降级结果。
444
391
  async function preparePiAssembly(
445
392
  options: PreparePiAssemblyOptions,
446
393
  ): Promise<PreparedPiAssembly> {
447
394
  const { snapshot } = options;
448
- const extensionNames = new Set(
449
- snapshot.pi.extensions.map((extension) =>
450
- extension.manifest.name.trim()
451
- ),
452
- );
453
395
  const extensions = await assemblePiExtensions({
454
396
  extensions: snapshot.pi.extensions,
455
- published: extensionNames,
456
- enabled: extensionNames,
457
- authorized: extensionNames,
458
397
  load: async (extension) => {
459
398
  const workspacePermission =
460
399
  extension.manifest.permissions?.workspace ?? "none";
@@ -469,18 +408,22 @@ async function preparePiAssembly(
469
408
  });
470
409
  },
471
410
  });
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,
411
+ return Object.freeze({
412
+ loaded: extensions.loaded,
413
+ context: extensions.context,
414
+ degradations: extensions.degradations,
415
+ candidates: snapshot.pi.toolSurface.finalize([
416
+ ...extensions.candidates,
417
+ listExtensionsPiToolCandidate(extensions.loaded),
418
+ ...(options.gatewaySession
419
+ ? createPiGatewayToolCandidates(options.gatewaySession)
420
+ : []),
421
+ ...createPiMcpToolCandidates(
422
+ options.mcpHost,
423
+ snapshot.profile.mcpServers,
424
+ ),
425
+ ]),
482
426
  });
483
- return prepared;
484
427
  }
485
428
 
486
429
  // #endregion
@@ -500,13 +443,15 @@ export async function preparePiRuntime(
500
443
  owner: object,
501
444
  ): Promise<PreparedPiRuntime> {
502
445
  const assembly = await preparePiAssembly(options);
503
- const candidates = assembly.toolCandidates();
446
+ const candidates = assembly.candidates;
504
447
  return Object.freeze(new PreparedRuntime(
505
448
  describeRuntime(options.snapshot, candidates, assembly.loaded),
506
449
  Object.freeze([
507
450
  ...options.snapshot.degradations,
451
+ ...(options.additionalDegradations ?? []),
508
452
  ...assembly.degradations,
509
453
  ]),
454
+ Object.freeze(assembly.loaded.map(({ name, version }) => ({ name, version }))),
510
455
  owner,
511
456
  Object.freeze({
512
457
  snapshot: options.snapshot,