@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
@@ -1,9 +1,8 @@
1
1
  import {
2
2
  callable,
3
3
  type Agent,
4
- type AgentToolRunStatus,
4
+ type AgentToolChildAdapter,
5
5
  type ChatCapableAgentClass,
6
- type RunAgentToolOptions,
7
6
  } from "agents";
8
7
  import type { UIMessage } from "ai";
9
8
  import type { PiCanonicalTranscriptSnapshot } from "./pi/runtime-adapter";
@@ -14,20 +13,52 @@ import type {
14
13
  MessageDispatchReceipt,
15
14
  SubmissionReceipt,
16
15
  } from "./kernel/receipts";
17
- import type { ExecutionLevel } from "./lib/execution-level";
18
16
  import type { RuntimeState } from "./kernel/state";
19
17
  import type {
20
18
  TemporaryAgentApprovalDecision,
21
19
  TemporaryAgentApprovalRequest,
22
- TemporaryAgentExecutor,
23
- TemporaryAgentRequest,
24
- TemporaryAgentRunContext,
20
+ TemporaryAgentLaunch,
25
21
  } from "./layers/orchestration/temporary-agent/core";
26
- import type { AgentConfig } from "./plugins";
27
- import { AgentRuntimeKernel } from "./runtime";
22
+ import { TEMPORARY_AGENT_LAUNCH_KEY } from "./layers/orchestration/temporary-agent/core";
23
+ import {
24
+ temporaryAgentExtensionIsSafe,
25
+ temporaryAgentToolAllowed,
26
+ } from "./layers/orchestration/temporary-agent/runner";
27
+ import {
28
+ EXECUTION_LEVELS,
29
+ requiresExecutionApproval,
30
+ type ExecutionLevel,
31
+ } from "./lib/execution-level";
32
+ import {
33
+ assembleRuntimeSnapshot,
34
+ type RuntimeCandidate,
35
+ } from "./runtime-assembler";
36
+ import type {
37
+ ResolvedResources,
38
+ RuntimeAgentHooks,
39
+ } from "./runtime-definition";
40
+ import {
41
+ normalizeToolAssembly,
42
+ type ToolAssemblyResult,
43
+ type ToolRegistry,
44
+ } from "./tool-registry";
45
+ import type { RuntimeAssemblyView } from "./kernel/runtime-assembly-view";
46
+ import type { RuntimeConfigUpdateResult } from "./kernel/runtime-config";
47
+ import {
48
+ logRuntimeLoadFailure,
49
+ RUNTIME_LOAD_TIMEOUT_MS,
50
+ withRuntimeLoadTimeout,
51
+ } from "./kernel/runtime-load";
52
+ import { AgentToolRuntimeKernel } from "./agent-tool-runtime";
53
+ import type {
54
+ RuntimeAgentConfigContext,
55
+ RuntimeAgentPlanningContext,
56
+ RuntimeAgentRole,
57
+ RuntimeAgentToolResult,
58
+ } from "./runtime-agent-context";
28
59
 
