@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,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,19 +13,49 @@ 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 { RuntimeAssemblyInput } from "./runtime-assembler";
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";
27
45
  import type { RuntimeAssemblyView } from "./kernel/runtime-assembly-view";
28
46
  import type { RuntimeConfigUpdateResult } from "./kernel/runtime-config";
29
- import { AgentRuntimeKernel } from "./runtime";
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";
30
59
 
31
60
  /**
32
61
  * 本文件把应用提供的 Config Definition 与 Planner 接到 Cloudflare Agent 生命周期。
@@ -37,165 +66,21 @@ import { AgentRuntimeKernel } from "./runtime";
37
66
 
38
67
  // #region Agent 身份与公开契约
39
68
 
40
- const RESERVED_IDENTIFIERS = new Set([
41
- "arguments",
42
- "await",
43
- "break",
44
- "case",
45
- "catch",
46
- "class",
47
- "const",
48
- "continue",
49
- "debugger",
50
- "default",
51
- "delete",
52
- "do",
53
- "else",
54
- "enum",
55
- "eval",
56
- "export",
57
- "extends",
58
- "false",
59
- "finally",
60
- "for",
61
- "function",
62
- "if",
63
- "implements",
64
- "import",
65
- "in",
66
- "instanceof",
67
- "interface",
68
- "let",
69
- "new",
70
- "null",
71
- "package",
72
- "private",
73
- "protected",
74
- "public",
75
- "return",
76
- "static",
77
- "super",
78
- "switch",
79
- "this",
80
- "throw",
81
- "true",
82
- "try",
83
- "typeof",
84
- "var",
85
- "void",
86
- "while",
87
- "with",
88
- "yield",
89
- ]);
90
- const JAVASCRIPT_IDENTIFIER =
91
- /^[$_\p{ID_Start}][$\u200C\u200D\p{ID_Continue}]*$/u;
92
-
93
- /**
94
- * 描述 Agent 寻址路径中的一级父子节点。
95
- *
96
- * @remarks
97
- * Cloudflare Agents SDK 用 `parentPath` 暴露根节点到直接父节点的路径。
98
- *
99
- * 调用方通常只读取,不应自行构造后写回 SDK。
100
- */
101
- export interface RuntimeAgentPathStep {
102
- className: string;
103
- name: string;
104
- }
105
-
106
- /**
107
- * 表示一次 Agent Tool 执行返回给 Runtime 的稳定字段。
108
- *
109
- * @remarks
110
- * Subagent 端口在调用 `RuntimeAgentConfigContext.runAgentTool` 后读取它。
111
- *
112
- * 这里只保留 Runtime 需要的结果,避免把 SDK 内部运行对象暴露给应用。
113
- */
114
- export interface RuntimeAgentToolResult {
115
- status: string;
116
- runId: string;
117
- output?: unknown;
118
- summary?: string;
119
- error?: string;
120
- }
121
-
122
- /**
123
- * 这是应用创建 Config 时唯一可以接触的 Agent 实例能力。
124
- *
125
- * 身份通过实时 getter 暴露。
126
- *
127
- * 待确认:原注释把 getter 的必要性归因于 facet 元数据在 Durable Object
128
- * 构造后恢复,但当前 SDK 会在用户 `onStart` 前完成恢复。
129
- */
130
- export interface RuntimeAgentConfigContext<
131
- Env extends Cloudflare.Env = Cloudflare.Env,
132
- > {
133
- /** 读取当前 Durable Object 的状态句柄。 */
134
- readonly ctx: DurableObjectState;
135
- /** 读取当前 Worker 的环境绑定。 */
136
- readonly env: Env;
137
- /** 读取当前 Agent 实例名称。 */
138
- readonly name: string;
139
- /** 读取从根节点到直接父节点的 Agent 路径。 */
140
- readonly parentPath: readonly RuntimeAgentPathStep[];
141
- /**
142
- * 通过当前 Agent 调用一个受 SDK 管理的子 Agent Tool。
143
- *
144
- * @remarks
145
- * subagent 端口在父 Agent 需要保留子运行状态、事件和取消边界时调用。
146
- *
147
- * 调用必须经过当前 Agent 实例,才能使用 Cloudflare Agents SDK 的受管子运行登记。
148
- */
149
- runAgentTool(
150
- agentClass: ChatCapableAgentClass,
151
- options: RunAgentToolOptions<unknown>,
152
- ): Promise<RuntimeAgentToolResult>;
153
- /**
154
- * 清理符合年龄和状态条件的 SDK 子运行记录。
155
- *
156
- * @remarks
157
- * subagent 清理逻辑在需要回收已保留运行时调用。
158
- *
159
- * 必须显式提供年龄和状态筛选,避免无边界地删除仍需查询的子运行。
160
- */
161
- clearAgentToolRuns(options: {
162
- olderThan: number;
163
- status: AgentToolRunStatus[];
164
- }): Promise<void>;
165
- /** 读取当前已装配 User Agent 的执行档位。 */
166
- executionLevel(): ExecutionLevel;
167
- /**
168
- * 在当前 Session Runtime 中执行一个仅对本次调用存活的临时 Agent。
169
- *
170
- * @remarks
171
- * temporary-agent 端口在 Tool 需要一次性委派时调用。
172
- *
173
- * 它复用当前 Session 的 Runtime 和取消信号,但不创建持久 facet。
174
- */
175
- runTemporaryAgent(
176
- request: TemporaryAgentRequest,
177
- context: TemporaryAgentRunContext,
178
- execute: TemporaryAgentExecutor,
179
- ): Promise<string>;
180
- }
181
-
182
- export interface RuntimeAgentPlanningContext<
183
- Env extends Cloudflare.Env = Cloudflare.Env,
184
- Command = never,
185
- Change = never,
186
- > extends RuntimeAgentConfigContext<Env> {
187
- /** 仅可变 Definition 提供;Host Tool 用它复用生成类的更新与重载编排。 */
188
- readonly updateConfig?: (
189
- command: Command,
190
- ) => Promise<RuntimeConfigUpdateResult<Change>>;
191
- }
192
-
193
- /**
194
- * Config 读取的显式结果;runtimeKey 是生成类唯一的缓存键。
195
- */
196
- export interface ResolvedRuntimeConfig<Config> {
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> {
197
81
  readonly runtimeKey: string;
198
82
  readonly config: Config;
83
+ readonly resources: ResolvedResources;
199
84
  }
200
85
 
201
86
  export interface RuntimeConfigUpdate<Change> {
@@ -203,60 +88,36 @@ export interface RuntimeConfigUpdate<Change> {
203
88
  readonly change: Change;
204
89
  }
205
90
 
206
- export interface RuntimeAgentConfigDefinition<
91
+ interface RuntimeAgentDefinitionBase<
207
92
  Env extends Cloudflare.Env,
208
93
  Config,
209
94
  Command,
210
95
  Change,
211
96
  > {
212
- read(
213
- context: RuntimeAgentConfigContext<Env>,
97
+ /** 必须等于 Worker 导出名(Wrangler `class_name`)。 */
98
+ readonly name: string;
99
+
100
+ readonly load: (
101
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
214
102
  requestedRuntimeKey?: string,
215
- ): Promise<ResolvedRuntimeConfig<Config>>;
216
- update?(
217
- context: RuntimeAgentConfigContext<Env>,
218
- command: Command,
219
- ): Promise<RuntimeConfigUpdate<Change>>;
220
- }
103
+ ) => Promise<LoadedRuntime<Config>>;
221
104
 
222
- /**
223
- * 告诉 Runtime 如何读取有效配置,并把它规划为一次扁平装配输入。
224
- *
225
- * @remarks
226
- * Worker 入口把它传给 `defineRuntimeAgent`。
227
- *
228
- * Config 只负责业务寻址,Planner 把已读取配置投影为装配输入。
229
- */
230
- interface RuntimeAgentDefinitionBase<
231
- Env extends Cloudflare.Env,
232
- Config,
233
- Command,
234
- Change,
235
- > {
236
- /**
237
- * 必须等于 Worker 导出名。
238
- *
239
- * 顶层 Agent 还必须与 Wrangler `class_name` 一致。
240
- *
241
- * 纯子 facet 不需要自己的顶层绑定。
242
- *
243
- * Agents SDK 使用构造器名称解析 facet 回调。
244
- */
245
- className: string;
246
- planRuntime(
105
+ readonly tools: (
247
106
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
248
107
  config: Config,
249
- ): Promise<RuntimeAssemblyInput>;
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>);
250
114
  }
251
115
 
252
116
  export interface ReadonlyRuntimeAgentDefinition<
253
117
  Env extends Cloudflare.Env = Cloudflare.Env,
254
118
  Config = unknown,
255
119
  > extends RuntimeAgentDefinitionBase<Env, Config, never, never> {
256
- readonly config: Omit<
257
- RuntimeAgentConfigDefinition<Env, Config, never, never>,
258
- "update"
259
- > & { readonly update?: never };
120
+ readonly update?: never;
260
121
  }
261
122
 
262
123
  export interface MutableRuntimeAgentDefinition<
@@ -265,14 +126,10 @@ export interface MutableRuntimeAgentDefinition<
265
126
  Command = unknown,
266
127
  Change = unknown,
267
128
  > extends RuntimeAgentDefinitionBase<Env, Config, Command, Change> {
268
- readonly config: RuntimeAgentConfigDefinition<
269
- Env,
270
- Config,
271
- Command,
272
- Change
273
- > & Required<
274
- Pick<RuntimeAgentConfigDefinition<Env, Config, Command, Change>, "update">
275
- >;
129
+ readonly update: (
130
+ context: RuntimeAgentConfigContext<Env>,
131
+ command: Command,
132
+ ) => Promise<RuntimeConfigUpdate<Change>>;
276
133
  }
277
134
 
278
135
  export type RuntimeAgentDefinition<
@@ -447,14 +304,11 @@ export interface RuntimeAgentControls {
447
304
  ): Promise<{ ok: boolean }>;
448
305
  }
449
306
 
450
- export type RuntimeAgentConfigControls<Command, Change> =
451
- [Command] extends [never]
452
- ? Record<never, never>
453
- : {
454
- updateConfig(
455
- command: Command,
456
- ): Promise<RuntimeConfigUpdateResult<Change>>;
457
- };
307
+ export interface RuntimeAgentConfigControls<Command, Change> {
308
+ updateConfig(
309
+ command: Command,
310
+ ): Promise<RuntimeConfigUpdateResult<Change>>;
311
+ }
458
312
 
459
313
  /**
460
314
  * 表示 `defineRuntimeAgent` 生成类的实例。
@@ -467,7 +321,7 @@ export type RuntimeAgentInstance<
467
321
  Command = never,
468
322
  Change = never,
469
323
  > = Agent<Env, RuntimeState> & RuntimeAgentControls &
470
- RuntimeAgentConfigControls<Command, Change>;
324
+ RuntimeAgentConfigControls<Command, Change> & AgentToolChildAdapter<unknown, string>;
471
325
 
472
326
  /**
473
327
  * 表示 `defineRuntimeAgent` 返回的 Durable Object 类构造器。
@@ -490,18 +344,45 @@ export interface RuntimeAgentClass<
490
344
 
491
345
  // #region 生成类的构造与生命周期
492
346
 
493
- // 作用:确认导出类名是可以安全创建的 JavaScript 标识符。
494
- // 调用:`defineRuntimeAgent` 在生成类之前调用一次。
495
- // 原因:动态计算类名时提前拒绝关键字,避免部署后才出现寻址失败。
496
- 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;
497
369
  if (
498
- !JAVASCRIPT_IDENTIFIER.test(className) ||
499
- RESERVED_IDENTIFIERS.has(className)
370
+ typeof executionLevel !== "string" ||
371
+ !EXECUTION_LEVELS.includes(executionLevel as ExecutionLevel)
500
372
  ) {
501
- throw new Error(
502
- `Invalid Runtime Agent className "${className}": expected a non-reserved JavaScript identifier`,
503
- );
373
+ throw new Error("invalid temporary agent executionLevel");
374
+ }
375
+ if (!("config" in record)) {
376
+ throw new Error("invalid temporary agent config");
504
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
+ };
505
386
  }
506
387
 
507
388
  /**
@@ -512,20 +393,6 @@ function assertClassName(className: string): void {
512
393
  *
513
394
  * 生成类把首次加载、并发去重、按 key 重载和失败重试收口到一个入口。
514
395
  */
515
- export function defineRuntimeAgent<
516
- Env extends Cloudflare.Env,
517
- Config,
518
- Command,
519
- Change,
520
- >(
521
- definition: MutableRuntimeAgentDefinition<Env, Config, Command, Change>,
522
- ): RuntimeAgentClass<Env, Command, Change>;
523
- export function defineRuntimeAgent<
524
- Env extends Cloudflare.Env = Cloudflare.Env,
525
- Config = unknown,
526
- >(
527
- definition: ReadonlyRuntimeAgentDefinition<Env, Config>,
528
- ): RuntimeAgentClass<Env>;
529
396
  export function defineRuntimeAgent<
530
397
  Env extends Cloudflare.Env = Cloudflare.Env,
531
398
  Config = unknown,
@@ -536,17 +403,32 @@ export function defineRuntimeAgent<
536
403
  | MutableRuntimeAgentDefinition<Env, Config, Command, Change>
537
404
  | ReadonlyRuntimeAgentDefinition<Env, Config>,
538
405
  ): RuntimeAgentClass<Env, Command, Change> {
539
- assertClassName(definition.className);
540
- const planRuntime = definition.planRuntime as (
406
+ const resolveDefinitionHooks = (
541
407
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
542
- config: Config,
543
- ) => Promise<RuntimeAssemblyInput>;
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
+ };
544
417
 
545
418
  const GeneratedRuntimeAgent = {
546
- [definition.className]: class extends AgentRuntimeKernel<Env> {
419
+ [definition.name]: class extends AgentToolRuntimeKernel<Env> {
547
420
  private hasLoadedRuntime = false;
548
421
  private loadedRuntimeKey?: string;
549
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
+ }
550
432
 
551
433
  // 作用:在 Kernel 真正消费 RuntimeSnapshot 前确保配置已完整提交。
552
434
  // 调用:Submission 准入、恢复、审批续跑与 Runtime Workspace 路径调用。
@@ -568,8 +450,13 @@ export function defineRuntimeAgent<
568
450
  async updateConfig(
569
451
  command: Command,
570
452
  ): Promise<RuntimeConfigUpdateResult<Change>> {
571
- const update = definition.config.update;
572
- if (!update) throw new Error("Runtime Agent Config is read-only");
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
+ }
573
460
  const result = await update(this.createDefinitionContext(), command);
574
461
  if (!result.changed) {
575
462
  return { change: result.change, runtime: "unchanged" };
@@ -629,90 +516,268 @@ export function defineRuntimeAgent<
629
516
  return super.stopTurn(requestId, reason);
630
517
  }
631
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
+
632
532
  // 作用:把首次加载和按 key 重载合并成一个串行入口。
633
533
  // 调用:Runtime readiness、Session 配置和重载都会调用。
634
534
  // 原因:共享同一个 Promise 可合并并发首加载,显式切 key 则等待前序完成后再判断。
635
- private ensureConfig(
535
+ protected ensureConfig(
636
536
  requestedRuntimeKey?: string,
637
537
  force = false,
638
538
  fromQueue = false,
639
539
  ): Promise<void> {
640
- if (this.loading && !fromQueue) {
641
- if (!requestedRuntimeKey && !force) return this.loading;
642
-
643
- // 显式 key 或强制刷新不能被正在进行的其他加载吞掉,必须等它结束后重新核对。
644
- const queued = this.loading
645
- .catch(() => undefined)
646
- .then(() => {
647
- if (
648
- !force &&
649
- this.hasLoadedRuntime &&
650
- this.loadedRuntimeKey === requestedRuntimeKey
651
- ) {
652
- return;
653
- }
654
- 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,
655
590
  requestedRuntimeKey,
656
- force,
657
- true,
658
- );
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,
659
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。
660
641
  let tracked: Promise<void>;
661
- tracked = queued.finally(() => {
642
+ tracked = pending.finally(() => {
662
643
  if (this.loading === tracked) this.loading = undefined;
663
644
  });
664
645
  this.loading = tracked;
665
646
  return tracked;
666
647
  }
667
648
 
668
- if (
669
- !force &&
670
- this.hasLoadedRuntime &&
671
- (!requestedRuntimeKey ||
672
- requestedRuntimeKey === this.loadedRuntimeKey)
673
- ) {
674
- return Promise.resolve();
675
- }
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
+ }
676
674
 
677
- // 先创建局部 Promise,再赋给 `loading`,失败时也能在 finally 中放开重试。
678
- const pending = (async () => {
679
- this.runtimeLoad.begin();
680
- const context = this.createDefinitionContext();
681
- const resolved = await definition.config.read(
682
- context,
683
- requestedRuntimeKey,
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,
684
681
  );
685
- const resolvedRuntimeKey = resolved.runtimeKey.trim();
686
- if (!resolvedRuntimeKey) {
687
- throw new Error("Resolved Runtime key must not be empty");
688
- }
689
- const input = await planRuntime(context, resolved.config);
690
-
691
- // 只有原子提交成功后才更新已加载标记和 key。
692
- await this.initConfig(input);
693
- this.hasLoadedRuntime = true;
694
- this.loadedRuntimeKey = resolvedRuntimeKey;
695
- this.runtimeLoad.complete();
696
- // 提交之前 turnEvents 绑定还不存在,此前每一次广播都到不了 Host。
697
- // Host 的会话列表把装配后的第一次投影当作「facet 已就绪」的信号。
698
- await this.broadcastApprovals();
699
- })().catch((error) => {
700
- this.runtimeLoad.fail();
701
- throw error;
702
- });
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
+ }
703
698
 
704
- if (fromQueue) return pending;
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
+ });
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
+ }
705
760
 
706
- // 无论成功失败都清掉进行中标记,后续调用才可以重试或切 key。
707
- let tracked: Promise<void>;
708
- tracked = pending.finally(() => {
709
- if (this.loading === tracked) this.loading = undefined;
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);
710
774
  });
711
- this.loading = tracked;
712
- return tracked;
775
+ return this.temporaryCleanup;
713
776
  }
714
777
 
715
- private createDefinitionContext(): RuntimeAgentPlanningContext<
778
+ protected createDefinitionContext(
779
+ runtimeConfig?: { value: Config },
780
+ ): RuntimeAgentPlanningContext<
716
781
  Env,
717
782
  Command,
718
783
  Change
@@ -727,7 +792,10 @@ export function defineRuntimeAgent<
727
792
  get parentPath() {
728
793
  return agent.parentPath;
729
794
  },
730
- ...(definition.config.update
795
+ get role() {
796
+ return agent.role;
797
+ },
798
+ ...(agent.role === "primary"
731
799
  ? {
732
800
  updateConfig: (command: Command) =>
733
801
  agent.updateConfig(command),
@@ -741,16 +809,64 @@ export function defineRuntimeAgent<
741
809
  clearAgentToolRuns: (options) =>
742
810
  agent.clearAgentToolRuns(options),
743
811
  executionLevel: () => agent.executionLevel(),
744
- runTemporaryAgent: (request, runContext, execute) =>
745
- agent.runTemporaryAgent(request, runContext, execute),
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") {
845
+ throw new Error(
846
+ result.error ??
847
+ `temporary agent ended with status ${result.status}`,
848
+ );
849
+ }
850
+ if (
851
+ typeof result.summary !== "string" ||
852
+ !result.summary.trim()
853
+ ) {
854
+ throw new Error(
855
+ "temporary agent completed without final text",
856
+ );
857
+ }
858
+ return result.summary;
859
+ },
860
+ );
861
+ },
746
862
  };
