@springbrand/agent-runtime 0.1.3-alpha.4 → 0.1.3-alpha.6

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 (45) hide show
  1. package/package.json +11 -3
  2. package/src/adapter/cloudflare/index.ts +55 -0
  3. package/src/adapter/cloudflare/resources/runtime-resources.ts +89 -0
  4. package/src/adapter/cloudflare/sandbox/adapter.ts +1509 -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 +273 -0
  12. package/src/adapter/cloudflare/universal-agent/tools.ts +74 -0
  13. package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
  14. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
  15. package/src/agent-tool-runtime.ts +152 -0
  16. package/src/index.ts +49 -7
  17. package/src/kernel/bindings.ts +6 -6
  18. package/src/kernel/recoverable-chat-agent.ts +12 -0
  19. package/src/kernel/runtime-load.ts +89 -0
  20. package/src/layers/orchestration/temporary-agent/core.ts +12 -1
  21. package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
  22. package/src/lib/mcp.ts +7 -3
  23. package/src/pi/assembly/context.ts +3 -3
  24. package/src/pi/assembly/extensions.ts +11 -22
  25. package/src/pi/assembly/snapshot.ts +1 -1
  26. package/src/pi/message/contract.ts +7 -0
  27. package/src/pi/message/conversion.ts +9 -1
  28. package/src/pi/runtime-adapter/assembly.ts +4 -10
  29. package/src/pi/runtime-adapter/index.ts +6 -2
  30. package/src/pi/tool/base.ts +17 -2
  31. package/src/pi/tool/compiler.ts +0 -1
  32. package/src/pi/tool/core.ts +13 -3
  33. package/src/pi/tool/mcp.ts +3 -4
  34. package/src/pi/tool/schedule.ts +11 -1
  35. package/src/pi/tool/skill.ts +55 -41
  36. package/src/pi/tool/subagent.ts +14 -2
  37. package/src/pi/tool/web-fetch.ts +0 -1
  38. package/src/pi/tool/web-search/web-search.ts +0 -1
  39. package/src/pi/tool/workspace-sandbox.ts +15 -7
  40. package/src/runtime-agent-context.ts +112 -0
  41. package/src/runtime-agent.ts +442 -328
  42. package/src/runtime-assembler.ts +255 -103
  43. package/src/runtime-definition.ts +173 -0
  44. package/src/runtime.ts +185 -25
  45. package/src/tool-registry.ts +143 -0
@@ -1,5 +1,94 @@
1
1
  import type { RuntimeLoadPhase, RuntimeLoadState } from "./state";
2
2
 
