@springbrand/agent-runtime 0.1.3-alpha.5 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.1.3-alpha.5",
3
+ "version": "0.1.3-alpha.6",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -22,9 +22,15 @@
22
22
  "@cloudflare/think": "0.15.0",
23
23
  "@earendil-works/pi-agent-core": "0.83.0",
24
24
  "@earendil-works/pi-ai": "0.83.0",
25
- "agents": "^0.19.0",
26
- "ai": "^7.0.0",
27
25
  "lodash-es": "^4.18.1",
28
26
  "zod": "^4.4.3"
27
+ },
28
+ "peerDependencies": {
29
+ "agents": "^0.19.0",
30
+ "ai": "^7.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "agents": "0.19.0",
34
+ "ai": "7.0.37"
29
35
  }
30
36
  }
@@ -1,8 +1,3 @@
1
- export {
2
- defineCloudflareUniversalAgent,
3
- type CloudflareUniversalAgentClass,
4
- type CloudflareUniversalAgentInstance,
5
- } from "./universal-agent/definition";
6
1
  export {
7
2
  createPlatformLoader,
8
3
  createWorkspaceLoader,
@@ -1,6 +1,7 @@
1
1
  import { r2, type SkillSource } from "agents/skills";
2
2
  import type { RuntimeDegradation } from "../../../kernel/degradation";
3
3
  import type { RuntimeExtensionConfig } from "../../../kernel/extensions";
4
+ import { withRuntimeLoadTimeout } from "../../../kernel/runtime-load";
4
5
  import type { RuntimeExtensionContribution } from "../../../runtime-definition";
5
6
 
6
7
  export interface CloudflareRuntimeExtensionSource {
@@ -38,17 +39,19 @@ export async function readVerifiedR2Text(
38
39
  sourceRef: string,
39
40
  sourceHash: string,
40
41
  ): Promise<string> {
41
- const object = await bucket.get(sourceRef);
42
- if (!object) throw new Error(`Runtime content not found: ${sourceRef}`);
43
- const source = await object.text();
44
- const digest = await crypto.subtle.digest(
45
- "SHA-256",
46
- new TextEncoder().encode(source),
47
- );
48
- if (hex(digest) !== sourceHash) {
49
- throw new Error(`Runtime content hash mismatch: ${sourceRef}`);
50
- }
51
- return source;
42
+ return withRuntimeLoadTimeout("extension-source:r2", async () => {
43
+ const object = await bucket.get(sourceRef);
44
+ if (!object) throw new Error(`Runtime content not found: ${sourceRef}`);
45
+ const source = await object.text();
46
+ const digest = await crypto.subtle.digest(
47
+ "SHA-256",
48
+ new TextEncoder().encode(source),
49
+ );
50
+ if (hex(digest) !== sourceHash) {
51
+ throw new Error(`Runtime content hash mismatch: ${sourceRef}`);
52
+ }
53
+ return source;
54
+ });
52
55
  }
53
56
 
54
57
  export async function prepareRuntimeExtensions(
@@ -104,7 +104,6 @@ function executeCandidate(options: {
104
104
  };
105
105
  return {
106
106
  owner: "subagent-execute",
107
- authorized: true,
108
107
  requiredExecutionLevel: "safe",
109
108
  tool,
110
109
  };
@@ -181,7 +180,6 @@ function fetchCandidate(options: {
181
180
  };
182
181
  return {
183
182
  owner: "subagent-fetch",
184
- authorized: true,
185
183
  requiredExecutionLevel: "safe",
186
184
  tool,
187
185
  };
@@ -242,11 +240,11 @@ export function createCloudflareSubAgentTools(options: {
242
240
  ]
243
241
  : [...workspace, fetchCandidate(options)];
244
242
 
245
- // 子 Agent 是独立的嵌套 Tool surface:先移除未授权和会递归委派的候选,
243
+ // 子 Agent 是独立的嵌套 Tool surface:先移除会递归委派的候选,
246
244
  // compiler 只负责执行治理和结算。
247
245
  const deny = new Set(DENIED_TOOLS);
248
246
  const mounted = candidates.filter(
249
- (candidate) => candidate.authorized && !deny.has(candidate.tool.name),
247
+ (candidate) => !deny.has(candidate.tool.name),
250
248
  );
251
249
  assertSafeExecutionLevel(mounted);
252
250
 
@@ -893,15 +893,15 @@ export interface RuntimePlatformPort {
893
893
  }
894
894
 
895
895
  /**
896
- * 汇总本次装配已授权的 Skill 来源。
896
+ * 汇总本次装配已配置的 Skill 来源。
897
897
  *
898
898
  * @remarks
899
899
  * Builder 在生成候选 Snapshot 时创建,Pi 组装在生成 Skill 工具时读取。
900
900
  *
901
- * catalog、来源与脚本策略一起保留,避免装配期访问来源或让授权在 Skill 之间漂移。
901
+ * catalog、来源与脚本策略一起保留,避免装配期重新访问来源。
902
902
  */
903
903
  export interface RuntimeSkillBindings {
904
- /** Already-authorized external sources and their isolated script policy. */
904
+ /** Configured external sources and their isolated script policy. */
905
905
  sources: readonly RuntimeSkillSourceBinding[];
906
906
  }
907
907
 
@@ -911,7 +911,7 @@ export interface RuntimeSkillBindings {
911
911
  * @remarks
912
912
  * Skill Plugin 在准备来源时设置,Pi Skill 工具在决定是否注入脚本执行能力时读取。
913
913
  *
914
- * 这是已授权结果,执行期不应为某个 Skill 自动放宽。
914
+ * 这是 Config 已解析的执行策略,执行期不应自动放宽。
915
915
  */
916
916
  export interface RuntimeSkillScriptPolicy {
917
917
  network: "none" | "full";
@@ -920,12 +920,12 @@ export interface RuntimeSkillScriptPolicy {
920
920
  }
921
921
 
922
922
  /**
923
- * 把一个已授权 Skill 来源与其脚本策略绑定。
923
+ * 把一个已配置 Skill 来源与其脚本策略放在同一快照中。
924
924
  *
925
925
  * @remarks
926
926
  * Builder 为每个 Skill 贡献创建,Pi 组装和 Skill 工具按名称查找并读取。
927
927
  *
928
- * 名称、内容来源与权限不分开存放,防止脚本改用其他 Skill 的授权。
928
+ * 名称、内容来源与脚本策略不分开存放,脚本 runner 才能按 Skill 名找到对应策略。
929
929
  */
930
930
  export interface RuntimeSkillSourceBinding {
931
931
  name: string;
@@ -353,7 +353,13 @@ export abstract class RecoverableChatAgent<
353
353
  * 清理延后执行,为断线重连的客户端保留终态流片段。
354
354
  */
355
355
  protected completeRecoverableStream(streamId: string): void {
356
+ const completedRequestId = this.resumableStream.activeRequestId;
356
357
  this.resumableStream.complete(streamId);
358
+ this.pendingResumeConnections.clear();
359
+ if (completedRequestId === this.continuation.activeRequestId) {
360
+ this.continuation.activeRequestId = null;
361
+ this.continuation.activeConnectionId = null;
362
+ }
357
363
  void this.ensureStreamCleanupScheduled();
358
364
  }
359
365
 
@@ -366,7 +372,13 @@ export abstract class RecoverableChatAgent<
366
372
  * 流错误标志与业务失败分开,因为续传协议和提交生命周期的责任不同。
367
373
  */
368
374
  protected failRecoverableStream(streamId: string): void {
375
+ const erroredRequestId = this.resumableStream.activeRequestId;
369
376
  this.resumableStream.markError(streamId);
377
+ this.pendingResumeConnections.clear();
378
+ if (erroredRequestId === this.continuation.activeRequestId) {
379
+ this.continuation.activeRequestId = null;
380
+ this.continuation.activeConnectionId = null;
381
+ }
370
382
  void this.ensureStreamCleanupScheduled();
371
383
  }
372
384
 
@@ -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
  *
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
 
@@ -392,16 +392,8 @@ async function preparePiAssembly(
392
392
  options: PreparePiAssemblyOptions,
393
393
  ): Promise<PreparedPiAssembly> {
394
394
  const { snapshot } = options;
395
- const extensionNames = new Set(
396
- snapshot.pi.extensions.map((extension) =>
397
- extension.manifest.name.trim()
398
- ),
399
- );
400
395
  const extensions = await assemblePiExtensions({
401
396
  extensions: snapshot.pi.extensions,
402
- published: extensionNames,
403
- enabled: extensionNames,
404
- authorized: extensionNames,
405
397
  load: async (extension) => {
406
398
  const workspacePermission =
407
399
  extension.manifest.permissions?.workspace ?? "none";
@@ -144,7 +144,6 @@ function result<T>(details: T): AgentToolResult<T> {
144
144
  function candidate<T extends TSchema>(tool: AgentTool<T>): PiToolCandidate {
145
145
  return {
146
146
  owner: "runtime-base",
147
- authorized: true,
148
147
  requiredExecutionLevel: "safe",
149
148
  source: "action",
150
149
  tool,
@@ -269,7 +268,6 @@ export function memoryPiToolCandidate(
269
268
  };
270
269
  return {
271
270
  owner: "runtime-memory",
272
- authorized: true,
273
271
  requiredExecutionLevel: "safe",
274
272
  source: "action",
275
273
  tool,
@@ -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;
@@ -51,7 +51,6 @@ export function browserQuickActionPiToolCandidates(
51
51
  return Object.entries(createQuickActionTools({ browser })).map(
52
52
  ([name, tool]) => ({
53
53
  owner: "core:browser",
54
- authorized: true,
55
54
  requiredExecutionLevel: "low",
56
55
  tool: aiToolToPi(name, tool, {
57
56
  label: BROWSER_TOOL_LABELS[name] ?? name,
@@ -94,7 +93,6 @@ export function listExtensionsPiToolCandidate(
94
93
  };
95
94
  return {
96
95
  owner: "core:extensions",
97
- authorized: true,
98
96
  requiredExecutionLevel: "low",
99
97
  tool,
100
98
  };
@@ -174,7 +172,6 @@ export function codeExecutionPiToolCandidate(
174
172
  };
175
173
  return {
176
174
  owner: "core:codemode",
177
- authorized: true,
178
175
  requiredExecutionLevel: "high",
179
176
  source: "codemode",
180
177
  summary: "Run JavaScript with network and configured connector access",
@@ -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`。
@@ -99,7 +99,6 @@ function candidate<T extends TSchema>(
99
99
  ): PiToolCandidate {
100
100
  return {
101
101
  owner: options.owner ?? "runtime-base",
102
- authorized: true,
103
102
  requiredExecutionLevel: options.requiredExecutionLevel ?? "safe",
104
103
  source: "action",
105
104
  tool,
@@ -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
  }
@@ -91,7 +91,6 @@ export function subagentPiToolCandidates(
91
91
  };
92
92
  return {
93
93
  owner: `subagent:${type.name}`,
94
- authorized: true,
95
94
  requiredExecutionLevel: "safe",
96
95
  tool,
97
96
  };
@@ -142,7 +141,6 @@ export function subagentPiToolCandidates(
142
141
  };
143
142
  candidates.push({
144
143
  owner: "subagent:background",
145
- authorized: true,
146
144
  requiredExecutionLevel: "low",
147
145
  summary: "Dispatch a background sub-agent",
148
146
  source: "action",
@@ -272,7 +272,6 @@ export function webFetchPiToolCandidate(): PiToolCandidate {
272
272
 
273
273
  return {
274
274
  owner: "runtime-base",
275
- authorized: true,
276
275
  requiredExecutionLevel: "safe",
277
276
  source: "action",
278
277
  tool,
@@ -119,7 +119,6 @@ export function webSearchPiToolCandidate(
119
119
  };
120
120
  return {
121
121
  owner: "runtime-base",
122
- authorized: true,
123
122
  requiredExecutionLevel: "safe",
124
123
  source: "action",
125
124
  tool,
@@ -288,13 +288,11 @@ export function workspacePiToolCandidates(
288
288
  return [
289
289
  ...[read, write, edit, list, find, grep, remove].map((tool) => ({
290
290
  owner: "workspace",
291
- authorized: true,
292
291
  requiredExecutionLevel: "safe" as const,
293
292
  tool,
294
293
  })),
295
294
  {
296
295
  owner: "workspace",
297
- authorized: true,
298
296
  requiredExecutionLevel: "high" as const,
299
297
  summary: "Run a Bash script over Workspace files",
300
298
  source: "action" as const,
@@ -474,31 +472,26 @@ export function sandboxPiToolCandidates(
474
472
  return [
475
473
  {
476
474
  owner: "sandbox",
477
- authorized: true,
478
475
  requiredExecutionLevel: "high",
479
476
  tool: exec,
480
477
  },
481
478
  {
482
479
  owner: "sandbox",
483
- authorized: true,
484
480
  requiredExecutionLevel: "high",
485
481
  tool: startProcess,
486
482
  },
487
483
  {
488
484
  owner: "sandbox",
489
- authorized: true,
490
485
  requiredExecutionLevel: "safe",
491
486
  tool: processLogs,
492
487
  },
493
488
  {
494
489
  owner: "sandbox",
495
- authorized: true,
496
490
  requiredExecutionLevel: "high",
497
491
  tool: stopProcess,
498
492
  },
499
493
  {
500
494
  owner: "sandbox",
501
- authorized: true,
502
495
  requiredExecutionLevel: "high",
503
496
  tool: publishFiles,
504
497
  },
@@ -44,6 +44,11 @@ import {
44
44
  } from "./tool-registry";
45
45
  import type { RuntimeAssemblyView } from "./kernel/runtime-assembly-view";
46
46
  import type { RuntimeConfigUpdateResult } from "./kernel/runtime-config";
47
+ import {
48
+ logRuntimeLoadFailure,
49
+ RUNTIME_LOAD_TIMEOUT_MS,
50
+ withRuntimeLoadTimeout,
51
+ } from "./kernel/runtime-load";
47
52
  import { AgentToolRuntimeKernel } from "./agent-tool-runtime";
48
53
  import type {
49
54
  RuntimeAgentConfigContext,
@@ -388,20 +393,6 @@ function parseTemporaryAgentLaunch<Config>(
388
393
  *
389
394
  * 生成类把首次加载、并发去重、按 key 重载和失败重试收口到一个入口。
390
395
  */
391
- export function defineRuntimeAgent<
392
- Env extends Cloudflare.Env,
393
- Config,
394
- Command,
395
- Change,
396
- >(
397
- definition: MutableRuntimeAgentDefinition<Env, Config, Command, Change>,
398
- ): RuntimeAgentClass<Env, Command, Change>;
399
- export function defineRuntimeAgent<
400
- Env extends Cloudflare.Env = Cloudflare.Env,
401
- Config = unknown,
402
- >(
403
- definition: ReadonlyRuntimeAgentDefinition<Env, Config>,
404
- ): RuntimeAgentClass<Env>;
405
396
  export function defineRuntimeAgent<
406
397
  Env extends Cloudflare.Env = Cloudflare.Env,
407
398
  Config = unknown,
@@ -587,14 +578,18 @@ export function defineRuntimeAgent<
587
578
  const pending = (async () => {
588
579
  this.runtimeLoad.begin();
589
580
  const loadContext = this.createDefinitionContext();
590
- const loaded = await (
591
- definition.load as (
592
- context: RuntimeAgentPlanningContext<Env, Command, Change>,
593
- requestedRuntimeKey?: string,
594
- ) => Promise<LoadedRuntime<Config>>
595
- )(
596
- loadContext,
597
- requestedRuntimeKey,
581
+ const loaded = await withRuntimeLoadTimeout(
582
+ "definition.load",
583
+ () => (
584
+ definition.load as (
585
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
586
+ requestedRuntimeKey?: string,
587
+ ) => Promise<LoadedRuntime<Config>>
588
+ )(
589
+ loadContext,
590
+ requestedRuntimeKey,
591
+ ),
592
+ { timeoutMs: RUNTIME_LOAD_TIMEOUT_MS },
598
593
  );
599
594
  const resolvedRuntimeKey = loaded.runtimeKey.trim();
600
595
  if (!resolvedRuntimeKey) {
@@ -603,12 +598,16 @@ export function defineRuntimeAgent<
603
598
  const context = this.createDefinitionContext({
604
599
  value: loaded.config,
605
600
  });
606
- let tools = normalizeToolAssembly(await (
607
- definition.tools as (
608
- context: RuntimeAgentPlanningContext<Env, Command, Change>,
609
- config: Config,
610
- ) => ToolRegistry | ToolAssemblyResult | Promise<ToolRegistry | ToolAssemblyResult>
611
- )(context, loaded.config));
601
+ let tools = normalizeToolAssembly(await withRuntimeLoadTimeout(
602
+ "definition.tools",
603
+ () => (
604
+ definition.tools as (
605
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
606
+ config: Config,
607
+ ) => ToolRegistry | ToolAssemblyResult | Promise<ToolRegistry | ToolAssemblyResult>
608
+ )(context, loaded.config),
609
+ { timeoutMs: RUNTIME_LOAD_TIMEOUT_MS },
610
+ ));
612
611
  let hooks = resolveDefinitionHooks(context);
613
612
  if (this.role === "temporary") {
614
613
  this.temporaryDispose = tools.dispose;
@@ -631,6 +630,7 @@ export function defineRuntimeAgent<
631
630
  // Host 的会话列表把装配后的第一次投影当作「facet 已就绪」的信号。
632
631
  await this.broadcastApprovals();
633
632
  })().catch((error) => {
633
+ logRuntimeLoadFailure("runtime", error);
634
634
  this.runtimeLoad.fail();
635
635
  throw error;
636
636
  });
@@ -45,6 +45,7 @@ import type {
45
45
  RuntimeToolSurfacePolicy,
46
46
  } from "./runtime-definition";
47
47
  import type { ToolRegistry } from "./tool-registry";
48
+ import { withRuntimeLoadTimeout } from "./kernel/runtime-load";
48
49
  import {
49
50
  basePiToolCandidates,
50
51
  memoryPiToolCandidate,
@@ -168,7 +169,7 @@ interface ToolSurface {
168
169
  readonly degradations: readonly RuntimeDegradation[];
169
170
  }
170
171
 
171
- // 唯一 Tool Surface seam:只读取已授权 Port、Resource 和 Agent policy,
172
+ // 唯一 Tool Surface seam:只读取已配置 Port、Resource 和 Agent policy,
172
173
  // 统一生成可见 Tool,并在一处做名称、执行档位与冲突校验。
173
174
  async function createToolSurface(
174
175
  input: ToolSurfaceInput,
@@ -177,7 +178,6 @@ async function createToolSurface(
177
178
  const allowsTool = input.policy?.allowsTool;
178
179
  const allowsExtension = input.policy?.allowsExtension;
179
180
  const visible = (candidate: PiToolCandidate) =>
180
- candidate.authorized &&
181
181
  !deny.has(candidate.tool.name) &&
182
182
  allowsTool?.(candidate.tool.name) !== false;
183
183
  const browserCandidates = input.platform.browser
@@ -207,12 +207,12 @@ async function createToolSurface(
207
207
 
208
208
  return {
209
209
  surface: Object.freeze({
210
- finalize(authorizedCandidates: readonly PiToolCandidate[]) {
210
+ finalize(candidates: readonly PiToolCandidate[]) {
211
211
  const tools = new Map<string, PiToolCandidate>();
212
212
 
213
213
  for (const candidate of [
214
214
  ...staticCandidates,
215
- ...authorizedCandidates,
215
+ ...candidates,
216
216
  ]) {
217
217
  const name = requiredName(candidate.tool.name, "Pi Tool");
218
218
  if (!visible(candidate)) continue;
@@ -576,7 +576,6 @@ export function toolRegistryPiToolCandidates(
576
576
  const normalized = requiredName(name, "Platform Tool");
577
577
  candidates.push(Object.freeze({
578
578
  owner: "platform",
579
- authorized: true,
580
579
  requiredExecutionLevel: spec.requiredExecutionLevel,
581
580
  ...(spec.alwaysRequiresApproval
582
581
  ? { alwaysRequiresApproval: true }
@@ -675,9 +674,12 @@ export async function assembleRuntimeSnapshot<
675
674
  const provider = resources.provider;
676
675
  let profile = settings.profile;
677
676
  if (input.hooks?.profileOverrides) {
678
- const overrides = await input.hooks.profileOverrides(
679
- input.ctx,
680
- input.config,
677
+ const overrides = await withRuntimeLoadTimeout(
678
+ "profile-overrides",
679
+ () => input.hooks!.profileOverrides!(
680
+ input.ctx,
681
+ input.config,
682
+ ),
681
683
  );
682
684
  if (overrides.systemPrompt !== undefined) {
683
685
  profile = { ...profile, systemPrompt: overrides.systemPrompt };
package/src/runtime.ts CHANGED
@@ -45,7 +45,10 @@ import type {
45
45
  RuntimeState,
46
46
  RuntimeTurnState,
47
47
  } from "./kernel/state";
48
- import { RuntimeLoadTracker } from "./kernel/runtime-load";
48
+ import {
49
+ RuntimeLoadTracker,
50
+ withRuntimeLoadTimeout,
51
+ } from "./kernel/runtime-load";
49
52
  import {
50
53
  prepareRuntimeCandidate,
51
54
  type RuntimeAssemblyInput,
@@ -175,6 +178,16 @@ function json(value: unknown): string {
175
178
  return JSON.stringify(value);
176
179
  }
177
180
 
181
+ async function closeRuntimeGatewaySession(
182
+ session: RuntimeGatewaySession | undefined,
183
+ step: string,
184
+ ): Promise<void> {
185
+ if (!session) return;
186
+ await withRuntimeLoadTimeout(step, () => session.close()).catch(
187
+ () => undefined,
188
+ );
189
+ }
190
+
178
191
  function assistantUsageEvent(
179
192
  eventId: string,
180
193
  submissionId: string,
@@ -530,8 +543,15 @@ export abstract class AgentRuntimeKernel<
530
543
  let gatewaySession: RuntimeGatewaySession | undefined;
531
544
  if (candidateSnapshot.bindings.gateway) {
532
545
  try {
533
- gatewaySession = await candidateSnapshot.bindings.gateway.open(
534
- `${this.ctx.id.toString()}:${crypto.randomUUID()}`,
546
+ gatewaySession = await withRuntimeLoadTimeout(
547
+ "gateway.open",
548
+ () => candidateSnapshot.bindings.gateway!.open(
549
+ `${this.ctx.id.toString()}:${crypto.randomUUID()}`,
550
+ ),
551
+ {
552
+ onLateResult: (session) =>
553
+ closeRuntimeGatewaySession(session, "gateway.close.late"),
554
+ },
535
555
  );
536
556
  } catch {
537
557
  gatewayDegradations.push({
@@ -557,7 +577,7 @@ export abstract class AgentRuntimeKernel<
557
577
  ),
558
578
  });
559
579
  } catch (error) {
560
- await gatewaySession?.close().catch(() => undefined);
580
+ await closeRuntimeGatewaySession(gatewaySession, "gateway.close.failed");
561
581
  throw error;
562
582
  }
563
583
  const revision = await this.hash(prepared.revisionDescriptor);
@@ -569,18 +589,23 @@ export abstract class AgentRuntimeKernel<
569
589
  this.submissions.isBusy();
570
590
  const parked = contested && this.db.everyUnfinishedSubmissionParked();
571
591
  if (contested && !parked) {
572
- await gatewaySession?.close().catch(() => undefined);
592
+ await closeRuntimeGatewaySession(gatewaySession, "gateway.close.contested");
573
593
  throw new Error(
574
594
  "Cannot reload Runtime while a revision-pinned Pi Turn is active",
575
595
  );
576
596
  }
577
597
  const repin = parked
578
- ? await this.prepareParkedSubmissionRepin(prepared, revision)
598
+ ? await withRuntimeLoadTimeout(
599
+ "parked-submission.repin",
600
+ () => this.prepareParkedSubmissionRepin(prepared, revision),
601
+ )
579
602
  : undefined;
580
603
  let activated = false;
581
604
  const previousGatewaySession = this.runtimeGatewaySession;
582
605
  try {
583
- for (const guard of candidate.commitGuards) await guard();
606
+ for (const [index, guard] of candidate.commitGuards.entries()) {
607
+ await withRuntimeLoadTimeout(`commit-guard:${index}`, guard);
608
+ }
584
609
  if (candidateSnapshot.bindings.platform.telemetryConsole) {
585
610
  installConsoleSink();
586
611
  }
@@ -594,13 +619,19 @@ export abstract class AgentRuntimeKernel<
594
619
  } catch (error) {
595
620
  repin?.abort();
596
621
  if (activated && previous) this.pi.activate(previous);
597
- await gatewaySession?.close().catch(() => undefined);
622
+ await closeRuntimeGatewaySession(gatewaySession, "gateway.close.aborted");
598
623
  throw error;
599
624
  }
600
- await repin?.interrupt().catch((error) => {
601
- console.error("[runtime-repin:interrupt-failed]", error);
602
- });
603
- await previousGatewaySession?.close().catch(() => undefined);
625
+ if (repin) {
626
+ await withRuntimeLoadTimeout(
627
+ "parked-submission.interrupt",
628
+ () => repin.interrupt(),
629
+ ).catch(() => undefined);
630
+ }
631
+ await closeRuntimeGatewaySession(
632
+ previousGatewaySession,
633
+ "gateway.close.previous",
634
+ );
604
635
 
605
636
  const degradations = prepared.degradations;
606
637
  if (degradations.length > 0) {
@@ -2099,7 +2130,9 @@ export abstract class AgentRuntimeKernel<
2099
2130
  continuation: recovery,
2100
2131
  assistantOrdinal,
2101
2132
  onRecord: (record) =>
2102
- this.persistAndBroadcastRecord(turn, record),
2133
+ this.migratedSubmissions.has(submissionId)
2134
+ ? undefined
2135
+ : this.persistAndBroadcastRecord(turn, record),
2103
2136
  onTerminal: (intent) => {
2104
2137
  terminalIntent = intent;
2105
2138
  this.appendTerminalIntent(
@@ -1,71 +0,0 @@
1
- import {
2
- defineRuntimeAgent,
3
- type MutableRuntimeAgentDefinition,
4
- type ReadonlyRuntimeAgentDefinition,
5
- type RuntimeAgentClass,
6
- type RuntimeAgentInstance,
7
- } from "../../../runtime-agent";
8
-
9
- export type CloudflareUniversalAgentInstance<
10
- Env extends Cloudflare.Env = Cloudflare.Env,
11
- Command = never,
12
- Change = never,
13
- > = RuntimeAgentInstance<Env, Command, Change> & {
14
- revokeUserConnections(): Promise<void>;
15
- };
16
-
17
- export interface CloudflareUniversalAgentClass<
18
- Env extends Cloudflare.Env = Cloudflare.Env,
19
- Command = never,
20
- Change = never,
21
- > {
22
- new (
23
- ctx: DurableObjectState,
24
- env: Env,
25
- ): CloudflareUniversalAgentInstance<Env, Command, Change>;
26
- }
27
-
28
- export function defineCloudflareUniversalAgent<
29
- Env extends Cloudflare.Env,
30
- Config,
31
- Command,
32
- Change,
33
- >(
34
- definition: MutableRuntimeAgentDefinition<Env, Config, Command, Change>,
35
- ): CloudflareUniversalAgentClass<Env, Command, Change>;
36
- export function defineCloudflareUniversalAgent<
37
- Env extends Cloudflare.Env = Cloudflare.Env,
38
- Config = unknown,
39
- >(
40
- definition: ReadonlyRuntimeAgentDefinition<Env, Config>,
41
- ): CloudflareUniversalAgentClass<Env>;
42
- export function defineCloudflareUniversalAgent<
43
- Env extends Cloudflare.Env,
44
- Config,
45
- Command,
46
- Change,
47
- >(
48
- definition:
49
- | MutableRuntimeAgentDefinition<Env, Config, Command, Change>
50
- | ReadonlyRuntimeAgentDefinition<Env, Config>,
51
- ): CloudflareUniversalAgentClass<Env, Command, Change> {
52
- const Base = defineRuntimeAgent(
53
- definition as MutableRuntimeAgentDefinition<
54
- Env,
55
- Config,
56
- Command,
57
- Change
58
- >,
59
- ) as RuntimeAgentClass<Env, Command, Change>;
60
- const Generated = {
61
- [definition.name]: class extends Base {
62
- async revokeUserConnections(): Promise<void> {
63
- await this.stopTurn(undefined, "Session ended");
64
- for (const connection of this.getConnections()) {
65
- connection.close(1008, "Session ended");
66
- }
67
- }
68
- },
69
- }[definition.name];
70
- return Generated as CloudflareUniversalAgentClass<Env, Command, Change>;
71
- }