747
863
  }
748
864
  },
749
- }[definition.className];
865
+ }[definition.name];
750
866
 
751
- if (GeneratedRuntimeAgent.name !== definition.className) {
867
+ if (GeneratedRuntimeAgent.name !== definition.name) {
752
868
  throw new Error(
753
- `Runtime Agent class name mismatch: expected "${definition.className}", got "${GeneratedRuntimeAgent.name}"`,
869
+ `Runtime Agent class name mismatch: expected "${definition.name}", got "${GeneratedRuntimeAgent.name}"`,
754
870
  );
755
871
  }
756
872
 
@@ -758,6 +874,8 @@ export function defineRuntimeAgent<
758
874
  // Apply the same decorator function after class-name inference instead.
759
875
  const callableContext = {} as ClassMethodDecoratorContext;
760
876
  callable()(GeneratedRuntimeAgent.prototype.reloadRuntime, callableContext);
877
+ callable()(GeneratedRuntimeAgent.prototype.updateConfig, callableContext);
878
+ callable()(GeneratedRuntimeAgent.prototype.getRuntimeAssembly, callableContext);
761
879
  callable()(GeneratedRuntimeAgent.prototype.dispatchMessage, callableContext);
762
880
  callable()(
763
881
  GeneratedRuntimeAgent.prototype.steerQueuedSubmission,
@@ -767,21 +885,17 @@ export function defineRuntimeAgent<
767
885
  GeneratedRuntimeAgent.prototype.cancelSubmissionById,
768
886
  callableContext,
769
887
  );
770
- callable()(GeneratedRuntimeAgent.prototype.stopTurn, callableContext);
771
888
  callable()(
772
889
  GeneratedRuntimeAgent.prototype.respondToolInteraction,
773
890
  callableContext,
774
891
  );
775
- callable()(GeneratedRuntimeAgent.prototype.getRuntimeAssembly, callableContext);
776
- if (definition.config.update) {
777
- callable()(GeneratedRuntimeAgent.prototype.updateConfig, callableContext);
778
- } else {
779
- delete (GeneratedRuntimeAgent.prototype as {
780
- updateConfig?: unknown;
781
- }).updateConfig;
782
- }
892
+ callable()(GeneratedRuntimeAgent.prototype.stopTurn, callableContext);
783
893
 
784
- return GeneratedRuntimeAgent as RuntimeAgentClass<Env, Command, Change>;
894
+ return GeneratedRuntimeAgent as unknown as RuntimeAgentClass<
895
+ Env,
896
+ Command,
897
+ Change
898
+ >;
785
899
  }
786
900
 
787
901
  // #endregion