@springbrand/agent-runtime 0.1.3-alpha.1 → 0.1.3-alpha.11

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 (76) 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 +57 -21
  24. package/src/kernel/approval-lifecycle.ts +41 -6
  25. package/src/kernel/bindings.ts +73 -9
  26. package/src/kernel/interaction-lifecycle.ts +395 -0
  27. package/src/kernel/public-contracts.ts +2 -0
  28. package/src/kernel/recoverable-chat-agent.ts +104 -6
  29. package/src/kernel/runtime-assembly-view.ts +37 -0
  30. package/src/kernel/runtime-assembly.ts +41 -0
  31. package/src/kernel/runtime-config.ts +4 -0
  32. package/src/kernel/runtime-load.ts +191 -0
  33. package/src/kernel/state.ts +12 -1
  34. package/src/kernel/submission-lifecycle.ts +33 -2
  35. package/src/layers/orchestration/temporary-agent/core.ts +12 -1
  36. package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
  37. package/src/lib/mcp.ts +7 -3
  38. package/src/lib/prompt.ts +4 -1
  39. package/src/lib/telemetry-dev.ts +7 -4
  40. package/src/pi/assembly/context.ts +3 -3
  41. package/src/pi/assembly/extensions.ts +11 -22
  42. package/src/pi/assembly/snapshot.ts +6 -3
  43. package/src/pi/message/contract.ts +7 -0
  44. package/src/pi/message/conversion.ts +9 -1
  45. package/src/pi/runtime-adapter/assembly.ts +26 -31
  46. package/src/pi/runtime-adapter/execution.ts +198 -15
  47. package/src/pi/runtime-adapter/index.ts +24 -8
  48. package/src/pi/runtime-adapter/models.ts +382 -35
  49. package/src/pi/runtime-adapter/recovery.ts +188 -1
  50. package/src/pi/runtime-adapter/transcript.ts +61 -3
  51. package/src/pi/tool/ai-adapter.ts +58 -1
  52. package/src/pi/tool/base.ts +190 -12
  53. package/src/pi/tool/compiler.ts +34 -1
  54. package/src/pi/tool/core-host.ts +19 -24
  55. package/src/pi/tool/core.ts +30 -120
  56. package/src/pi/tool/gateway.ts +54 -0
  57. package/src/pi/tool/index.ts +2 -0
  58. package/src/pi/tool/mcp.ts +96 -68
  59. package/src/pi/tool/schedule.ts +41 -19
  60. package/src/pi/tool/skill.ts +126 -420
  61. package/src/pi/tool/subagent.ts +14 -2
  62. package/src/pi/tool/web-fetch.ts +281 -0
  63. package/src/pi/tool/web-search/api.ts +34 -18
  64. package/src/pi/tool/web-search/web-search.ts +0 -1
  65. package/src/pi/tool/workspace-revision.ts +64 -0
  66. package/src/pi/tool/workspace-sandbox.ts +105 -263
  67. package/src/pi/turn/index.ts +20 -0
  68. package/src/pi/turn/interaction.ts +181 -0
  69. package/src/pi/turn/tool-recovery.ts +244 -1
  70. package/src/runtime-agent-context.ts +112 -0
  71. package/src/runtime-agent.ts +568 -321
  72. package/src/{plugins.ts → runtime-assembler.ts} +372 -398
  73. package/src/runtime-definition.ts +175 -0
  74. package/src/runtime.ts +835 -204
  75. package/src/tool-registry.ts +143 -0
  76. package/src/workspace-versioning.ts +46 -0