3
+ /** Hard ceiling for one Host-owned Runtime definition phase. */
4
+ export const RUNTIME_LOAD_TIMEOUT_MS = 180_000;
5
+
6
+ /** Hard ceiling for one optional external capability load. */
7
+ export const RUNTIME_CAPABILITY_LOAD_TIMEOUT_MS = 60_000;
8
+
9
+ const reportedFailures = new WeakSet<object>();
10
+
11
+ function errorText(error: unknown): string {
12
+ return error instanceof Error ? error.message : String(error);
13
+ }
14
+
15
+ /** Emit one stable structured record for a Runtime load failure. */
16
+ export function logRuntimeLoadFailure(
17
+ step: string,
18
+ error: unknown,
19
+ details: {
20
+ reason?: "error" | "timeout";
21
+ timeoutMs?: number;
22
+ durationMs?: number;
23
+ } = {},
24
+ ): void {
25
+ if (typeof error === "object" && error !== null) {
26
+ if (reportedFailures.has(error)) return;
27
+ reportedFailures.add(error);
28
+ }
29
+ console.error(
30
+ "[runtime-load:failed]",
31
+ JSON.stringify({
32
+ step,
33
+ reason: details.reason ?? "error",
34
+ ...(details.timeoutMs !== undefined
35
+ ? { timeoutMs: details.timeoutMs }
36
+ : {}),
37
+ ...(details.durationMs !== undefined
38
+ ? { durationMs: details.durationMs }
39
+ : {}),
40
+ error: errorText(error),
41
+ }),
42
+ );
43
+ }
44
+
45
+ /** Stop waiting for one Runtime load boundary and report any failure once. */
46
+ export async function withRuntimeLoadTimeout<T>(
47
+ step: string,
48
+ load: () => T | PromiseLike<T>,
49
+ options: {
50
+ timeoutMs?: number;
51
+ onLateResult?: (value: T) => void | Promise<void>;
52
+ } = {},
53
+ ): Promise<T> {
54
+ const timeoutMs = options.timeoutMs ?? RUNTIME_CAPABILITY_LOAD_TIMEOUT_MS;
55
+ const startedAt = Date.now();
56
+ let timeout: ReturnType<typeof setTimeout> | undefined;
57
+ let timedOut = false;
58
+ const operation = Promise.resolve().then(load);
59
+
60
+ if (options.onLateResult) {
61
+ void operation.then(async (value) => {
62
+ if (timedOut) await options.onLateResult!(value);
63
+ }, () => undefined).catch((error) => {
64
+ logRuntimeLoadFailure(`${step}:late-cleanup`, error);
65
+ });
66
+ }
67
+
68
+ try {
69
+ return await Promise.race([
70
+ operation,
71
+ new Promise<never>((_, reject) => {
72
+ timeout = setTimeout(() => {
73
+ timedOut = true;
74
+ reject(new Error(
75
+ `Runtime load step "${step}" timed out after ${timeoutMs}ms`,
76
+ ));
77
+ }, timeoutMs);
78
+ }),
79
+ ]);
80
+ } catch (error) {
81
+ logRuntimeLoadFailure(step, error, {
82
+ reason: timedOut ? "timeout" : "error",
83
+ timeoutMs,
84
+ durationMs: Date.now() - startedAt,
85
+ });
86
+ throw error;
87
+ } finally {
88
+ if (timeout !== undefined) clearTimeout(timeout);
89
+ }
90
+ }
91
+
3
92
  /**
4
93
  * 装载状态机与宿主之间的全部接触面。
5
94
  *
@@ -1,9 +1,21 @@
1
+ import type { ExecutionLevel } from "../../../lib/execution-level";
2
+
3
+ export const TEMPORARY_AGENT_LAUNCH_KEY =
4
+ "universal-agent:temporary-agent-launch";
5
+
1
6
  export interface TemporaryAgentRequest {
2
7
  subagentName: string;
3
8
  instructions: string;
4
9
  task: string;
5
10
  }
6
11
 
12
+ export interface TemporaryAgentLaunch<Config = unknown>
13
+ extends TemporaryAgentRequest {
14
+ runtimeKey: string;
15
+ config: Config;
16
+ executionLevel: ExecutionLevel;
17
+ }
18
+
7
19
  export interface TemporaryAgentRunContext {
8
20
  signal: AbortSignal;
9
21
  requestId: string;
@@ -149,4 +161,3 @@ export class TemporaryAgentCoordinator {
149
161
  });
150
162
  }
151
163
  }
152
- import type { ExecutionLevel } from "../../../lib/execution-level";
@@ -10,7 +10,6 @@ const BLOCKED_TOOLS = new Set([
10
10
  "resume_schedule",
11
11
  "change_schedule_agent",
12
12
  "cancel_schedule",
13
- "execute",
14
13
  "set_context",
15
14
  "load_context",
16
15
  "search_context",
@@ -25,7 +24,7 @@ const BLOCKED_TOOLS = new Set([
25
24
  * @remarks
26
25
  * 宿主在装配临时 Agent 的平台、Workspace、Sandbox 和其他 Tool 时调用;调用方可再加名字或前缀黑名单。
27
26
  *
28
- * 固定黑名单排除再次委派、调度、主 Session 上下文和绕过隔离边界的 execute,以保持单层临时 Agent 和已确认的继承范围。
27
+ * 固定黑名单排除再次委派、调度和主 Session 上下文,以保持单层临时 Agent 和已确认的继承范围。
29
28
  *
30
29
  * Agent、Session、Workspace 和 Tool 的术语见 `src/index.ts`。
31
30
  */