29
60
  /**
30
- * 本文件把应用提供的 `createConfig` 接到 Cloudflare Agent 生命周期。
61
+ * 本文件把应用提供的 Config Definition 与 Planner 接到 Cloudflare Agent 生命周期。
31
62
  *
32
63
  * @remarks
33
64
  * 核心术语见包入口 `index.ts`。
@@ -35,201 +66,81 @@ import { AgentRuntimeKernel } from "./runtime";
35
66
 
36
67
  // #region Agent 身份与公开契约
37
68
 
38
- const RESERVED_IDENTIFIERS = new Set([
39
- "arguments",
40
- "await",
41
- "break",
42
- "case",
43
- "catch",
44
- "class",
45
- "const",
46
- "continue",
47
- "debugger",
48
- "default",
49
- "delete",
50
- "do",
51
- "else",
52
- "enum",
53
- "eval",
54
- "export",
55
- "extends",
56
- "false",
57
- "finally",
58
- "for",
59
- "function",
60
- "if",
61
- "implements",
62
- "import",
63
- "in",
64
- "instanceof",
65
- "interface",
66
- "let",
67
- "new",
68
- "null",
69
- "package",
70
- "private",
71
- "protected",
72
- "public",
73
- "return",
74
- "static",
75
- "super",
76
- "switch",
77
- "this",
78
- "throw",
79
- "true",
80
- "try",
81
- "typeof",
82
- "var",
83
- "void",
84
- "while",
85
- "with",
86
- "yield",
87
- ]);
88
- const JAVASCRIPT_IDENTIFIER =
89
- /^[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*$/u;
90
-
91
- /**
92
- * 描述 Agent 寻址路径中的一级父子节点。
93
- *
94
- * @remarks
95
- * Cloudflare Agents SDK 用 `parentPath` 暴露根节点到直接父节点的路径。
96
- *
97
- * 调用方通常只读取,不应自行构造后写回 SDK。
98
- */
99
- export interface RuntimeAgentPathStep {
100
- className: string;
101
- name: string;
69
+ export type {
70
+ RuntimeAgentConfigContext,
71
+ RuntimeAgentContext,
72
+ RuntimeAgentPathStep,
73
+ RuntimeAgentPlanningContext,
74
+ RuntimeAgentRole,
75
+ RuntimeAgentToolResult,
76
+ RuntimeAssemblyContext,
77
+ } from "./runtime-agent-context";
78
+
79
+ /** Host 一次性准备的完整 Runtime 加载结果。 */
80
+ export interface LoadedRuntime<Config> {
81
+ readonly runtimeKey: string;
82
+ readonly config: Config;
83
+ readonly resources: ResolvedResources;
102
84
  }
103
85
 
104
- /**
105
- * 表示一次 Agent Tool 执行返回给 Runtime 的稳定字段。
106
- *
107
- * @remarks
108
- * Subagent 端口在调用 `RuntimeAgentConfigContext.runAgentTool` 后读取它。
109
- *
110
- * 这里只保留 Runtime 需要的结果,避免把 SDK 内部运行对象暴露给应用。
111
- */
112
- export interface RuntimeAgentToolResult {
113
- status: string;
114
- runId: string;
115
- output?: unknown;
116
- summary?: string;
117
- error?: string;
86
+ export interface RuntimeConfigUpdate<Change> {
87
+ readonly changed: boolean;
88
+ readonly change: Change;
118
89
  }
119
90
 
120
- /**
121
- * 这是应用创建 Config 时唯一可以接触的 Agent 实例能力。
122
- *
123
- * 身份通过实时 getter 暴露。
124
- *
125
- * 待确认:原注释把 getter 的必要性归因于 facet 元数据在 Durable Object
126
- * 构造后恢复,但当前 SDK 会在用户 `onStart` 前完成恢复。
127
- */
128
- export interface RuntimeAgentConfigContext<
129
- Env extends Cloudflare.Env = Cloudflare.Env,
91
+ interface RuntimeAgentDefinitionBase<
92
+ Env extends Cloudflare.Env,
93
+ Config,
94
+ Command,
95
+ Change,
130
96
  > {
131
- /** 读取当前 Durable Object 的状态句柄。 */
132
- readonly ctx: DurableObjectState;
133
- /** 读取当前 Worker 的环境绑定。 */
134
- readonly env: Env;
135
- /** 读取当前 Agent 实例名称。 */
97
+ /** 必须等于 Worker 导出名(Wrangler `class_name`)。 */
136
98
  readonly name: string;
137
- /** 读取从根节点到直接父节点的 Agent 路径。 */
138
- readonly parentPath: readonly RuntimeAgentPathStep[];
139
- /**
140
- * 告诉生成类本次配置最终解析到了哪个 Runtime key。
141
- *
142
- * @remarks
143
- * `createConfig` 在完成业务寻址后调用,同一次创建只能报告一个非空值。
144
- *
145
- * 生成类用该 key 合并重复加载;它只是运行时选择,不会写回业务配置。
146
- */
147
- setResolvedRuntimeKey(runtimeKey: string): void;
148
- /**
149
- * 请求重新装配一次 Runtime,让业务侧刚写入的配置立刻生效。
150
- *
151
- * @remarks
152
- * 给「运行中改变自身可用能力」的 Tool 使用:先把变更写进持久层,再调用它,
153
- * 新的 Snapshot 会按同一条解析链重新装配。不要在 Plugin 的 loader 里调用,
154
- * 那会让装配递归。
155
- */
156
- reloadRuntime(): Promise<void>;
157
- /**
158
- * 通过当前 Agent 调用一个受 SDK 管理的子 Agent Tool。
159
- *
160
- * @remarks
161
- * subagent 端口在父 Agent 需要保留子运行状态、事件和取消边界时调用。
162
- *
163
- * 调用必须经过当前 Agent 实例,才能使用 Cloudflare Agents SDK 的受管子运行登记。
164
- */
165
- runAgentTool(
166
- agentClass: ChatCapableAgentClass,
167
- options: RunAgentToolOptions<unknown>,
168
- ): Promise<RuntimeAgentToolResult>;
169
- /**
170
- * 清理符合年龄和状态条件的 SDK 子运行记录。
171
- *
172
- * @remarks
173
- * subagent 清理逻辑在需要回收已保留运行时调用。
174
- *
175
- * 必须显式提供年龄和状态筛选,避免无边界地删除仍需查询的子运行。
176
- */
177
- clearAgentToolRuns(options: {
178
- olderThan: number;
179
- status: AgentToolRunStatus[];
180
- }): Promise<void>;
181
- /** 读取当前已装配 User Agent 的执行档位。 */
182
- executionLevel(): ExecutionLevel;
183
- /**
184
- * 在当前 Session Runtime 中执行一个仅对本次调用存活的临时 Agent。
185
- *
186
- * @remarks
187
- * temporary-agent 端口在 Tool 需要一次性委派时调用。
188
- *
189
- * 它复用当前 Session 的 Runtime 和取消信号,但不创建持久 facet。
190
- */
191
- runTemporaryAgent(
192
- request: TemporaryAgentRequest,
193
- context: TemporaryAgentRunContext,
194
- execute: TemporaryAgentExecutor,
195
- ): Promise<string>;
99
+
100
+ readonly load: (
101
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
102
+ requestedRuntimeKey?: string,
103
+ ) => Promise<LoadedRuntime<Config>>;
104
+
105
+ readonly tools: (
106
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
107
+ config: Config,
108
+ ) => ToolRegistry | ToolAssemblyResult | Promise<ToolRegistry | ToolAssemblyResult>;
109
+
110
+ readonly hooks?:
111
+ | RuntimeAgentHooks<Env, Config, Command, Change>
112
+ | ((context: RuntimeAgentPlanningContext<Env, Command, Change>) =>
113
+ RuntimeAgentHooks<Env, Config, Command, Change>);
196
114
  }
197
115
 
198
- /**
199
- * 告诉 Runtime 如何定义 Agent 类并创建配置。
200
- *
201
- * @remarks
202
- * Worker 入口把它传给 `defineRuntimeAgent`。
203
- *
204
- * `createConfig` 只负责业务寻址和 Plugin 创建,原子装配由 Runtime 完成。
205
- */
206
- export interface RuntimeAgentDefinition<
116
+ export interface ReadonlyRuntimeAgentDefinition<
207
117
  Env extends Cloudflare.Env = Cloudflare.Env,
208
- > {
209
- /**
210
- * 必须等于 Worker 导出名。
211
- *
212
- * 顶层 Agent 还必须与 Wrangler `class_name` 一致。
213
- *
214
- * 纯子 facet 不需要自己的顶层绑定。
215
- *
216
- * Agents SDK 使用构造器名称解析 facet 回调。
217
- */
218
- className: string;
219
- /**
220
- * 为当前 Agent 实例创建一次完整的 Plugin 配置。
221
- *
222
- * @remarks
223
- * 生成类在首次执行需求和显式切换 Runtime key 时调用。
224
- *
225
- * 返回值尚未生效,只有 Runtime 完成校验和提交后才会替换当前 Snapshot。
226
- */
227
- createConfig(
118
+ Config = unknown,
119
+ > extends RuntimeAgentDefinitionBase<Env, Config, never, never> {
120
+ readonly update?: never;
121
+ }
122
+
123
+ export interface MutableRuntimeAgentDefinition<
124
+ Env extends Cloudflare.Env = Cloudflare.Env,
125
+ Config = unknown,
126
+ Command = unknown,
127
+ Change = unknown,
128
+ > extends RuntimeAgentDefinitionBase<Env, Config, Command, Change> {
129
+ readonly update: (
228
130
  context: RuntimeAgentConfigContext<Env>,
229
- runtimeKey?: string,
230
- ): Promise<AgentConfig>;
131
+ command: Command,
132
+ ) => Promise<RuntimeConfigUpdate<Change>>;
231
133
  }
232
134
 
135
+ export type RuntimeAgentDefinition<
136
+ Env extends Cloudflare.Env = Cloudflare.Env,
137
+ Config = unknown,
138
+ Command = never,
139
+ Change = never,
140
+ > = [Command] extends [never]
141
+ ? ReadonlyRuntimeAgentDefinition<Env, Config>
142
+ : MutableRuntimeAgentDefinition<Env, Config, Command, Change>;
143
+
233
144
  /**
234
145
  * 这是生成 Runtime Agent 的稳定公开控制面。
235
146
  *
@@ -257,6 +168,14 @@ export interface RuntimeAgentControls {
257
168
  * 待确认:当前 `refreshContext` 只检查 Snapshot 已安装,没有重新读取 Context 或刷新缓存。
258
169
  */
259
170
  refreshMemoryContext(): Promise<void>;
171
+ /**
172
+ * Read the Runtime Snapshot currently installed on this Session facet.
173
+ *
174
+ * @remarks
175
+ * Debug panels and Host tooling call this to inspect skills, subagents,
176
+ * extensions, and degradations that were actually assembled.
177
+ */
178
+ getRuntimeAssembly(): Promise<RuntimeAssemblyView>;
260
179
  /**
261
180
  * 读取 canonical transcript 的浏览器投影。
262
181
  *
@@ -286,6 +205,16 @@ export interface RuntimeAgentControls {
286
205
  executionId: string,
287
206
  decision: ApprovalDecision,
288
207
  ): Promise<{ ok: boolean }>;
208
+ /**
209
+ * 把客户端投递的响应作为某次 Tool 调用的结果并续跑原 Turn。
210
+ *
211
+ * @remarks
212
+ * 前端经 WS RPC 调用,只带 `toolCallId`。查无、已结算或响应体不过校验都返回 `{ ok: false }`。
213
+ */
214
+ respondToolInteraction(
215
+ toolCallId: string,
216
+ response: unknown,
217
+ ): Promise<{ ok: boolean }>;
289
218
  /**
290
219
  * 登记一条受管临时 Agent 发起的审批请求。
291
220
  *
@@ -375,6 +304,12 @@ export interface RuntimeAgentControls {
375
304
  ): Promise<{ ok: boolean }>;
376
305
  }
377
306
 
307
+ export interface RuntimeAgentConfigControls<Command, Change> {
308
+ updateConfig(
309
+ command: Command,
310
+ ): Promise<RuntimeConfigUpdateResult<Change>>;
311
+ }
312
+
378
313
  /**
379
314
  * 表示 `defineRuntimeAgent` 生成类的实例。
380
315
  *
@@ -383,7 +318,10 @@ export interface RuntimeAgentControls {
383
318
  */
384
319
  export type RuntimeAgentInstance<
385
320
  Env extends Cloudflare.Env = Cloudflare.Env,
386
- > = Agent<Env, RuntimeState> & RuntimeAgentControls;
321
+ Command = never,
322
+ Change = never,
323
+ > = Agent<Env, RuntimeState> & RuntimeAgentControls &
324
+ RuntimeAgentConfigControls<Command, Change> & AgentToolChildAdapter<unknown, string>;
387
325
 
388
326
  /**
389
327
  * 表示 `defineRuntimeAgent` 返回的 Durable Object 类构造器。
@@ -393,26 +331,58 @@ export type RuntimeAgentInstance<
393
331
  */
394
332
  export interface RuntimeAgentClass<
395
333
  Env extends Cloudflare.Env = Cloudflare.Env,
334
+ Command = never,
335
+ Change = never,
396
336
  > {
397
- new (ctx: DurableObjectState, env: Env): RuntimeAgentInstance<Env>;
337
+ new (
338
+ ctx: DurableObjectState,
339
+ env: Env,
340
+ ): RuntimeAgentInstance<Env, Command, Change>;
398
341
  }
399
342
 
400
343
  // #endregion
401
344
 
402
345
  // #region 生成类的构造与生命周期
403
346
 
404
- // 作用:确认导出类名是可以安全创建的 JavaScript 标识符。
405
- // 调用:`defineRuntimeAgent` 在生成类之前调用一次。
406
- // 原因:动态计算类名时提前拒绝关键字,避免部署后才出现寻址失败。
407
- function assertClassName(className: string): void {
347
+ const TERMINAL_AGENT_TOOL_STATUSES = [
348
+ "completed",
349
+ "error",
350
+ "aborted",
351
+ "interrupted",
352
+ ] as const;
353
+ const AGENT_TOOL_RETENTION_MS = 24 * 60 * 60 * 1_000;
354
+
355
+ function parseTemporaryAgentLaunch<Config>(
356
+ input: unknown,
357
+ ): TemporaryAgentLaunch<Config> {
358
+ const record = input != null && typeof input === "object"
359
+ ? input as Record<string, unknown>
360
+ : {};
361
+ const read = (name: string, max: number) => {
362
+ const value = record[name];
363
+ if (typeof value !== "string" || !value.trim() || value.length > max) {
364
+ throw new Error(`invalid temporary agent ${name}`);
365
+ }
366
+ return value.trim();
367
+ };
368
+ const executionLevel = record.executionLevel;
408
369
  if (
409
- !JAVASCRIPT_IDENTIFIER.test(className) ||
410
- RESERVED_IDENTIFIERS.has(className)
370
+ typeof executionLevel !== "string" ||
371
+ !EXECUTION_LEVELS.includes(executionLevel as ExecutionLevel)
411
372
  ) {
412
- throw new Error(
413
- `Invalid Runtime Agent className "${className}": expected a non-reserved JavaScript identifier`,
414
- );
373
+ throw new Error("invalid temporary agent executionLevel");
415
374
  }
375
+ if (!("config" in record)) {
376
+ throw new Error("invalid temporary agent config");
377
+ }
378
+ return {
379
+ subagentName: read("subagentName", 128),
380
+ instructions: read("instructions", 65_536),
381
+ task: read("task", 65_536),
382
+ runtimeKey: read("runtimeKey", 512),
383
+ config: record.config as Config,
384
+ executionLevel: executionLevel as ExecutionLevel,
385
+ };
416
386
  }
417
387
 
418
388
  /**
@@ -425,20 +395,44 @@ function assertClassName(className: string): void {
425
395
  */
426
396
  export function defineRuntimeAgent<
427
397
  Env extends Cloudflare.Env = Cloudflare.Env,
398
+ Config = unknown,
399
+ Command = never,
400
+ Change = never,
428
401
  >(
429
- definition: RuntimeAgentDefinition<Env>,
430
- ): RuntimeAgentClass<Env> {
431
- assertClassName(definition.className);
402
+ definition:
403
+ | MutableRuntimeAgentDefinition<Env, Config, Command, Change>
404
+ | ReadonlyRuntimeAgentDefinition<Env, Config>,
405
+ ): RuntimeAgentClass<Env, Command, Change> {
406
+ const resolveDefinitionHooks = (
407
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
408
+ ): RuntimeAgentHooks<Env, Config, Command, Change> | undefined => {
409
+ const hooks = definition.hooks;
410
+ if (!hooks) return undefined;
411
+ return typeof hooks === "function"
412
+ ? (hooks as (
413
+ ctx: RuntimeAgentPlanningContext<Env, Command, Change>,
414
+ ) => RuntimeAgentHooks<Env, Config, Command, Change>)(context)
415
+ : hooks as RuntimeAgentHooks<Env, Config, Command, Change>;
416
+ };
432
417
 
433
418
  const GeneratedRuntimeAgent = {
434
- [definition.className]: class extends AgentRuntimeKernel<Env> {
419
+ [definition.name]: class extends AgentToolRuntimeKernel<Env> {
435
420
  private hasLoadedRuntime = false;
436
421
  private loadedRuntimeKey?: string;
437
422
  private loading?: Promise<void>;
423
+ private temporaryLaunch?: TemporaryAgentLaunch<Config>;
424
+ private temporaryDispose?: () => Promise<void>;
425
+ private temporaryCleanup?: Promise<void>;
426
+
427
+ protected get role(): RuntimeAgentRole {
428
+ return this.parentPath.at(-1)?.className === definition.name
429
+ ? "temporary"
430
+ : "primary";
431
+ }
438
432
 
439
433
  // 作用:在 Kernel 真正消费 RuntimeSnapshot 前确保配置已完整提交。
440
434
  // 调用:Submission 准入、恢复、审批续跑与 Runtime Workspace 路径调用。
441
- // 原因:Session 本地读取不应支付 Config、Plugin、MCP 和 Pi 装配成本。
435
+ // 原因:Session 本地读取不应支付 Config、Assembly、MCP 和 Pi 装配成本。
442
436
  protected ensureRuntimeReady(): Promise<void> {
443
437
  return this.ensureConfig();
444
438
  }
@@ -453,6 +447,28 @@ export function defineRuntimeAgent<
453
447
  await this.ensureConfig(runtimeKey, options?.force === true);
454
448
  }
455
449
 
450
+ async updateConfig(
451
+ command: Command,
452
+ ): Promise<RuntimeConfigUpdateResult<Change>> {
453
+ if (this.role === "temporary") {
454
+ throw new Error("Temporary agents cannot update Runtime Config");
455
+ }
456
+ const update = definition.update;
457
+ if (!update) {
458
+ return { change: undefined as Change, runtime: "unchanged" };
459
+ }
460
+ const result = await update(this.createDefinitionContext(), command);
461
+ if (!result.changed) {
462
+ return { change: result.change, runtime: "unchanged" };
463
+ }
464
+ try {
465
+ await this.ensureConfig(undefined, true);
466
+ return { change: result.change, runtime: "reloaded" };
467
+ } catch {
468
+ return { change: result.change, runtime: "reload-failed" };
469
+ }
470
+ }
471
+
456
472
  // 作用:确保 Runtime 可用后请求 Kernel 刷新 Session Context。
457
473
  // 调用:Inbox 等 RPC 调用方在外部 Context 变更后调用。
458
474
  // 待确认:当前 Kernel 的 `refreshContext` 只检查 Snapshot,并未执行实际刷新。
@@ -461,6 +477,11 @@ export function defineRuntimeAgent<
461
477
  await this.refreshContext();
462
478
  }
463
479
 
480
+ async getRuntimeAssembly(): Promise<RuntimeAssemblyView> {
481
+ await this.ensureRuntimeReady();
482
+ return this.readRuntimeAssembly();
483
+ }
484
+
464
485
  dispatchMessage(
465
486
  message: UIMessage,
466
487
  delivery: MessageDelivery,
@@ -481,6 +502,13 @@ export function defineRuntimeAgent<
481
502
  return super.cancelSubmissionById(submissionId, reason);
482
503
  }
483
504
 
505
+ respondToolInteraction(
506
+ toolCallId: string,
507
+ response: unknown,
508
+ ): Promise<{ ok: boolean }> {
509
+ return super.respondToolInteraction(toolCallId, response);
510
+ }
511
+
484
512
  stopTurn(
485
513
  requestId?: string,
486
514
  reason?: string,
@@ -488,155 +516,366 @@ export function defineRuntimeAgent<
488
516
  return super.stopTurn(requestId, reason);
489
517
  }
490
518
 
519
+ protected override async prepareAgentToolRun(
520
+ input: unknown,
521
+ ): Promise<string> {
522
+ if (this.role !== "temporary") {
523
+ throw new Error("Agent Tool runs require a temporary child facet");
524
+ }
525
+ const launch = parseTemporaryAgentLaunch<Config>(input);
526
+ this.temporaryLaunch = launch;
527
+ await this.ctx.storage.put(TEMPORARY_AGENT_LAUNCH_KEY, launch);
528
+ await this.reloadRuntime(launch.runtimeKey, { force: true });
529
+ return launch.task;
530
+ }
531
+
491
532
  // 作用:把首次加载和按 key 重载合并成一个串行入口。
492
533
  // 调用:Runtime readiness、Session 配置和重载都会调用。
493
534
  // 原因:共享同一个 Promise 可合并并发首加载,显式切 key 则等待前序完成后再判断。
494
- private ensureConfig(
535
+ protected ensureConfig(
495
536
  requestedRuntimeKey?: string,
496
537
  force = false,
497
538
  fromQueue = false,
498
539
  ): Promise<void> {
499
- if (this.loading && !fromQueue) {
500
- if (!requestedRuntimeKey && !force) return this.loading;
501
-
502
- // 显式 key 或强制刷新不能被正在进行的其他加载吞掉,必须等它结束后重新核对。
503
- const queued = this.loading
504
- .catch(() => undefined)
505
- .then(() => {
506
- if (
507
- !force &&
508
- this.hasLoadedRuntime &&
509
- this.loadedRuntimeKey === requestedRuntimeKey
510
- ) {
511
- return;
512
- }
513
- return this.ensureConfig(
540
+ if (this.loading && !fromQueue) {
541
+ if (!requestedRuntimeKey && !force) return this.loading;
542
+
543
+ // 显式 key 或强制刷新不能被正在进行的其他加载吞掉,必须等它结束后重新核对。
544
+ const queued = this.loading
545
+ .catch(() => undefined)
546
+ .then(() => {
547
+ if (
548
+ !force &&
549
+ this.hasLoadedRuntime &&
550
+ this.loadedRuntimeKey === requestedRuntimeKey
551
+ ) {
552
+ return;
553
+ }
554
+ return this.ensureConfig(
555
+ requestedRuntimeKey,
556
+ force,
557
+ true,
558
+ );
559
+ });
560
+ let tracked: Promise<void>;
561
+ tracked = queued.finally(() => {
562
+ if (this.loading === tracked) this.loading = undefined;
563
+ });
564
+ this.loading = tracked;
565
+ return tracked;
566
+ }
567
+
568
+ if (
569
+ !force &&
570
+ this.hasLoadedRuntime &&
571
+ (!requestedRuntimeKey ||
572
+ requestedRuntimeKey === this.loadedRuntimeKey)
573
+ ) {
574
+ return Promise.resolve();
575
+ }
576
+
577
+ // 先创建局部 Promise,再赋给 `loading`,失败时也能在 finally 中放开重试。
578
+ const pending = (async () => {
579
+ this.runtimeLoad.begin();
580
+ const loadContext = this.createDefinitionContext();
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,
514
590
  requestedRuntimeKey,
515
- force,
516
- true,
517
- );
591
+ ),
592
+ { timeoutMs: RUNTIME_LOAD_TIMEOUT_MS },
593
+ );
594
+ const resolvedRuntimeKey = loaded.runtimeKey.trim();
595
+ if (!resolvedRuntimeKey) {
596
+ throw new Error("Resolved Runtime key must not be empty");
597
+ }
598
+ const context = this.createDefinitionContext({
599
+ value: loaded.config,
600
+ });
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
+ ));
611
+ let hooks = resolveDefinitionHooks(context);
612
+ if (this.role === "temporary") {
613
+ this.temporaryDispose = tools.dispose;
614
+ tools = this.applyTemporaryToolPolicy(tools);
615
+ hooks = await this.createTemporaryHooks();
616
+ }
617
+ const candidate = await assembleRuntimeSnapshot({
618
+ ctx: context,
619
+ config: loaded.config,
620
+ tools,
621
+ resources: loaded.resources,
622
+ hooks,
518
623
  });
624
+
625
+ await this.initCandidate(candidate);
626
+ this.hasLoadedRuntime = true;
627
+ this.loadedRuntimeKey = resolvedRuntimeKey;
628
+ this.runtimeLoad.complete();
629
+ // 提交之前 turnEvents 绑定还不存在,此前每一次广播都到不了 Host。
630
+ // Host 的会话列表把装配后的第一次投影当作「facet 已就绪」的信号。
631
+ await this.broadcastApprovals();
632
+ })().catch((error) => {
633
+ logRuntimeLoadFailure("runtime", error);
634
+ this.runtimeLoad.fail();
635
+ throw error;
636
+ });
637
+
638
+ if (fromQueue) return pending;
639
+
640
+ // 无论成功失败都清掉进行中标记,后续调用才可以重试或切 key。
519
641
  let tracked: Promise<void>;
520
- tracked = queued.finally(() => {
642
+ tracked = pending.finally(() => {
521
643
  if (this.loading === tracked) this.loading = undefined;
522
644
  });
523
645
  this.loading = tracked;
524
646
  return tracked;
525
647
  }
526
648
 
527
- if (
528
- !force &&
529
- this.hasLoadedRuntime &&
530
- (!requestedRuntimeKey ||
531
- requestedRuntimeKey === this.loadedRuntimeKey)
532
- ) {
533
- return Promise.resolve();
649
+ private applyTemporaryToolPolicy(
650
+ assembly: ToolAssemblyResult,
651
+ ): ToolAssemblyResult {
652
+ const policy = assembly.surfacePolicy;
653
+ return {
654
+ ...assembly,
655
+ bindings: assembly.bindings?.workspace
656
+ ? { workspace: assembly.bindings.workspace }
657
+ : {},
658
+ memoryProfile: {
659
+ enabled: false,
660
+ memoryTokens: 2_000,
661
+ preferencesTokens: 500,
662
+ },
663
+ enabledSubagents: [],
664
+ surfacePolicy: {
665
+ allowsTool: (name) =>
666
+ temporaryAgentToolAllowed(name) &&
667
+ policy?.allowsTool?.(name) !== false,
668
+ allowsExtension: (extension) =>
669
+ temporaryAgentExtensionIsSafe(extension) &&
670
+ policy?.allowsExtension?.(extension) !== false,
671
+ },
672
+ };
673
+ }
674
+
675
+ private async createTemporaryHooks(): Promise<
676
+ RuntimeAgentHooks<Env, Config, Command, Change>
677
+ > {
678
+ const launch = this.temporaryLaunch ??
679
+ await this.ctx.storage.get<TemporaryAgentLaunch<Config>>(
680
+ TEMPORARY_AGENT_LAUNCH_KEY,
681
+ );
682
+ if (!launch) throw new Error("Temporary Agent is not initialized");
683
+ this.temporaryLaunch = launch;
684
+ return {
685
+ profileOverrides: () => ({
686
+ systemPrompt: launch.instructions,
687
+ executionLevel: "high",
688
+ }),
689
+ gateTool: async (request) => {
690
+ if (!requiresExecutionApproval(
691
+ launch.executionLevel,
692
+ request.requiredExecutionLevel,
693
+ )) return;
694
+ await this.requestParentTemporaryApproval(launch, request);
695
+ },
696
+ };
697
+ }
698
+
699
+ private async requestParentTemporaryApproval(
700
+ launch: TemporaryAgentLaunch<Config>,
701
+ request: {
702
+ toolCallId: string;
703
+ toolName: string;
704
+ input: unknown;
705
+ requiredExecutionLevel: ExecutionLevel;
706
+ signal: AbortSignal;
707
+ },
708
+ ): Promise<void> {
709
+ if (request.signal.aborted) {
710
+ throw new Error("temporary agent was cancelled", {
711
+ cause: request.signal.reason,
712
+ });
534
713
  }
714
+ const parent = await this.parentAgent(
715
+ this.constructor as ChatCapableAgentClass,
716
+ ) as unknown as Pick<
717
+ RuntimeAgentControls,
718
+ "requestTemporaryAgentApproval" | "cancelTemporaryAgentApproval"
719
+ >;
720
+ const executionId =
721
+ `temporary-agent:${this.name}:${request.toolCallId}`;
722
+ let onAbort = () => {};
723
+ const aborted = new Promise<never>((_resolve, reject) => {
724
+ onAbort = () => {
725
+ void parent.cancelTemporaryAgentApproval(
726
+ executionId,
727
+ "temporary agent was cancelled",
728
+ );
729
+ reject(new Error("temporary agent was cancelled", {
730
+ cause: request.signal.reason,
731
+ }));
732
+ };
733
+ request.signal.addEventListener("abort", onAbort, { once: true });
734
+ });
735
+ try {
736
+ const decision = await Promise.race([
737
+ parent.requestTemporaryAgentApproval({
738
+ executionId,
739
+ subagentName: launch.subagentName,
740
+ requestId: `temporary-agent:${this.name}`,
741
+ toolCallId: request.toolCallId,
742
+ toolName: request.toolName,
743
+ executionLevel: launch.executionLevel,
744
+ requiredExecutionLevel: request.requiredExecutionLevel,
745
+ input: request.input,
746
+ }),
747
+ aborted,
748
+ ]);
749
+ if (!decision.approved) {
750
+ throw new Error(
751
+ `temporary agent "${launch.subagentName}" Tool ` +
752
+ `"${request.toolName}" was rejected: ` +
753
+ `${decision.reason?.trim() || "rejected by user"}`,
754
+ );
755
+ }
756
+ } finally {
757
+ request.signal.removeEventListener("abort", onAbort);
758
+ }
759
+ }
535
760
 
536
- // 先创建局部 Promise,再赋给 `loading`,失败时也能在 finally 中放开重试。
537
- const pending = (async () => {
538
- this.beginRuntimeLoad();
539
- let resolvedRuntimeKey: string | undefined;
540
- const agent = this;
541
- const config = await definition.createConfig(
542
- {
543
- ctx: this.ctx,
544
- env: this.env,
545
- // 作用:在创建配置时读取当前 Agent 实例名。
546
- // 调用:应用的 `createConfig` 在解析 Session 身份时读取。
547
- // 原因:使用实时 getter,不在上下文创建时复制 SDK 身份值。
548
- get name() {
549
- return agent.name;
550
- },
551
- // 作用:在创建配置时读取根到直接父节点的路径。
552
- // 调用:应用的 `createConfig` 在解析 facet 归属时读取。
553
- // 原因:路径由 Agents SDK 维护,Runtime 只转发而不另存副本。
554
- get parentPath() {
555
- return agent.parentPath;
556
- },
557
- // 作用:记住本次配置解析出的 Runtime key。
558
- // 调用:应用的 `createConfig` 在完成业务寻址后调用。
559
- // 原因:只接受一个非空结果,防止同一次装配在两个业务身份之间摇摆。
560
- setResolvedRuntimeKey(runtimeKey) {
561
- const normalized = runtimeKey.trim();
562
- if (!normalized) {
761
+ protected override cleanupAgentToolRun(): Promise<void> {
762
+ if (this.temporaryCleanup) return this.temporaryCleanup;
763
+ this.temporaryCleanup = (async () => {
764
+ if (!this.temporaryDispose) {
765
+ const launch = this.temporaryLaunch ??
766
+ await this.ctx.storage.get<TemporaryAgentLaunch<Config>>(
767
+ TEMPORARY_AGENT_LAUNCH_KEY,
768
+ );
769
+ if (launch) await this.ensureConfig(launch.runtimeKey);
770
+ }
771
+ await this.temporaryDispose?.();
772
+ })().catch((error) => {
773
+ console.warn("Temporary Agent resource cleanup failed", error);
774
+ });
775
+ return this.temporaryCleanup;
776
+ }
777
+
778
+ protected createDefinitionContext(
779
+ runtimeConfig?: { value: Config },
780
+ ): RuntimeAgentPlanningContext<
781
+ Env,
782
+ Command,
783
+ Change
784
+ > {
785
+ const agent = this;
786
+ return {
787
+ ctx: this.ctx,
788
+ env: this.env,
789
+ get name() {
790
+ return agent.name;
791
+ },
792
+ get parentPath() {
793
+ return agent.parentPath;
794
+ },
795
+ get role() {
796
+ return agent.role;
797
+ },
798
+ ...(agent.role === "primary"
799
+ ? {
800
+ updateConfig: (command: Command) =>
801
+ agent.updateConfig(command),
802
+ }
803
+ : {}),
804
+ runAgentTool: async (agentClass, options) =>
805
+ agent.runAgentTool(
806
+ agentClass,
807
+ options as never,
808
+ ) as Promise<RuntimeAgentToolResult>,
809
+ clearAgentToolRuns: (options) =>
810
+ agent.clearAgentToolRuns(options),
811
+ executionLevel: () => agent.executionLevel(),
812
+ runTemporaryAgent: (request, runContext) => {
813
+ if (agent.role !== "primary" || !runtimeConfig) {
814
+ throw new Error("Temporary Agent cannot delegate again");
815
+ }
816
+ return agent.runTemporaryAgent(
817
+ request,
818
+ runContext,
819
+ async (normalized) => {
820
+ await agent.clearAgentToolRuns({
821
+ olderThan: Date.now() - AGENT_TOOL_RETENTION_MS,
822
+ status: [...TERMINAL_AGENT_TOOL_STATUSES],
823
+ });
824
+ const result = await agent.runAgentTool(
825
+ agent.constructor as ChatCapableAgentClass,
826
+ {
827
+ input: {
828
+ ...normalized,
829
+ runtimeKey: agent.loadedRuntimeKey,
830
+ config: runtimeConfig.value,
831
+ executionLevel: agent.executionLevel(),
832
+ },
833
+ runId: `temporary-agent:${runContext.toolCallId}`,
834
+ parentToolCallId: runContext.toolCallId,
835
+ signal: runContext.signal,
836
+ inputPreview: normalized.task,
837
+ display: {
838
+ name: normalized.subagentName,
839
+ displayName: normalized.subagentName,
840
+ kind: "temporary",
841
+ },
842
+ },
843
+ );
844
+ if (result.status !== "completed") {
563
845
  throw new Error(
564
- "Resolved Runtime key must not be empty",
846
+ result.error ??
847
+ `temporary agent ended with status ${result.status}`,
565
848
  );
566
849
  }
567
850
  if (
568
- resolvedRuntimeKey &&
569
- resolvedRuntimeKey !== normalized
851
+ typeof result.summary !== "string" ||
852
+ !result.summary.trim()
570
853
  ) {
571
854
  throw new Error(
572
- "Resolved Runtime key cannot change during Config creation",
855
+ "temporary agent completed without final text",
573
856
  );
574
857
  }
575
- resolvedRuntimeKey = normalized;
858
+ return result.summary;
576
859
  },
577
- // 作用:强制按当前业务数据重新装配 Runtime。
578
- // 调用:会修改自身配置的 Tool 在持久化成功后调用。
579
- // 原因:配置内容可能已变而 key 不变,因此不能复用已加载标记。
580
- reloadRuntime: () => agent.ensureConfig(undefined, true),
581
- // 作用:通过当前 Agent 启动一个受 SDK 管理的子运行。
582
- // 调用:subagent 端口在委派 Agent Tool 时调用。
583
- // 原因:保留 Agents SDK 的运行登记、事件、取消和清理边界。
584
- runAgentTool: async (agentClass, options) =>
585
- agent.runAgentTool(
586
- agentClass,
587
- options as never,
588
- ) as Promise<RuntimeAgentToolResult>,
589
- // 作用:清理符合明确策略的已保留子运行。
590
- // 调用:subagent 端口在执行运行保留期回收时调用。
591
- // 原因:清理仍由 Agents SDK 执行,Runtime 不越过 SDK 直接修改子运行存储。
592
- clearAgentToolRuns: (options) =>
593
- agent.clearAgentToolRuns(options),
594
- // 作用:返回当前已装配 User Agent 的执行档位。
595
- // 调用:Plugin 在装配临时 Agent 审批桥时调用。
596
- // 原因:只读 Profile,不再维护 Session 覆盖状态。
597
- executionLevel: () => agent.executionLevel(),
598
- // 作用:在当前 Session 中执行一次性临时 Agent。
599
- // 调用:temporary-agent 端口在 Tool 需要调用内委派时调用。
600
- // 原因:交给 Kernel 统一管理重名、取消和审批,不创建持久 facet。
601
- runTemporaryAgent: (request, runContext, execute) =>
602
- agent.runTemporaryAgent(request, runContext, execute),
603
- },
604
- requestedRuntimeKey,
605
- );
606
-
607
- // 只有原子提交成功后才更新已加载标记和 key。
608
- await this.initConfig(config);
609
- this.hasLoadedRuntime = true;
610
- this.loadedRuntimeKey =
611
- resolvedRuntimeKey ?? requestedRuntimeKey;
612
- this.completeRuntimeLoad();
613
- })().catch((error) => {
614
- this.failRuntimeLoad();
615
- throw error;
616
- });
617
-
618
- if (fromQueue) return pending;
619
-
620
- // 无论成功失败都清掉进行中标记,后续调用才可以重试或切 key。
621
- let tracked: Promise<void>;
622
- tracked = pending.finally(() => {
623
- if (this.loading === tracked) this.loading = undefined;
624
- });
625
- this.loading = tracked;
626
- return tracked;
860
+ );
861
+ },
862
+ };
627
863
  }
628
864
  },
629
- }[definition.className];
865
+ }[definition.name];
630
866
 
631
- if (GeneratedRuntimeAgent.name !== definition.className) {
867
+ if (GeneratedRuntimeAgent.name !== definition.name) {
632
868
  throw new Error(
633
- `Runtime Agent class name mismatch: expected "${definition.className}", got "${GeneratedRuntimeAgent.name}"`,
869
+ `Runtime Agent class name mismatch: expected "${definition.name}", got "${GeneratedRuntimeAgent.name}"`,
634
870
  );
635
871
  }
636
872
 
637
873
  // Decorator syntax renames this computed class during Worker bundling.
638
874
  // Apply the same decorator function after class-name inference instead.
639
875
  const callableContext = {} as ClassMethodDecoratorContext;
876
+ callable()(GeneratedRuntimeAgent.prototype.reloadRuntime, callableContext);
877
+ callable()(GeneratedRuntimeAgent.prototype.updateConfig, callableContext);
878
+ callable()(GeneratedRuntimeAgent.prototype.getRuntimeAssembly, callableContext);
640
879
  callable()(GeneratedRuntimeAgent.prototype.dispatchMessage, callableContext);
641
880
  callable()(
642
881
  GeneratedRuntimeAgent.prototype.steerQueuedSubmission,
@@ -646,9 +885,17 @@ export function defineRuntimeAgent<
646
885
  GeneratedRuntimeAgent.prototype.cancelSubmissionById,
647
886
  callableContext,
648
887
  );
888
+ callable()(
889
+ GeneratedRuntimeAgent.prototype.respondToolInteraction,
890
+ callableContext,
891
+ );
649
892
  callable()(GeneratedRuntimeAgent.prototype.stopTurn, callableContext);
650
893
 
651
- return GeneratedRuntimeAgent as RuntimeAgentClass<Env>;
894
+ return GeneratedRuntimeAgent as unknown as RuntimeAgentClass<
895
+ Env,
896
+ Command,
897
+ Change
898
+ >;
652
899
  }
653
900
 
654
901
  // #endregion