@@ -13,14 +13,47 @@ import {
13
13
 
14
14
  // #region Public contracts
15
15
 
16
+ /**
17
+ * 声明一个 Tool 的结果由客户端投递,而不是由 `execute()` 产出。
18
+ *
19
+ * 带这个字段的 Tool 会在执行链里 park 住,等宿主经 `respondToolInteraction` 把响应送进来,
20
+ * 再用 `settle` 把响应映射成这次 `toolCallId` 的权威 ToolResult。它的 `execute` 不会被调用。
21
+ *
22
+ * 这是 AI SDK 里「没有 execute 的 user-interaction tool」的等价物;差别是我们 park 住整个 Turn,
23
+ * 而不是让 tool part 悬空到下一次请求 —— 我们的 transcript 是服务端权威的,悬空的 tool_use
24
+ * 会让下一次模型调用非法。
25
+ */
26
+ export interface PiToolInteractionSpec {
27
+ /** 校验客户端响应体;不通过则拒绝投递,park 保持不变。 */
28
+ readonly validateResponse: (response: unknown) => boolean;
29
+ /** 把通过校验的响应映射成 ToolResult;缺省是原样回传。 */
30
+ readonly settle?: (
31
+ input: unknown,
32
+ response: unknown,
33
+ ) => AgentToolResult<unknown>;
34
+ }
35
+
16
36
  /** 描述一个尚未进入最终 Tool Surface 和结算包装的 Pi 工具。 */
17
37
  export interface PiToolCandidate {
18
38
  readonly owner: string;
19
- readonly authorized: boolean;
20
39
  readonly tool: AgentTool<any, any>;
40
+ /** Conservative maximum used in the stable Runtime descriptor. */
21
41
  readonly requiredExecutionLevel: ExecutionLevel;
42
+ /** Trusted parameter-level policy, evaluated before approval or dispatch. */
43
+ readonly requiredExecutionLevelForInput?: (
44
+ input: unknown,
45
+ ) => ExecutionLevel | Promise<ExecutionLevel>;
46
+ /**
47
+ * 无条件需要人来决定:不看执行档位矩阵,每次调用都 park 出审批。
48
+ *
49
+ * 档位差是「够不够格自己跑」,不是「该不该问人」。花用户钱、改变账户
50
+ * 状态这类工具,一旦 Agent 档位被 `allow_level` 永久拉高就会静默执行,
51
+ * 那是失效不是降级。`deny` 仍然优先,被 deny 的工具根本不会注入。
52
+ */
53
+ readonly alwaysRequiresApproval?: boolean;
22
54
  readonly summary?: string;
23
55
  readonly source?: ApprovalReceipt["source"];
56
+ readonly interaction?: PiToolInteractionSpec;
24
57
  }
25
58
 
26
59
  /** 控制候选 Pi 工具如何被编译为可执行工具集。 */
@@ -1,13 +1,8 @@
1
- import {
2
- createCodemodeRuntime,
3
- DynamicWorkerExecutor,
4
- truncateResult,
5
- } from "@cloudflare/codemode";
6
1
  import {
7
2
  createWorkspaceStateBackend,
8
3
  type WorkspaceFsLike,
9
4
  } from "@cloudflare/shell";
10
- import { StateConnector } from "@cloudflare/shell/workers";
5
+ import { createExecuteRuntime } from "@cloudflare/think/tools/execute";
11
6
  import type {
12
7
  RuntimeCodeExecutionPort,
13
8
  WorkspacePort,
@@ -22,7 +17,7 @@ const CODEMODE_SANDBOX_TIMEOUT_MS = 55_000;
22
17
  *
23
18
  * Worker 宿主在具备 Durable Object state 和完整平台绑定时调用,然后把返回值交给 Runtime 工具组装。
24
19
  *
25
- * Cloudflare Codemode 把持久化 Runtime 和无状态 Dynamic Worker executor 分开;这里必须用 StateConnector 显式注入已限定范围的 Workspace,并在返回模型前用官方 `truncateResult` 限制结果。
20
+ * Think 的独立 execute factory 接受显式宿主参数,不要求 Agent 继承 Think;这里只借它组装 Codemode Runtime、Dynamic Worker executor 和已限定范围的 Workspace state connector。
26
21
  */
27
22
  export function createWorkspaceCodeExecutionPort(options: {
28
23
  readonly ctx: DurableObjectState;
@@ -30,26 +25,26 @@ export function createWorkspaceCodeExecutionPort(options: {
30
25
  readonly outbound: Fetcher;
31
26
  readonly workspace: WorkspacePort;
32
27
  }): RuntimeCodeExecutionPort {
33
- const runtime = createCodemodeRuntime({
28
+ const { tool } = createExecuteRuntime({
34
29
  ctx: options.ctx,
35
- executor: new DynamicWorkerExecutor({
36
- loader: options.loader,
37
- globalOutbound: options.outbound,
38
- // 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
39
- timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
40
- }),
41
- connectors: [
42
- new StateConnector(
43
- options.ctx,
44
- createWorkspaceStateBackend(
45
- options.workspace as unknown as WorkspaceFsLike,
46
- ),
47
- ),
48
- ],
30
+ loader: options.loader,
31
+ globalOutbound: options.outbound,
32
+ // 先于外层 60s 截止结束,给 Runtime RPC 结算和 Worker 释放留出时间。
33
+ timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
34
+ state: createWorkspaceStateBackend(
35
+ options.workspace as unknown as WorkspaceFsLike,
36
+ ),
49
37
  name: "execute",
50
- transformResult: truncateResult,
51
38
  });
39
+ const execute = tool.execute;
40
+ if (typeof execute !== "function") {
41
+ throw new Error("Think createExecuteRuntime returned a non-executable tool");
42
+ }
52
43
  return {
53
- execute: (input) => runtime.execute(input),
44
+ execute: (input) => Promise.resolve(execute(input, {
45
+ toolCallId: "execute",
46
+ messages: [],
47
+ context: undefined,
48
+ })),
54
49
  };
55
50
  }
@@ -1,22 +1,21 @@
1
+ import { createQuickActionTools } from "@cloudflare/think/tools/browser";
1
2
  import type {
2
3
  AgentTool,
3
4
  AgentToolResult,
4
5
  } from "@earendil-works/pi-agent-core";
5
6
  import { Type } from "@earendil-works/pi-ai";
6
- import {
7
- browserExtract,
8
- browserLinks,
9
- browserMarkdown,
10
- browserScrape,
11
- type QuickActionPage,
12
- } from "agents/browser";
13
7
  import type {
14
8
  RuntimeBrowserPort,
15
9
  RuntimeCodeExecutionPort,
16
10
  } from "../../kernel/bindings";
17
11
  import { serializeOutput } from "../../lib/artifacts";
18
12
  import type { PiLoadedExtension } from "../assembly/extensions";
13
+ import { aiToolToPi } from "./ai-adapter";
19
14
  import type { PiToolCandidate } from "./compiler";
15
+ import {
16
+ toolRegistryFromPiCandidates,
17
+ type ToolRegistry,
18
+ } from "../../tool-registry";
20
19
 
21
20
  // 本文件沿用 `../../index.ts` 入口定义的 Extension、Port 和 Tool Candidate 术语。
22
21
 
@@ -32,128 +31,32 @@ function result(details: unknown): AgentToolResult<unknown> {
32
31
 
33
32
  // #region Browser Quick Actions
34
33
 
35
- const pageParameters = {
36
- url: Type.Optional(Type.String({ format: "uri" })),
37
- html: Type.Optional(Type.String({ minLength: 1 })),
34
+ const BROWSER_TOOL_LABELS: Readonly<Record<string, string>> = {
35
+ browser_markdown: "Read web page",
36
+ browser_extract: "Extract web data",
37
+ browser_links: "List web links",
38
+ browser_scrape: "Scrape web elements",
38
39
  };
39
- const browserPageParameters = Type.Object(pageParameters);
40
- const browserExtractParameters = Type.Object({
41
- ...pageParameters,
42
- prompt: Type.Optional(Type.String({ minLength: 1 })),
43
- schema: Type.Optional(Type.Unknown()),
44
- });
45
- const browserScrapeParameters = Type.Object({
46
- ...pageParameters,
47
- selectors: Type.Array(Type.String({ minLength: 1 }), {
48
- minItems: 1,
49
- }),
50
- });
51
-
52
- // 把工具输入收窄为 Browser Quick Action 接受的页面来源。
53
- // 四个 Browser Quick Action execute 方法在调用 Agents SDK 前共用它。
54
- // 实际运行时再拒绝空输入,而同时给出 url 和 html 时优先 url,不能把不完整来源传给宿主绑定。
55
- function page(input: {
56
- url?: string;
57
- html?: string;
58
- }): QuickActionPage {
59
- if (input.url) return { url: input.url };
60
- if (input.html) return { html: input.html };
61
- throw new Error("Provide either 'url' or 'html'");
62
- }
63
40
 
64
41
  /**
65
42
  * 为 Cloudflare Browser Run 的四个一次性 Quick Action 创建 Pi 工具候选项。
66
43
  *
67
44
  * Runtime 在宿主提供 Browser port 时调用,模型分别用它读取 Markdown、抽取数据、列出链接或按选择器抓取。
68
45
  *
69
- * Cloudflare 官方将 Quick Actions 定位为只需 browser binding 的无状态单次操作,所以这里直接调用四个宿主函数,不引入额外会话或重试状态。
46
+ * Cloudflare 官方将 Quick Actions 定位为只需 browser binding 的无状态单次操作,这里直接复用官方工厂。
70
47
  */
71
48
  export function browserQuickActionPiToolCandidates(
72
49
  browser: RuntimeBrowserPort,
73
50
  ): PiToolCandidate[] {
74
- const markdown: AgentTool<typeof browserPageParameters> = {
75
- name: "browser_markdown",
76
- label: "Read web page",
77
- description:
78
- "Load a web page (or render raw HTML) and return its content as Markdown. Best for reading articles, docs, or any page as text.",
79
- parameters: browserPageParameters,
80
- // 把 URL 或 HTML 读成 Markdown。
81
- // Pi 工具循环在模型选择 `browser_markdown` 时调用,调用前允许 Turn 取消。
82
- // 取消检查必须放在 Browser binding 调用前,避免已终止 Turn 继续发起页面请求。
83
- async execute(_toolCallId, input, signal) {
84
- signal?.throwIfAborted();
85
- return result(await browserMarkdown(browser, page(input)));
86
- },
87
- };
88
- const extract: AgentTool<typeof browserExtractParameters> = {
89
- name: "browser_extract",
90
- label: "Extract web data",
91
- description:
92
- "Extract structured data from a web page using AI. Describe what you want in 'prompt'. Passing a JSON Schema in 'schema' is strongly recommended — without one the extractor often fails to produce JSON.",
93
- parameters: browserExtractParameters,
94
- // 按自然语言提示或 JSON Schema 从页面抽取数据。
95
- // Pi 工具循环在模型选择 `browser_extract` 时调用,至少需要 prompt 或 schema 之一。
96
- // schema 必须转成 Browser Run 期望的 `json_schema` response_format,而空抽取要求必须在发起请求前失败。
97
- async execute(
98
- _toolCallId,
99
- { prompt, schema, ...input },
100
- signal,
101
- ) {
102
- signal?.throwIfAborted();
103
- if (!prompt && schema === undefined) {
104
- throw new Error("Provide either 'prompt' or 'schema'");
105
- }
106
- return result(await browserExtract(browser, {
107
- ...page(input),
108
- prompt,
109
- response_format: schema === undefined
110
- ? undefined
111
- : { type: "json_schema", schema },
112
- }));
113
- },
114
- };
115
- const links: AgentTool<typeof browserPageParameters> = {
116
- name: "browser_links",
117
- label: "List web links",
118
- description:
119
- "Return every link found on a web page (including ones not visible). Useful for discovering pages to follow.",
120
- parameters: browserPageParameters,
121
- // 列出 URL 或 HTML 页面中的链接。
122
- // Pi 工具循环在模型选择 `browser_links` 时调用,调用前允许 Turn 取消。
123
- // 保留 Agents SDK 的原始结果形状,让统一 result 边界负责文本投影而不在此处二次整形。
124
- async execute(_toolCallId, input, signal) {
125
- signal?.throwIfAborted();
126
- return result(await browserLinks(browser, page(input)));
127
- },
128
- };
129
- const scrape: AgentTool<typeof browserScrapeParameters> = {
130
- name: "browser_scrape",
131
- label: "Scrape web elements",
132
- description:
133
- "Scrape specific elements from a web page by CSS selector. Returns the matched elements' text, HTML, and attributes.",
134
- parameters: browserScrapeParameters,
135
- // 按 CSS 选择器抓取 URL 或 HTML 页面的指定元素。
136
- // Pi 工具循环在模型选择 `browser_scrape` 时调用,并把简化的字符串列表传入。
137
- // Browser Run 需要 `{ selector }` 对象列表,因此这个适配只在边界做一次形状转换。
138
- async execute(
139
- _toolCallId,
140
- { selectors, ...input },
141
- signal,
142
- ) {
143
- signal?.throwIfAborted();
144
- return result(await browserScrape(browser, {
145
- ...page(input),
146
- elements: selectors.map((selector) => ({ selector })),
147
- }));
148
- },
149
- };
150
-
151
- return [markdown, extract, links, scrape].map((tool) => ({
152
- owner: "core:browser",
153
- authorized: true,
154
- requiredExecutionLevel: "low",
155
- tool,
156
- }));
51
+ return Object.entries(createQuickActionTools({ browser })).map(
52
+ ([name, tool]) => ({
53
+ owner: "core:browser",
54
+ requiredExecutionLevel: "low",
55
+ tool: aiToolToPi(name, tool, {
56
+ label: BROWSER_TOOL_LABELS[name] ?? name,
57
+ }),
58
+ }),
59
+ );
157
60
  }
158
61
 
159
62
  // #endregion
@@ -190,7 +93,6 @@ export function listExtensionsPiToolCandidate(
190
93
  };
191
94
  return {
192
95
  owner: "core:extensions",
193
- authorized: true,
194
96
  requiredExecutionLevel: "low",
195
97
  tool,
196
98
  };
@@ -270,7 +172,6 @@ export function codeExecutionPiToolCandidate(
270
172
  };
271
173
  return {
272
174
  owner: "core:codemode",
273
- authorized: true,
274
175
  requiredExecutionLevel: "high",
275
176
  source: "codemode",
276
177
  summary: "Run JavaScript with network and configured connector access",
@@ -278,4 +179,13 @@ export function codeExecutionPiToolCandidate(
278
179
  };
279
180
  }
280
181
 
182
+ /** 从 Codemode Runtime Port 生成 `execute` Tool。 */
183
+ export function createCodeExecutionTool(
184
+ runtime: RuntimeCodeExecutionPort,
185
+ ): ToolRegistry {
186
+ return toolRegistryFromPiCandidates([
187
+ codeExecutionPiToolCandidate(runtime),
188
+ ]);
189
+ }
190
+
281
191
  // #endregion
@@ -0,0 +1,54 @@
1
+ import type { RuntimeGatewaySession } from "../../kernel/bindings";
2
+ import type { PiToolCandidate } from "./compiler";
3
+ import { createPiMcpToolCandidate } from "./mcp";
4
+
5
+ const GATEWAY_TOOLS = [
6
+ "search_capabilities",
7
+ "execute_capability",
8
+ ] as const;
9
+
10
+ /** Builds the two platform Gateway tools from one authenticated logical MCP Session. */
11
+ export function createPiGatewayToolCandidates(
12
+ session: RuntimeGatewaySession,
13
+ ): PiToolCandidate[] {
14
+ const tools = new Map(session.tools.map((tool) => [tool.name, tool]));
15
+ if (
16
+ tools.size !== GATEWAY_TOOLS.length ||
17
+ GATEWAY_TOOLS.some((name) => !tools.has(name))
18
+ ) {
19
+ throw new Error("Connector Gateway returned an invalid MCP catalog");
20
+ }
21
+
22
+ return GATEWAY_TOOLS.map((name) => {
23
+ const tool = tools.get(name)!;
24
+ const execute = name === "execute_capability";
25
+ return createPiMcpToolCandidate(
26
+ { ...tool, serverId: "gateway" },
27
+ (input, signal) => session.callTool(name, input, signal),
28
+ {
29
+ owner: "gateway",
30
+ modelName: name,
31
+ requiredExecutionLevel: execute ? "high" : "safe",
32
+ ...(execute
33
+ ? {
34
+ requiredExecutionLevelForInput: async (input: unknown) => {
35
+ const reference = input && typeof input === "object" &&
36
+ !Array.isArray(input)
37
+ ? (input as Record<string, unknown>).name
38
+ : undefined;
39
+ if (typeof reference !== "string" || !reference) {
40
+ throw new Error(
41
+ "Gateway Capability Reference is not verified",
42
+ );
43
+ }
44
+ return (await session.resolveCapabilityRisk(reference)) ===
45
+ "none"
46
+ ? "safe" as const
47
+ : "high" as const;
48
+ },
49
+ }
50
+ : {}),
51
+ },
52
+ );
53
+ });
54
+ }
@@ -24,9 +24,11 @@ export * from "./base";
24
24
  export * from "./compiler";
25
25
  export * from "./core";
26
26
  export * from "./core-host";
27
+ export * from "./gateway";
27
28
  export * from "./mcp";
28
29
  export * from "./schedule";
29
30
  export * from "./skill";
30
31
  export * from "./subagent";
31
32
  export * from "./workspace-sandbox";
33
+ export * from "./workspace-revision";
32
34
  export * from "./web-search";
@@ -9,6 +9,22 @@ interface McpCallResult {
9
9
  isError?: boolean;
10
10
  }
11
11
 
12
+ export interface PiDiscoveredMcpTool {
13
+ readonly serverId: string;
14
+ readonly name: string;
15
+ readonly title?: string;
16
+ readonly description?: string;
17
+ readonly inputSchema?: unknown;
18
+ readonly annotations?: { readonly title?: string } & Record<string, unknown>;
19
+ }
20
+
21
+ interface PiMcpCandidatePolicy {
22
+ readonly owner: string;
23
+ readonly modelName: string;
24
+ readonly requiredExecutionLevel: PiToolCandidate["requiredExecutionLevel"];
25
+ readonly requiredExecutionLevelForInput?: PiToolCandidate["requiredExecutionLevelForInput"];
26
+ }
27
+
12
28
  /**
13
29
  * Pi 的 MCP 适配器所需的最小 Host 能力。
14
30
  *
@@ -43,7 +59,8 @@ export interface PiMcpHost {
43
59
  * 参数和中止信号必须原样交给 Agents SDK,避免另造一套传输生命周期。
44
60
  */
45
61
  callTool(
46
- ...args: Parameters<MCPClientManager["callTool"]>
62
+ params: Parameters<MCPClientManager["callTool"]>[0],
63
+ options?: { signal?: AbortSignal },
47
64
  ): Promise<unknown>;
48
65
  };
49
66
  }
@@ -200,16 +217,76 @@ function errorMessage(result: McpCallResult): string {
200
217
  : "MCP tool call failed";
201
218
  }
202
219
 
220
+ /** 共享 MCP 结果投影;Gateway 与用户配置的 Remote MCP 均经过此边界。 */
221
+ export function createPiMcpToolCandidate(
222
+ mcpTool: PiDiscoveredMcpTool,
223
+ callTool: (
224
+ input: Readonly<Record<string, unknown>>,
225
+ signal?: AbortSignal,
226
+ ) => Promise<unknown>,
227
+ policy: PiMcpCandidatePolicy,
228
+ ): PiToolCandidate {
229
+ const label =
230
+ mcpTool.title ??
231
+ mcpTool.annotations?.title ??
232
+ mcpTool.name;
233
+ const tool: AgentTool<any, {
234
+ kind: "mcp";
235
+ toolName: string;
236
+ }> = {
237
+ name: policy.modelName,
238
+ label,
239
+ description: mcpTool.description ?? label,
240
+ parameters: structuredClone(
241
+ mcpTool.inputSchema ?? {
242
+ type: "object",
243
+ additionalProperties: true,
244
+ },
245
+ ) as AgentTool["parameters"],
246
+ execute: async (_toolCallId, args, signal) => {
247
+ const result = normalizeCallResult(
248
+ await callTool(args as Record<string, unknown>, signal),
249
+ );
250
+ if (result.isError) throw new Error(errorMessage(result));
251
+ const structuredContent = result.structuredContent === undefined
252
+ ? undefined
253
+ : publicMcpValue(result.structuredContent);
254
+ const content = resultContent({ ...result, structuredContent });
255
+ return {
256
+ content,
257
+ details: {
258
+ kind: "mcp",
259
+ toolName: mcpTool.name,
260
+ output: structuredContent ?? content,
261
+ },
262
+ };
263
+ },
264
+ };
265
+ return {
266
+ owner: policy.owner,
267
+ tool,
268
+ summary: label,
269
+ requiredExecutionLevel: policy.requiredExecutionLevel,
270
+ ...(policy.requiredExecutionLevelForInput
271
+ ? {
272
+ requiredExecutionLevelForInput:
273
+ policy.requiredExecutionLevelForInput,
274
+ }
275
+ : {}),
276
+ source: "action",
277
+ };
278
+ }
279
+
203
280
  /**
204
- * 把当前已连接且获授权的 MCP 工具转换成 Pi 候选工具。
281
+ * 把当前已配置且 ready 的 MCP 工具转换成 Pi 候选工具。
205
282
  *
206
283
  * Tool Surface 在 Runtime 准备时调用它。调用方应传入
207
284
  * Agent Host,以及同一 Runtime 快照中的 MCP 配置;未就绪、未配置或重复 URL
208
285
  * 的连接不会进入模型工具目录。
209
286
  *
210
287
  * “候选工具”是进入 Pi 编译和统一治理前的内部描述。连接、凭据和恢复由 Host
211
- * 与 Agents SDK 管理;本函数只做授权快照过滤、Schema 转接和公开结果投影。
212
- * 远端 Server 提供的 annotations 只是提示,不是本地授权边界,所以所有动态
288
+ * 与 Agents SDK 管理;本函数只做动态连接状态过滤、Schema 转接和公开结果投影。
289
+ * 远端 Server 提供的 annotations 只是提示,不是本地审批边界,所以所有动态
213
290
  * MCP 工具继续标为高风险,不能据此绕过审批。
214
291
  *
215
292
  * `Host`、`Pi`、`Runtime Snapshot` 与“候选工具”见 `../../index.ts`。
@@ -249,71 +326,22 @@ export function createPiMcpToolCandidates(
249
326
  state: "ready",
250
327
  })
251
328
  .filter((tool) => selectedIds.has(tool.serverId))
252
- .map((mcpTool): PiToolCandidate => {
253
- const label =
254
- mcpTool.title ??
255
- mcpTool.annotations?.title ??
256
- mcpTool.name;
257
- const tool: AgentTool<any, {
258
- kind: "mcp";
259
- toolName: string;
260
- }> = {
261
- name: modelVisibleName(mcpTool.serverId, mcpTool.name),
262
- label,
263
- description: mcpTool.description ?? label,
264
- parameters: structuredClone(
265
- mcpTool.inputSchema ?? {
266
- type: "object",
267
- additionalProperties: true,
268
- },
269
- ) as AgentTool["parameters"],
270
- // 作用:执行模型选中的远端 MCP 工具并返回公开结果。
271
- // 调用:Pi 在参数校验和统一工具治理通过后调用。
272
- // 原因:调用交给 Agents SDK,并在边界处统一处理错误、
273
- // 私有元数据和非原生内容。
274
- execute: async (_toolCallId, args, signal) => {
275
- const result = normalizeCallResult(
276
- await host.mcp.callTool(
277
- {
278
- serverId: mcpTool.serverId,
279
- name: mcpTool.name,
280
- arguments: args as Record<string, unknown>,
281
- },
282
- undefined,
283
- { signal },
284
- ),
285
- );
286
- if (result.isError) {
287
- throw new Error(errorMessage(result));
288
- }
289
- const structuredContent =
290
- result.structuredContent === undefined
291
- ? undefined
292
- : publicMcpValue(result.structuredContent);
293
- const content = resultContent({
294
- ...result,
295
- structuredContent,
296
- });
297
- return {
298
- content,
299
- details: {
300
- kind: "mcp",
301
- toolName: mcpTool.name,
302
- output: structuredContent ?? content,
303
- },
304
- };
329
+ .map((mcpTool) => createPiMcpToolCandidate(
330
+ mcpTool,
331
+ (args, signal) => host.mcp.callTool(
332
+ {
333
+ serverId: mcpTool.serverId,
334
+ name: mcpTool.name,
335
+ arguments: args,
305
336
  },
306
- };
307
- return {
337
+ { signal },
338
+ ),
339
+ {
308
340
  owner: `mcp:${mcpTool.serverId}`,
309
- authorized: true,
310
- tool,
311
- summary: label,
312
- // MCP annotations are supplied by the remote server and are not an
313
- // authorization boundary. Until the Host supplies trusted per-method
314
- // policy, every dynamic MCP method must take the approval path.
341
+ modelName: modelVisibleName(mcpTool.serverId, mcpTool.name),
342
+ // Remote MCP annotations are untrusted; all user-configured methods
343
+ // retain the existing high-risk approval policy.
315
344
  requiredExecutionLevel: "high",
316
- source: "action",
317
- };
318
- });
345
+ },
346
+ ));
319
347
  }
@@ -10,6 +10,10 @@ import type {
10
10
  import type { ScheduleSpec } from "../../kernel/receipts";
11
11
  import { serializeOutput } from "../../lib/artifacts";
12
12
  import type { PiToolCandidate } from "./compiler";
13
+ import {
14
+ toolRegistryFromPiCandidates,
15
+ type ToolRegistry,
16
+ } from "../../tool-registry";
13
17
 
14
18
  const scheduleTriggerParameters = Type.Union([
15
19
  Type.Object({
@@ -90,15 +94,20 @@ function result<T>(details: T): AgentToolResult<T> {
90
94
  function candidate<T extends TSchema>(
91
95
  tool: AgentTool<T>,
92
96
  options: Partial<
93
- Pick<PiToolCandidate, "owner" | "requiredExecutionLevel" | "summary">
97
+ Pick<
98
+ PiToolCandidate,
99
+ "alwaysRequiresApproval" | "owner" | "requiredExecutionLevel" | "summary"
100
+ >
94
101
  > = {},
95
102
  ): PiToolCandidate {
96
103
  return {
97
104
  owner: options.owner ?? "runtime-base",
98
- authorized: true,
99
105
  requiredExecutionLevel: options.requiredExecutionLevel ?? "safe",
100
106
  source: "action",
101
107
  tool,
108
+ ...(options.alwaysRequiresApproval
109
+ ? { alwaysRequiresApproval: true }
110
+ : {}),
102
111
  ...(options.summary ? { summary: options.summary } : {}),
103
112
  };
104
113
  }
@@ -107,24 +116,30 @@ export function schedulePiToolCandidates(
107
116
  schedule: RuntimeSchedulePort,
108
117
  ): PiToolCandidate[] {
109
118
  return [
110
- candidate({
111
- name: "schedule",
112
- label: "Schedule prompt",
113
- description:
114
- "Schedule a prompt to run LATER, autonomously. Use when the user asks to be reminded, or to run something after a delay / at a time / on a recurring basis. The scheduled prompt runs in its own dedicated session at each trigger with NO memory of this conversation, so phrase `prompt` as a fully standalone instruction. After scheduling, tell the user what you set.",
115
- parameters: scheduleParameters,
116
- async execute(_toolCallId, input, signal) {
117
- signal?.throwIfAborted();
118
- const spec: ScheduleSpec = {
119
- trigger: input.trigger,
120
- job: { kind: "prompt", prompt: input.prompt },
121
- label: input.label,
122
- ...(input.timezone ? { tz: input.timezone } : {}),
123
- };
124
- const { id } = await schedule.create(spec);
125
- return result({ scheduled: true, id });
119
+ candidate(
120
+ {
121
+ name: "schedule",
122
+ label: "Schedule prompt",
123
+ description:
124
+ "Schedule a prompt to run LATER, autonomously. Use when the user asks to be reminded, or to run something after a delay / at a time / on a recurring basis. The user always reviews the proposed schedule before it is created. The scheduled prompt runs in its own dedicated session at each trigger with NO memory of this conversation, so phrase `prompt` as a fully standalone instruction. After scheduling, tell the user what you set.",
125
+ parameters: scheduleParameters,
126
+ async execute(_toolCallId, input, signal) {
127
+ signal?.throwIfAborted();
128
+ const spec: ScheduleSpec = {
129
+ trigger: input.trigger,
130
+ job: { kind: "prompt", prompt: input.prompt },
131
+ label: input.label,
132
+ ...(input.timezone ? { tz: input.timezone } : {}),
133
+ };
134
+ const { id } = await schedule.create(spec);
135
+ return result({ scheduled: true, id });
136
+ },
126
137
  },
127
- }),
138
+ {
139
+ alwaysRequiresApproval: true,
140
+ summary: "Create a scheduled task",
141
+ },
142
+ ),
128
143
  candidate({
129
144
  name: "list_schedules",
130
145
  label: "List schedules",
@@ -240,3 +255,10 @@ export function schedulePiToolCandidates(
240
255
  ),
241
256
  ];
242
257
  }
258
+
259
+ /** 从 {@link RuntimeSchedulePort} 生成定时任务 Tool 集。 */
260
+ export function createScheduleTools(
261
+ schedule: RuntimeSchedulePort,
262
+ ): ToolRegistry {
263
+ return toolRegistryFromPiCandidates(schedulePiToolCandidates(schedule));
264
+ }