package/src/lib/mcp.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { RetryOptions } from "agents";
2
+ import { withRuntimeLoadTimeout } from "../kernel/runtime-load";
2
3
 
3
4
  const MCP_RETRY: RetryOptions = {
4
5
  maxAttempts: 3,
@@ -63,9 +64,12 @@ export async function connectConfiguredMcpServers(
63
64
  const servers = [...pending.values()];
64
65
  const settled = await Promise.allSettled(
65
66
  servers.map((server) =>
66
- host.addMcpServer(server.name, server.url, {
67
- retry: MCP_RETRY,
68
- }),
67
+ withRuntimeLoadTimeout(
68
+ `mcp:${server.name}:connect`,
69
+ () => host.addMcpServer(server.name, server.url, {
70
+ retry: MCP_RETRY,
71
+ }),
72
+ ),
69
73
  ),
70
74
  );
71
75
  const failed = settled.flatMap((result, index) =>
@@ -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}`,
@@ -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,
@@ -22,7 +22,7 @@ import type { PiToolCandidate } from "../tool/compiler";
22
22
  */
23
23
  export interface PiToolSurface {
24
24
  finalize(
25
- authorizedCandidates: readonly PiToolCandidate[],
25
+ candidates: readonly PiToolCandidate[],
26
26
  ): readonly PiToolCandidate[];
27
27
  }
28
28
 
@@ -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
  }
@@ -248,10 +248,12 @@ function describeTools(candidates: readonly PiToolCandidate[]) {
248
248
  name: candidate.tool.name,
249
249
  owner: candidate.owner,
250
250
  label: candidate.tool.label,
251
- description:
252
- candidate.tool.description ?? candidate.tool.label,
251
+ description: candidate.tool.description ?? candidate.tool.label,
253
252
  parameters: candidate.tool.parameters,
254
253
  requiredExecutionLevel: candidate.requiredExecutionLevel,
254
+ ...(candidate.alwaysRequiresApproval
255
+ ? { alwaysRequiresApproval: true }
256
+ : {}),
255
257
  ...(candidate.source ? { source: candidate.source } : {}),
256
258
  retry: piToolRetryPolicy(candidate),
257
259
  }))
@@ -390,16 +392,8 @@ async function preparePiAssembly(
390
392
  options: PreparePiAssemblyOptions,
391
393
  ): Promise<PreparedPiAssembly> {
392
394
  const { snapshot } = options;
393
- const extensionNames = new Set(
394
- snapshot.pi.extensions.map((extension) =>
395
- extension.manifest.name.trim()
396
- ),
397
- );
398
395
  const extensions = await assemblePiExtensions({
399
396
  extensions: snapshot.pi.extensions,
400
- published: extensionNames,
401
- enabled: extensionNames,
402
- authorized: extensionNames,
403
397
  load: async (extension) => {
404
398
  const workspacePermission =
405
399
  extension.manifest.permissions?.workspace ?? "none";
@@ -107,8 +107,11 @@ export class PiRuntimeAdapter {
107
107
  * 实现理由:转换只生成 Pi UserMessage 的 content 和 timestamp,原始浏览器视图由 transcript sidecar 另行保存。
108
108
  * 不要把 UI id、metadata 或原始 part 结构并入 canonical message。
109
109
  */
110
- normalizeUserInput(input: UIMessage & { role: "user" }) {
111
- return uiUserMessageToPi(input);
110
+ normalizeUserInput(
111
+ input: UIMessage & { role: "user" },
112
+ privateContext?: string,
113
+ ) {
114
+ return uiUserMessageToPi(input, privateContext);
112
115
  }
113
116
 
114
117
  resolveApiKey(
@@ -209,6 +212,7 @@ export class PiRuntimeAdapter {
209
212
  }
210
213
 
211
214
  export type {
215
+ RequestedCapability,
212
216
  UIChatRequestBody,
213
217
  } from "../message";
214
218
  export type { PiToolApproval } from "../turn";
@@ -8,6 +8,10 @@ import type { RuntimeMemoryPort } from "../../kernel/bindings";
8
8
  import type { RuntimeMemoryProfile } from "../../kernel/profile";
9
9
  import { serializeOutput } from "../../lib/artifacts";
10
10
  import type { PiToolCandidate } from "./compiler";
11
+ import {
12
+ toolRegistryFromPiCandidates,
13
+ type ToolRegistry,
14
+ } from "../../tool-registry";
11
15
  import { webSearchPiToolCandidate } from "./web-search";
12
16
  import type { WebSearch } from "./web-search/api";
13
17
 
@@ -140,7 +144,6 @@ function result<T>(details: T): AgentToolResult<T> {
140
144
  function candidate<T extends TSchema>(tool: AgentTool<T>): PiToolCandidate {
141
145
  return {
142
146
  owner: "runtime-base",
143
- authorized: true,
144
147
  requiredExecutionLevel: "safe",
145
148
  source: "action",
146
149
  tool,
@@ -265,9 +268,21 @@ export function memoryPiToolCandidate(
265
268
  };
266
269
  return {
267
270
  owner: "runtime-memory",
268
- authorized: true,
269
271
  requiredExecutionLevel: "safe",
270
272
  source: "action",
271
273
  tool,
272
274
  };
273
275
  }
276
+
277
+ /** 从 Memory Port 生成 `set_context` Tool。 */
278
+ export function createMemoryTools(
279
+ memory: RuntimeMemoryPort,
280
+ profile: Pick<
281
+ RuntimeMemoryProfile,
282
+ "memoryTokens" | "preferencesTokens"
283
+ >,
284
+ ): ToolRegistry {
285
+ return toolRegistryFromPiCandidates([
286
+ memoryPiToolCandidate(memory, profile),
287
+ ]);
288
+ }
@@ -36,7 +36,6 @@ export interface PiToolInteractionSpec {
36
36
  /** 描述一个尚未进入最终 Tool Surface 和结算包装的 Pi 工具。 */
37
37
  export interface PiToolCandidate {
38
38
  readonly owner: string;
39
- readonly authorized: boolean;
40
39
  readonly tool: AgentTool<any, any>;
41
40
  /** Conservative maximum used in the stable Runtime descriptor. */
42
41
  readonly requiredExecutionLevel: ExecutionLevel;
@@ -12,6 +12,10 @@ import { serializeOutput } from "../../lib/artifacts";
12
12
  import type { PiLoadedExtension } from "../assembly/extensions";
13
13
  import { aiToolToPi } from "./ai-adapter";
14
14
  import type { PiToolCandidate } from "./compiler";
15
+ import {
16
+ toolRegistryFromPiCandidates,
17
+ type ToolRegistry,
18
+ } from "../../tool-registry";
15
19
 
16
20
  // 本文件沿用 `../../index.ts` 入口定义的 Extension、Port 和 Tool Candidate 术语。
17
21
 
@@ -47,7 +51,6 @@ export function browserQuickActionPiToolCandidates(
47
51
  return Object.entries(createQuickActionTools({ browser })).map(
48
52
  ([name, tool]) => ({
49
53
  owner: "core:browser",
50
- authorized: true,
51
54
  requiredExecutionLevel: "low",
52
55
  tool: aiToolToPi(name, tool, {
53
56
  label: BROWSER_TOOL_LABELS[name] ?? name,
@@ -90,7 +93,6 @@ export function listExtensionsPiToolCandidate(
90
93
  };
91
94
  return {
92
95
  owner: "core:extensions",
93
- authorized: true,
94
96
  requiredExecutionLevel: "low",
95
97
  tool,
96
98
  };
@@ -170,7 +172,6 @@ export function codeExecutionPiToolCandidate(
170
172
  };
171
173
  return {
172
174
  owner: "core:codemode",
173
- authorized: true,
174
175
  requiredExecutionLevel: "high",
175
176
  source: "codemode",
176
177
  summary: "Run JavaScript with network and configured connector access",
@@ -178,4 +179,13 @@ export function codeExecutionPiToolCandidate(
178
179
  };
179
180
  }
180
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
+
181
191
  // #endregion
@@ -263,7 +263,6 @@ export function createPiMcpToolCandidate(
263
263
  };
264
264
  return {
265
265
  owner: policy.owner,
266
- authorized: true,
267
266
  tool,
268
267
  summary: label,
269
268
  requiredExecutionLevel: policy.requiredExecutionLevel,
@@ -278,15 +277,15 @@ export function createPiMcpToolCandidate(
278
277
  }
279
278
 
280
279
  /**
281
- * 把当前已连接且获授权的 MCP 工具转换成 Pi 候选工具。
280
+ * 把当前已配置且 ready 的 MCP 工具转换成 Pi 候选工具。
282
281
  *
283
282
  * Tool Surface 在 Runtime 准备时调用它。调用方应传入
284
283
  * Agent Host,以及同一 Runtime 快照中的 MCP 配置;未就绪、未配置或重复 URL
285
284
  * 的连接不会进入模型工具目录。
286
285
  *
287
286
  * “候选工具”是进入 Pi 编译和统一治理前的内部描述。连接、凭据和恢复由 Host
288
- * 与 Agents SDK 管理;本函数只做授权快照过滤、Schema 转接和公开结果投影。
289
- * 远端 Server 提供的 annotations 只是提示,不是本地授权边界,所以所有动态
287
+ * 与 Agents SDK 管理;本函数只做动态连接状态过滤、Schema 转接和公开结果投影。
288
+ * 远端 Server 提供的 annotations 只是提示,不是本地审批边界,所以所有动态
290
289
  * MCP 工具继续标为高风险,不能据此绕过审批。
291
290
  *
292
291
  * `Host`、`Pi`、`Runtime Snapshot` 与“候选工具”见 `../../index.ts`。
@@ -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({
@@ -95,7 +99,6 @@ function candidate<T extends TSchema>(
95
99
  ): PiToolCandidate {
96
100
  return {
97
101
  owner: options.owner ?? "runtime-base",
98
- authorized: true,
99
102
  requiredExecutionLevel: options.requiredExecutionLevel ?? "safe",
100
103
  source: "action",
101
104
  tool,
@@ -240,3 +243,10 @@ export function schedulePiToolCandidates(
240
243
  ),
241
244
  ];
242
245
  }
246
+
247
+ /** 从 {@link RuntimeSchedulePort} 生成定时任务 Tool 集。 */
248
+ export function createScheduleTools(
249
+ schedule: RuntimeSchedulePort,
250
+ ): ToolRegistry {
251
+ return toolRegistryFromPiCandidates(schedulePiToolCandidates(schedule));
252
+ }
@@ -13,9 +13,10 @@ import type {
13
13
  import { aiToolToPi } from "./ai-adapter";
14
14
  import type { PiToolCandidate } from "./compiler";
15
15
 
16
- /** An authorized Skill source and its script capabilities. */
16
+ /** A configured Skill source and its script capabilities. */
17
17
  export interface PiSkillBinding {
18
18
  readonly name: string;
19
+ readonly description: string;
19
20
  readonly source: SkillSource;
20
21
  readonly script?: RuntimeSkillScriptPolicy;
21
22
  }
@@ -46,42 +47,23 @@ function scriptTools(
46
47
  })) as ToolSet;
47
48
  }
48
49
 
49
- // A binding authorizes one configured name even if its backing source happens
50
- // to contain more Skills. Source failures are also sanitized at this boundary.
51
- function authorizedSource(binding: PiSkillBinding): SkillSource {
52
- const { name, source } = binding;
50
+ function catalogSkillSource(binding: PiSkillBinding): SkillSource {
51
+ const source = binding.source;
53
52
  return {
54
- id: `${source.id}:${name}`,
53
+ id: source.id,
55
54
  get fingerprint() {
56
- return `${source.fingerprint}:${name}`;
57
- },
58
- async list() {
59
- return (await source.list()).filter((skill) => skill.name === name);
60
- },
61
- async load(requested) {
62
- if (requested !== name) return null;
63
- try {
64
- const skill = await source.load(requested);
65
- return skill?.name === name ? skill : null;
66
- } catch (cause) {
67
- throw new Error(`Skill unavailable: ${name}`, { cause });
68
- }
55
+ return source.fingerprint;
69
56
  },
57
+ // Runtime configuration already resolved the catalogue; Skill content stays lazy.
58
+ list: async () => [{
59
+ name: binding.name,
60
+ description: binding.description,
61
+ sourceId: source.id,
62
+ }],
63
+ load: (name) => source.load(name),
70
64
  ...(source.readResource
71
- ? {
72
- async readResource(requested: string, path: string) {
73
- if (requested !== name) return null;
74
- try {
75
- const resource = await source.readResource!(requested, path);
76
- return resource?.path === path ? resource : null;
77
- } catch (cause) {
78
- throw new Error(
79
- `Skill resource unavailable: ${name}/${path}`,
80
- { cause },
81
- );
82
- }
83
- },
84
- }
65
+ ? { readResource: (name: string, path: string) =>
66
+ source.readResource!(name, path) }
85
67
  : {}),
86
68
  ...(source.refresh ? { refresh: () => source.refresh!() } : {}),
87
69
  };
@@ -123,6 +105,9 @@ const SKILL_TOOL_LABELS: Readonly<Record<string, string>> = {
123
105
  run_skill_script: "Run Skill script",
124
106
  };
125
107
 
108
+ const SKILL_ENTRY_READ_GUIDANCE =
109
+ "SKILL.md contains the Skill instructions; use activate_skill instead.";
110
+
126
111
  /** Create Pi candidates from the official Agents SDK SkillRegistry tools. */
127
112
  export async function skillPiToolCandidates(
128
113
  bindings: readonly PiSkillBinding[],
@@ -131,17 +116,46 @@ export async function skillPiToolCandidates(
131
116
  if (bindings.length === 0) return [];
132
117
 
133
118
  const registry = new SkillRegistry(
134
- bindings.map(authorizedSource),
119
+ bindings.map(catalogSkillSource),
135
120
  scriptRunner(bindings, options),
136
121
  );
137
122
  await registry.load();
138
123
 
139
- return Object.entries(registry.tools()).map(([name, tool]) => ({
140
- owner: "runtime-skill",
141
- authorized: true,
142
- requiredExecutionLevel: name === "run_skill_script" ? "high" : "safe",
143
- tool: aiToolToPi(name, tool, {
124
+ return Object.entries(registry.tools()).map(([name, tool]) => {
125
+ const adapted = aiToolToPi(name, tool, {
144
126
  label: SKILL_TOOL_LABELS[name] ?? name,
145
- }),
146
- }));
127
+ ...(name === "read_skill_resource"
128
+ ? {
129
+ description:
130
+ "Read a bundled resource listed by activate_skill. Do not pass SKILL.md; activate_skill returns the Skill instructions.",
131
+ }
132
+ : {}),
133
+ });
134
+ if (name === "read_skill_resource") {
135
+ const execute = adapted.execute;
136
+ adapted.execute = (toolCallId, input, signal) => {
137
+ const target = input as { name?: unknown; path?: unknown };
138
+ const parts = typeof target.path === "string"
139
+ ? target.path.split("/")
140
+ : [];
141
+ if (
142
+ target.path === "SKILL.md" ||
143
+ (target.name === undefined &&
144
+ parts.length === 2 &&
145
+ parts[1] === "SKILL.md")
146
+ ) {
147
+ return Promise.resolve({
148
+ content: [{ type: "text", text: SKILL_ENTRY_READ_GUIDANCE }],
149
+ details: SKILL_ENTRY_READ_GUIDANCE,
150
+ });
151
+ }
152
+ return execute(toolCallId, input, signal);
153
+ };
154
+ }
155
+ return {
156
+ owner: "runtime-skill",
157
+ requiredExecutionLevel: name === "run_skill_script" ? "high" : "safe",
158
+ tool: adapted,
159
+ };
160
+ });
147
161
  }