@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,251 @@
1
+ import type {
2
+ AgentTool,
3
+ AgentToolResult,
4
+ } from "@earendil-works/pi-agent-core";
5
+ import { Type } from "@earendil-works/pi-ai";
6
+ import type { CodemodeRuntimeHandle } from "@cloudflare/codemode";
7
+ import {
8
+ browserExtract,
9
+ browserLinks,
10
+ browserMarkdown,
11
+ browserScrape,
12
+ type QuickActionPage,
13
+ } from "agents/browser";
14
+ import type { RuntimeBrowserPort } from "../../kernel/bindings";
15
+ import { serializeOutput } from "../../lib/artifacts";
16
+ import type { PiLoadedExtension } from "../assembly/extensions";
17
+ import type { PiToolCandidate } from "./compiler";
18
+
19
+ // 本文件沿用 `../../index.ts` 入口定义的 Extension、Port 和 Tool Candidate 术语。
20
+
21
+ // 把核心工具的结构化值投影为 Pi 同时需要的文本和 details。
22
+ // 本文件中所有核心工具 execute 方法在返回宿主结果时调用它。
23
+ // 统一使用 `serializeOutput()` 可以保持模型可见文本和持久化 details 来自同一个值。
24
+ function result(details: unknown): AgentToolResult<unknown> {
25
+ return {
26
+ content: [{ type: "text", text: serializeOutput(details).text }],
27
+ details,
28
+ };
29
+ }
30
+
31
+ // #region Browser Quick Actions
32
+
33
+ const pageParameters = {
34
+ url: Type.Optional(Type.String({ format: "uri" })),
35
+ html: Type.Optional(Type.String({ minLength: 1 })),
36
+ };
37
+ const browserPageParameters = Type.Object(pageParameters);
38
+ const browserExtractParameters = Type.Object({
39
+ ...pageParameters,
40
+ prompt: Type.Optional(Type.String({ minLength: 1 })),
41
+ schema: Type.Optional(Type.Unknown()),
42
+ });
43
+ const browserScrapeParameters = Type.Object({
44
+ ...pageParameters,
45
+ selectors: Type.Array(Type.String({ minLength: 1 }), {
46
+ minItems: 1,
47
+ }),
48
+ });
49
+
50
+ // 把工具输入收窄为 Browser Quick Action 接受的页面来源。
51
+ // 四个 Browser Quick Action execute 方法在调用 Agents SDK 前共用它。
52
+ // 实际运行时再拒绝空输入,而同时给出 url 和 html 时优先 url,不能把不完整来源传给宿主绑定。
53
+ function page(input: {
54
+ url?: string;
55
+ html?: string;
56
+ }): QuickActionPage {
57
+ if (input.url) return { url: input.url };
58
+ if (input.html) return { html: input.html };
59
+ throw new Error("Provide either 'url' or 'html'");
60
+ }
61
+
62
+ /**
63
+ * 为 Cloudflare Browser Run 的四个一次性 Quick Action 创建 Pi 工具候选项。
64
+ *
65
+ * Runtime 在宿主提供 Browser port 时调用,模型分别用它读取 Markdown、抽取数据、列出链接或按选择器抓取。
66
+ *
67
+ * Cloudflare 官方将 Quick Actions 定位为只需 browser binding 的无状态单次操作,所以这里直接调用四个宿主函数,不引入额外会话或重试状态。
68
+ */
69
+ export function browserQuickActionPiToolCandidates(
70
+ browser: RuntimeBrowserPort,
71
+ ): PiToolCandidate[] {
72
+ const markdown: AgentTool<typeof browserPageParameters> = {
73
+ name: "browser_markdown",
74
+ label: "Read web page",
75
+ description:
76
+ "Load a web page (or render raw HTML) and return its content as Markdown. Best for reading articles, docs, or any page as text.",
77
+ parameters: browserPageParameters,
78
+ // 把 URL 或 HTML 读成 Markdown。
79
+ // Pi 工具循环在模型选择 `browser_markdown` 时调用,调用前允许 Turn 取消。
80
+ // 取消检查必须放在 Browser binding 调用前,避免已终止 Turn 继续发起页面请求。
81
+ async execute(_toolCallId, input, signal) {
82
+ signal?.throwIfAborted();
83
+ return result(await browserMarkdown(browser, page(input)));
84
+ },
85
+ };
86
+ const extract: AgentTool<typeof browserExtractParameters> = {
87
+ name: "browser_extract",
88
+ label: "Extract web data",
89
+ description:
90
+ "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.",
91
+ parameters: browserExtractParameters,
92
+ // 按自然语言提示或 JSON Schema 从页面抽取数据。
93
+ // Pi 工具循环在模型选择 `browser_extract` 时调用,至少需要 prompt 或 schema 之一。
94
+ // schema 必须转成 Browser Run 期望的 `json_schema` response_format,而空抽取要求必须在发起请求前失败。
95
+ async execute(
96
+ _toolCallId,
97
+ { prompt, schema, ...input },
98
+ signal,
99
+ ) {
100
+ signal?.throwIfAborted();
101
+ if (!prompt && schema === undefined) {
102
+ throw new Error("Provide either 'prompt' or 'schema'");
103
+ }
104
+ return result(await browserExtract(browser, {
105
+ ...page(input),
106
+ prompt,
107
+ response_format: schema === undefined
108
+ ? undefined
109
+ : { type: "json_schema", schema },
110
+ }));
111
+ },
112
+ };
113
+ const links: AgentTool<typeof browserPageParameters> = {
114
+ name: "browser_links",
115
+ label: "List web links",
116
+ description:
117
+ "Return every link found on a web page (including ones not visible). Useful for discovering pages to follow.",
118
+ parameters: browserPageParameters,
119
+ // 列出 URL 或 HTML 页面中的链接。
120
+ // Pi 工具循环在模型选择 `browser_links` 时调用,调用前允许 Turn 取消。
121
+ // 保留 Agents SDK 的原始结果形状,让统一 result 边界负责文本投影而不在此处二次整形。
122
+ async execute(_toolCallId, input, signal) {
123
+ signal?.throwIfAborted();
124
+ return result(await browserLinks(browser, page(input)));
125
+ },
126
+ };
127
+ const scrape: AgentTool<typeof browserScrapeParameters> = {
128
+ name: "browser_scrape",
129
+ label: "Scrape web elements",
130
+ description:
131
+ "Scrape specific elements from a web page by CSS selector. Returns the matched elements' text, HTML, and attributes.",
132
+ parameters: browserScrapeParameters,
133
+ // 按 CSS 选择器抓取 URL 或 HTML 页面的指定元素。
134
+ // Pi 工具循环在模型选择 `browser_scrape` 时调用,并把简化的字符串列表传入。
135
+ // Browser Run 需要 `{ selector }` 对象列表,因此这个适配只在边界做一次形状转换。
136
+ async execute(
137
+ _toolCallId,
138
+ { selectors, ...input },
139
+ signal,
140
+ ) {
141
+ signal?.throwIfAborted();
142
+ return result(await browserScrape(browser, {
143
+ ...page(input),
144
+ elements: selectors.map((selector) => ({ selector })),
145
+ }));
146
+ },
147
+ };
148
+
149
+ return [markdown, extract, links, scrape].map((tool) => ({
150
+ owner: "core:browser",
151
+ authorized: true,
152
+ requiredExecutionLevel: "low",
153
+ tool,
154
+ }));
155
+ }
156
+
157
+ // #endregion
158
+
159
+ // #region Extension discovery
160
+
161
+ const noParameters = Type.Object({});
162
+
163
+ /**
164
+ * 为当前已加载 Extension 列表创建一个 Pi 工具候选项。
165
+ *
166
+ * Runtime 组装层在 Extension 装载完成后调用,模型需要发现当前可用扩展时使用该工具。
167
+ *
168
+ * 候选项捕获准备阶段的只读列表,这与 pinned Runtime 组装保持一致,不能在执行时重新扫描出另一份 Extension 集合。
169
+ */
170
+ export function listExtensionsPiToolCandidate(
171
+ extensions: readonly PiLoadedExtension[],
172
+ ): PiToolCandidate {
173
+ const tool: AgentTool<typeof noParameters> = {
174
+ name: "list_extensions",
175
+ label: "List extensions",
176
+ description: "List all currently loaded extensions and their tools.",
177
+ parameters: noParameters,
178
+ // 返回这个 pinned Runtime 已加载的 Extension 快照。
179
+ // Pi 工具循环在模型选择 `list_extensions` 时调用,调用前允许 Turn 取消。
180
+ // 同时返回 count 和原列表,使模型既能快速判空又不丢失 Extension 细节。
181
+ async execute(_toolCallId, _input, signal) {
182
+ signal?.throwIfAborted();
183
+ return result({
184
+ count: extensions.length,
185
+ extensions,
186
+ });
187
+ },
188
+ };
189
+ return {
190
+ owner: "core:extensions",
191
+ authorized: true,
192
+ requiredExecutionLevel: "low",
193
+ tool,
194
+ };
195
+ }
196
+
197
+ // #endregion
198
+
199
+ // #region Code Mode
200
+
201
+ const executeParameters = Type.Object({
202
+ code: Type.String({
203
+ minLength: 1,
204
+ description:
205
+ "Plain JavaScript async function. TypeScript annotations are not supported.",
206
+ }),
207
+ });
208
+
209
+ /**
210
+ * 把 Cloudflare Codemode Runtime handle 包装为高风险 Pi 代码执行工具候选项。
211
+ *
212
+ * Worker 宿主用 `workspaceCodeExecutionPiToolCandidate()` 组装好持久化 Codemode Runtime 后调用,模型再通过 `execute` 运行代码。
213
+ *
214
+ * Codemode 代码可访问出站网络和已配置 connector,因此必须保留 high 档和 codemode source 标记,不能降级成普通本地计算工具。
215
+ */
216
+ export function codeExecutionPiToolCandidate(
217
+ runtime: Pick<CodemodeRuntimeHandle, "execute">,
218
+ ): PiToolCandidate {
219
+ const tool: AgentTool<typeof executeParameters> = {
220
+ name: "execute",
221
+ label: "Execute JavaScript",
222
+ description:
223
+ "Run JavaScript in a short-lived Code Mode Dynamic Worker and return a value. This is your " +
224
+ "raw-network and tool-composition instrument, not a general web-search tool. Call fetch directly " +
225
+ "when you need a raw response, custom headers/method/body, a structured API, or content that " +
226
+ "web_search could not retrieve. Check res.status; when several known endpoints are necessary, " +
227
+ "fetch them in one invocation. Also available: " +
228
+ "state.* for your workspace filesystem (readFile/writeFile/glob/searchFiles/replaceInFiles, " +
229
+ "each taking one object argument), and codemode.step(name, fn) to run a side-effecting block " +
230
+ "exactly once so it survives replay. Write plain JavaScript — TypeScript type annotations are " +
231
+ "a syntax error. Use return for the result and console.log for notes.",
232
+ parameters: executeParameters,
233
+ // 把模型提供的 JavaScript 交给 Codemode Runtime 执行。
234
+ // Pi 工具循环在模型选择 `execute` 时调用,调用前允许 Turn 取消。
235
+ // 必须通过 Runtime handle 而不是直接调用 executor,因为 Cloudflare Codemode 把重放、审批和执行日志放在持久化 Runtime 层。
236
+ async execute(_toolCallId, input, signal) {
237
+ signal?.throwIfAborted();
238
+ return result(await runtime.execute(input));
239
+ },
240
+ };
241
+ return {
242
+ owner: "core:codemode",
243
+ authorized: true,
244
+ requiredExecutionLevel: "high",
245
+ source: "codemode",
246
+ summary: "Run JavaScript with network and configured connector access",
247
+ tool,
248
+ };
249
+ }
250
+
251
+ // #endregion
@@ -0,0 +1,32 @@
1
+ import {
2
+ compilePiTools,
3
+ createPiToolGovernance,
4
+ } from "./compiler";
5
+ import { listExtensionsPiToolCandidate } from "./core";
6
+ import { createPiMcpToolCandidates } from "./mcp";
7
+
8
+ /**
9
+ * 集中暴露 Pi 工具候选项的组装与治理入口。
10
+ *
11
+ * Runtime 组装层在收集内建、Extension 和 MCP 工具时使用,具体调用约定由各导出函数的 TSDoc 说明。
12
+ *
13
+ * 这里只是稳定的薄汇总,不复制工具注册状态;Tool Candidate、Tool Settlement 和 Governance 等核心术语统一见 `../../index.ts` 入口说明。
14
+ */
15
+ export const piTools = Object.freeze({
16
+ compile: compilePiTools,
17
+ createGovernance: createPiToolGovernance,
18
+ createMcpCandidates: createPiMcpToolCandidates,
19
+ listExtensionsCandidate: listExtensionsPiToolCandidate,
20
+ });
21
+
22
+ export * from "./ai-adapter";
23
+ export * from "./base";
24
+ export * from "./compiler";
25
+ export * from "./core";
26
+ export * from "./core-host";
27
+ export * from "./mcp";
28
+ export * from "./schedule";
29
+ export * from "./skill";
30
+ export * from "./subagent";
31
+ export * from "./workspace-sandbox";
32
+ export * from "./web-search";
@@ -0,0 +1,319 @@
1
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
2
+ import type { MCPClientManager } from "agents/mcp/client";
3
+ import type { RuntimeMcpServer } from "../../kernel/profile";
4
+ import type { PiToolCandidate } from "./compiler";
5
+
6
+ interface McpCallResult {
7
+ content: unknown[];
8
+ structuredContent?: unknown;
9
+ isError?: boolean;
10
+ }
11
+
12
+ /**
13
+ * Pi 的 MCP 适配器所需的最小 Host 能力。
14
+ *
15
+ * `PreparedAssembly` 在收集本轮工具时传入真正的 Agent Host;其他调用方只应
16
+ * 提供同样的服务器状态快照、工具目录和调用能力,不要在这里复制连接状态。
17
+ *
18
+ * 这里沿用 MCP 的官方术语:Host 是承载 Agent 的应用,Client 是 Host 内连接
19
+ * 单个 Server 的客户端,Server 是对外提供工具的服务。连接持久化、OAuth 和
20
+ * 恢复仍由 Agents SDK 的 MCP Client 管理,本接口只读取和调用。
21
+ *
22
+ * `Host` 与 `Pi` 等项目核心术语见 `../../index.ts`。
23
+ */
24
+ export interface PiMcpHost {
25
+ /**
26
+ * 返回 Agents SDK 当前保存的 MCP 服务器及连接状态。
27
+ *
28
+ * `createPiMcpToolCandidates` 在收集本轮工具时调用;调用方应返回 SDK 的当前
29
+ * 快照,不要另存一份状态,否则恢复或断线后可能继续暴露旧目录。
30
+ */
31
+ getMcpServers(): {
32
+ servers: Record<string, {
33
+ server_url: string;
34
+ state: string;
35
+ }>;
36
+ };
37
+ /** 提供原始工具发现,以及对指定 MCP Server 的一次工具调用。 */
38
+ readonly mcp: Pick<MCPClientManager, "listTools"> & {
39
+ /**
40
+ * 调用一个已发现的 MCP 工具。
41
+ *
42
+ * `createPiMcpToolCandidates` 生成的 `execute` 方法在模型选中工具后调用它;
43
+ * 参数和中止信号必须原样交给 Agents SDK,避免另造一套传输生命周期。
44
+ */
45
+ callTool(
46
+ ...args: Parameters<MCPClientManager["callTool"]>
47
+ ): Promise<unknown>;
48
+ };
49
+ }
50
+
51
+ // 作用:给一个远端 MCP 工具生成 Pi 模型可见的稳定名称。
52
+ // 调用:候选工厂在映射每个已发现工具时调用。
53
+ // 原因:服务器前缀隔离同名工具,去掉连字符则保持现有 Pi 工具名格式。
54
+ function modelVisibleName(serverId: string, toolName: string): string {
55
+ return `tool_${serverId.replace(/-/g, "")}_${toolName}`;
56
+ }
57
+
58
+ // 作用:把任意 MCP 返回值尽量转换成可展示文本。
59
+ // 调用:结果归一化和内容投影遇到非文本值时调用。
60
+ // 原因:JSON 优先保留结构;循环引用等不可序列化值再退回 String,
61
+ // 避免遮蔽原结果。
62
+ function safeText(value: unknown): string {
63
+ if (typeof value === "string") return value;
64
+ try {
65
+ return JSON.stringify(value) ?? String(value);
66
+ } catch {
67
+ return String(value);
68
+ }
69
+ }
70
+
71
+ // 作用:把 Agents SDK 的未知调用结果收敛为本文件处理的统一结构。
72
+ // 调用:MCP 工具的 execute 在 callTool 返回后立即调用。
73
+ // 原因:不同 Server 可能返回标准 content 或兼容层的 toolResult,
74
+ // 先收口可让后续只走一条投影路径。
75
+ function normalizeCallResult(value: unknown): McpCallResult {
76
+ if (typeof value !== "object" || value === null) {
77
+ return { content: [{ type: "text", text: safeText(value) }] };
78
+ }
79
+ const record = value as Record<string, unknown>;
80
+ if (Array.isArray(record.content)) {
81
+ return {
82
+ content: record.content,
83
+ structuredContent: record.structuredContent,
84
+ isError: record.isError === true,
85
+ };
86
+ }
87
+ return {
88
+ content: [{
89
+ type: "text",
90
+ text: safeText(record.toolResult ?? value),
91
+ }],
92
+ isError: record.isError === true,
93
+ };
94
+ }
95
+
96
+ // 作用:递归删除 MCP 结果中只供 Host 使用的 `_meta` 字段。
97
+ // 调用:结构化结果和非原生内容块公开给模型前调用。
98
+ // 原因:现有回归测试覆盖 `_meta` 可能携带凭据,
99
+ // 不能把 Host 私有元数据写进对话记录。
100
+ function publicMcpValue(value: unknown): unknown {
101
+ if (Array.isArray(value)) {
102
+ return value.map(publicMcpValue);
103
+ }
104
+ if (typeof value !== "object" || value === null) {
105
+ return value;
106
+ }
107
+ return Object.fromEntries(
108
+ Object.entries(value as Record<string, unknown>)
109
+ .filter(([key]) => key !== "_meta")
110
+ .map(([key, entry]) => [key, publicMcpValue(entry)]),
111
+ );
112
+ }
113
+
114
+ // 作用:把一个非原生 MCP 内容块裁成可公开给模型的值。
115
+ // 调用:resultContent 处理文本和图片以外的内容块时调用。
116
+ // 原因:除递归移除 `_meta` 外还去掉 Server 提供的 annotations,
117
+ // 避免把提示性元数据当成执行结果。
118
+ function publicMcpPart(part: unknown): unknown {
119
+ const sanitized = publicMcpValue(part);
120
+ if (
121
+ typeof sanitized !== "object" ||
122
+ sanitized === null ||
123
+ Array.isArray(sanitized)
124
+ ) {
125
+ return sanitized;
126
+ }
127
+ const {
128
+ annotations: _annotations,
129
+ ...publicPart
130
+ } = sanitized as Record<string, unknown>;
131
+ return publicPart;
132
+ }
133
+
134
+ // 作用:把 MCP 内容转换为 Pi 能直接返回的文本或图片列表。
135
+ // 调用:每次 MCP 调用成功后由工具 execute 调用。
136
+ // 原因:原生保留文本和图片,其余类型安全转成文本;
137
+ // 空 content 时仍要返回结构化结果或明确成功消息。
138
+ function resultContent(result: McpCallResult): Array<
139
+ | { type: "text"; text: string }
140
+ | { type: "image"; data: string; mimeType: string }
141
+ > {
142
+ const content: ReturnType<typeof resultContent> = [];
143
+ for (const part of result.content ?? []) {
144
+ if (
145
+ typeof part === "object" &&
146
+ part !== null &&
147
+ "type" in part &&
148
+ part.type === "text" &&
149
+ "text" in part &&
150
+ typeof part.text === "string"
151
+ ) {
152
+ content.push({ type: "text", text: part.text });
153
+ continue;
154
+ }
155
+ if (
156
+ typeof part === "object" &&
157
+ part !== null &&
158
+ "type" in part &&
159
+ part.type === "image" &&
160
+ "data" in part &&
161
+ typeof part.data === "string" &&
162
+ "mimeType" in part &&
163
+ typeof part.mimeType === "string"
164
+ ) {
165
+ content.push({
166
+ type: "image",
167
+ data: part.data,
168
+ mimeType: part.mimeType,
169
+ });
170
+ continue;
171
+ }
172
+ content.push({
173
+ type: "text",
174
+ text: safeText(publicMcpPart(part)),
175
+ });
176
+ }
177
+ if (content.length > 0) return content;
178
+ if (result.structuredContent !== undefined) {
179
+ return [{
180
+ type: "text",
181
+ text: safeText(result.structuredContent),
182
+ }];
183
+ }
184
+ return [{ type: "text", text: "MCP tool completed successfully" }];
185
+ }
186
+
187
+ // 作用:从 MCP 的失败结果中提取一条稳定、可读的错误消息。
188
+ // 调用:工具 execute 发现 isError 为 true 时调用。
189
+ // 原因:优先保留 Server 返回的首条文本,否则使用固定兜底,
190
+ // 避免抛出无信息的对象。
191
+ function errorMessage(result: McpCallResult): string {
192
+ const first = result.content?.[0];
193
+ return typeof first === "object" &&
194
+ first !== null &&
195
+ "type" in first &&
196
+ first.type === "text" &&
197
+ "text" in first &&
198
+ typeof first.text === "string"
199
+ ? first.text
200
+ : "MCP tool call failed";
201
+ }
202
+
203
+ /**
204
+ * 把当前已连接且获授权的 MCP 工具转换成 Pi 候选工具。
205
+ *
206
+ * `PreparedAssembly.toolCandidates()` 在每次收集本轮工具时调用它。调用方应传入
207
+ * Agent Host,以及同一 Runtime 快照中的 MCP 配置;未就绪、未配置或重复 URL
208
+ * 的连接不会进入模型工具目录。
209
+ *
210
+ * “候选工具”是进入 Pi 编译和统一治理前的内部描述。连接、凭据和恢复由 Host
211
+ * 与 Agents SDK 管理;本函数只做授权快照过滤、Schema 转接和公开结果投影。
212
+ * 远端 Server 提供的 annotations 只是提示,不是本地授权边界,所以所有动态
213
+ * MCP 工具继续标为高风险,不能据此绕过审批。
214
+ *
215
+ * `Host`、`Pi`、`Runtime Snapshot` 与“候选工具”见 `../../index.ts`。
216
+ */
217
+ export function createPiMcpToolCandidates(
218
+ host: PiMcpHost,
219
+ configured: readonly RuntimeMcpServer[],
220
+ ): PiToolCandidate[] {
221
+ // TODO(待确认):Runtime 配置目前只 trim URL,而 agents@0.19.0 会用
222
+ // new URL(url).href 保存连接地址;`https://x` 与 `https://x/`
223
+ // 可能因此精确匹配失败。
224
+ const configuredUrls = new Set(
225
+ configured.map((server) => server.url),
226
+ );
227
+ const selectedUrls = new Set<string>();
228
+ const selectedServerIds: string[] = [];
229
+
230
+ for (const [serverId, server] of Object.entries(
231
+ host.getMcpServers().servers,
232
+ )) {
233
+ if (
234
+ server.state !== "ready" ||
235
+ !configuredUrls.has(server.server_url) ||
236
+ selectedUrls.has(server.server_url)
237
+ ) {
238
+ continue;
239
+ }
240
+ selectedUrls.add(server.server_url);
241
+ selectedServerIds.push(serverId);
242
+ }
243
+ if (selectedServerIds.length === 0) return [];
244
+
245
+ const selectedIds = new Set(selectedServerIds);
246
+ return host.mcp
247
+ .listTools({
248
+ serverId: selectedServerIds,
249
+ state: "ready",
250
+ })
251
+ .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
+ };
305
+ },
306
+ };
307
+ return {
308
+ 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.
315
+ requiredExecutionLevel: "high",
316
+ source: "action",
317
+ };
318
+ });
319
+ }