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

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 (34) hide show
  1. package/package.json +3 -1
  2. package/src/adapter/cloudflare/index.ts +60 -0
  3. package/src/adapter/cloudflare/resources/runtime-resources.ts +86 -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 +256 -0
  10. package/src/adapter/cloudflare/universal-agent/definition.ts +71 -0
  11. package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
  12. package/src/adapter/cloudflare/universal-agent/preparation.ts +273 -0
  13. package/src/adapter/cloudflare/universal-agent/tools.ts +74 -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/agent-tool-runtime.ts +152 -0
  17. package/src/index.ts +49 -7
  18. package/src/layers/orchestration/temporary-agent/core.ts +12 -1
  19. package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
  20. package/src/pi/message/contract.ts +7 -0
  21. package/src/pi/message/conversion.ts +9 -1
  22. package/src/pi/runtime-adapter/assembly.ts +4 -2
  23. package/src/pi/runtime-adapter/index.ts +6 -2
  24. package/src/pi/tool/base.ts +17 -0
  25. package/src/pi/tool/core.ts +13 -0
  26. package/src/pi/tool/schedule.ts +11 -0
  27. package/src/pi/tool/subagent.ts +14 -0
  28. package/src/pi/tool/workspace-sandbox.ts +15 -0
  29. package/src/runtime-agent-context.ts +112 -0
  30. package/src/runtime-agent.ts +429 -315
  31. package/src/runtime-assembler.ts +249 -99
  32. package/src/runtime-definition.ts +173 -0
  33. package/src/runtime.ts +139 -12
  34. 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,44 @@ 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 { AgentToolRuntimeKernel } from "./agent-tool-runtime";
48
+ import type {
49
+ RuntimeAgentConfigContext,
50
+ RuntimeAgentPlanningContext,
51
+ RuntimeAgentRole,
52
+ RuntimeAgentToolResult,
53
+ } from "./runtime-agent-context";
30
54
 
31
55
  /**
32
56
  * 本文件把应用提供的 Config Definition 与 Planner 接到 Cloudflare Agent 生命周期。
@@ -37,165 +61,21 @@ import { AgentRuntimeKernel } from "./runtime";
37
61
 
38
62
  // #region Agent 身份与公开契约
39
63
 
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> {
64
+ export type {
65
+ RuntimeAgentConfigContext,
66
+ RuntimeAgentContext,
67
+ RuntimeAgentPathStep,
68
+ RuntimeAgentPlanningContext,
69
+ RuntimeAgentRole,
70
+ RuntimeAgentToolResult,
71
+ RuntimeAssemblyContext,
72
+ } from "./runtime-agent-context";
73
+
74
+ /** Host 一次性准备的完整 Runtime 加载结果。 */
75
+ export interface LoadedRuntime<Config> {
197
76
  readonly runtimeKey: string;
198
77
  readonly config: Config;
78
+ readonly resources: ResolvedResources;
199
79
  }
200
80
 
201
81
  export interface RuntimeConfigUpdate<Change> {
@@ -203,60 +83,36 @@ export interface RuntimeConfigUpdate<Change> {
203
83
  readonly change: Change;
204
84
  }
205
85
 
206
- export interface RuntimeAgentConfigDefinition<
86
+ interface RuntimeAgentDefinitionBase<
207
87
  Env extends Cloudflare.Env,
208
88
  Config,
209
89
  Command,
210
90
  Change,
211
91
  > {
212
- read(
213
- context: RuntimeAgentConfigContext<Env>,
92
+ /** 必须等于 Worker 导出名(Wrangler `class_name`)。 */
93
+ readonly name: string;
94
+
95
+ readonly load: (
96
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
214
97
  requestedRuntimeKey?: string,
215
- ): Promise<ResolvedRuntimeConfig<Config>>;
216
- update?(
217
- context: RuntimeAgentConfigContext<Env>,
218
- command: Command,
219
- ): Promise<RuntimeConfigUpdate<Change>>;
220
- }
98
+ ) => Promise<LoadedRuntime<Config>>;
221
99
 
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(
100
+ readonly tools: (
247
101
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
248
102
  config: Config,
249
- ): Promise<RuntimeAssemblyInput>;
103
+ ) => ToolRegistry | ToolAssemblyResult | Promise<ToolRegistry | ToolAssemblyResult>;
104
+
105
+ readonly hooks?:
106
+ | RuntimeAgentHooks<Env, Config, Command, Change>
107
+ | ((context: RuntimeAgentPlanningContext<Env, Command, Change>) =>
108
+ RuntimeAgentHooks<Env, Config, Command, Change>);
250
109
  }
251
110
 
252
111
  export interface ReadonlyRuntimeAgentDefinition<
253
112
  Env extends Cloudflare.Env = Cloudflare.Env,
254
113
  Config = unknown,
255
114
  > extends RuntimeAgentDefinitionBase<Env, Config, never, never> {
256
- readonly config: Omit<
257
- RuntimeAgentConfigDefinition<Env, Config, never, never>,
258
- "update"
259
- > & { readonly update?: never };
115
+ readonly update?: never;
260
116
  }
261
117
 
262
118
  export interface MutableRuntimeAgentDefinition<
@@ -265,14 +121,10 @@ export interface MutableRuntimeAgentDefinition<
265
121
  Command = unknown,
266
122
  Change = unknown,
267
123
  > 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
- >;
124
+ readonly update: (
125
+ context: RuntimeAgentConfigContext<Env>,
126
+ command: Command,
127
+ ) => Promise<RuntimeConfigUpdate<Change>>;
276
128
  }
277
129
 
278
130
  export type RuntimeAgentDefinition<
@@ -447,14 +299,11 @@ export interface RuntimeAgentControls {
447
299
  ): Promise<{ ok: boolean }>;
448
300
  }
449
301
 
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
- };
302
+ export interface RuntimeAgentConfigControls<Command, Change> {
303
+ updateConfig(
304
+ command: Command,
305
+ ): Promise<RuntimeConfigUpdateResult<Change>>;
306
+ }
458
307
 
459
308
  /**
460
309
  * 表示 `defineRuntimeAgent` 生成类的实例。
@@ -467,7 +316,7 @@ export type RuntimeAgentInstance<
467
316
  Command = never,
468
317
  Change = never,
469
318
  > = Agent<Env, RuntimeState> & RuntimeAgentControls &
470
- RuntimeAgentConfigControls<Command, Change>;
319
+ RuntimeAgentConfigControls<Command, Change> & AgentToolChildAdapter<unknown, string>;
471
320
 
472
321
  /**
473
322
  * 表示 `defineRuntimeAgent` 返回的 Durable Object 类构造器。
@@ -490,18 +339,45 @@ export interface RuntimeAgentClass<
490
339
 
491
340
  // #region 生成类的构造与生命周期
492
341
 
493
- // 作用:确认导出类名是可以安全创建的 JavaScript 标识符。
494
- // 调用:`defineRuntimeAgent` 在生成类之前调用一次。
495
- // 原因:动态计算类名时提前拒绝关键字,避免部署后才出现寻址失败。
496
- function assertClassName(className: string): void {
342
+ const TERMINAL_AGENT_TOOL_STATUSES = [
343
+ "completed",
344
+ "error",
345
+ "aborted",
346
+ "interrupted",
347
+ ] as const;
348
+ const AGENT_TOOL_RETENTION_MS = 24 * 60 * 60 * 1_000;
349
+
350
+ function parseTemporaryAgentLaunch<Config>(
351
+ input: unknown,
352
+ ): TemporaryAgentLaunch<Config> {
353
+ const record = input != null && typeof input === "object"
354
+ ? input as Record<string, unknown>
355
+ : {};
356
+ const read = (name: string, max: number) => {
357
+ const value = record[name];
358
+ if (typeof value !== "string" || !value.trim() || value.length > max) {
359
+ throw new Error(`invalid temporary agent ${name}`);
360
+ }
361
+ return value.trim();
362
+ };
363
+ const executionLevel = record.executionLevel;
497
364
  if (
498
- !JAVASCRIPT_IDENTIFIER.test(className) ||
499
- RESERVED_IDENTIFIERS.has(className)
365
+ typeof executionLevel !== "string" ||
366
+ !EXECUTION_LEVELS.includes(executionLevel as ExecutionLevel)
500
367
  ) {
501
- throw new Error(
502
- `Invalid Runtime Agent className "${className}": expected a non-reserved JavaScript identifier`,
503
- );
368
+ throw new Error("invalid temporary agent executionLevel");
504
369
  }
370
+ if (!("config" in record)) {
371
+ throw new Error("invalid temporary agent config");
372
+ }
373
+ return {
374
+ subagentName: read("subagentName", 128),
375
+ instructions: read("instructions", 65_536),
376
+ task: read("task", 65_536),
377
+ runtimeKey: read("runtimeKey", 512),
378
+ config: record.config as Config,
379
+ executionLevel: executionLevel as ExecutionLevel,
380
+ };
505
381
  }
506
382
 
507
383
  /**
@@ -536,17 +412,32 @@ export function defineRuntimeAgent<
536
412
  | MutableRuntimeAgentDefinition<Env, Config, Command, Change>
537
413
  | ReadonlyRuntimeAgentDefinition<Env, Config>,
538
414
  ): RuntimeAgentClass<Env, Command, Change> {
539
- assertClassName(definition.className);
540
- const planRuntime = definition.planRuntime as (
415
+ const resolveDefinitionHooks = (
541
416
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
542
- config: Config,
543
- ) => Promise<RuntimeAssemblyInput>;
417
+ ): RuntimeAgentHooks<Env, Config, Command, Change> | undefined => {
418
+ const hooks = definition.hooks;
419
+ if (!hooks) return undefined;
420
+ return typeof hooks === "function"
421
+ ? (hooks as (
422
+ ctx: RuntimeAgentPlanningContext<Env, Command, Change>,
423
+ ) => RuntimeAgentHooks<Env, Config, Command, Change>)(context)
424
+ : hooks as RuntimeAgentHooks<Env, Config, Command, Change>;
425
+ };
544
426
 
545
427
  const GeneratedRuntimeAgent = {
546
- [definition.className]: class extends AgentRuntimeKernel<Env> {
428
+ [definition.name]: class extends AgentToolRuntimeKernel<Env> {
547
429
  private hasLoadedRuntime = false;
548
430
  private loadedRuntimeKey?: string;
549
431
  private loading?: Promise<void>;
432
+ private temporaryLaunch?: TemporaryAgentLaunch<Config>;
433
+ private temporaryDispose?: () => Promise<void>;
434
+ private temporaryCleanup?: Promise<void>;
435
+
436
+ protected get role(): RuntimeAgentRole {
437
+ return this.parentPath.at(-1)?.className === definition.name
438
+ ? "temporary"
439
+ : "primary";
440
+ }
550
441
 
551
442
  // 作用:在 Kernel 真正消费 RuntimeSnapshot 前确保配置已完整提交。
552
443
  // 调用:Submission 准入、恢复、审批续跑与 Runtime Workspace 路径调用。
@@ -568,8 +459,13 @@ export function defineRuntimeAgent<
568
459
  async updateConfig(
569
460
  command: Command,
570
461
  ): Promise<RuntimeConfigUpdateResult<Change>> {
571
- const update = definition.config.update;
572
- if (!update) throw new Error("Runtime Agent Config is read-only");
462
+ if (this.role === "temporary") {
463
+ throw new Error("Temporary agents cannot update Runtime Config");
464
+ }
465
+ const update = definition.update;
466
+ if (!update) {
467
+ return { change: undefined as Change, runtime: "unchanged" };
468
+ }
573
469
  const result = await update(this.createDefinitionContext(), command);
574
470
  if (!result.changed) {
575
471
  return { change: result.change, runtime: "unchanged" };
@@ -629,90 +525,259 @@ export function defineRuntimeAgent<
629
525
  return super.stopTurn(requestId, reason);
630
526
  }
631
527
 
528
+ protected override async prepareAgentToolRun(
529
+ input: unknown,
530
+ ): Promise<string> {
531
+ if (this.role !== "temporary") {
532
+ throw new Error("Agent Tool runs require a temporary child facet");
533
+ }
534
+ const launch = parseTemporaryAgentLaunch<Config>(input);
535
+ this.temporaryLaunch = launch;
536
+ await this.ctx.storage.put(TEMPORARY_AGENT_LAUNCH_KEY, launch);
537
+ await this.reloadRuntime(launch.runtimeKey, { force: true });
538
+ return launch.task;
539
+ }
540
+
632
541
  // 作用:把首次加载和按 key 重载合并成一个串行入口。
633
542
  // 调用:Runtime readiness、Session 配置和重载都会调用。
634
543
  // 原因:共享同一个 Promise 可合并并发首加载,显式切 key 则等待前序完成后再判断。
635
- private ensureConfig(
544
+ protected ensureConfig(
636
545
  requestedRuntimeKey?: string,
637
546
  force = false,
638
547
  fromQueue = false,
639
548
  ): 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(
655
- requestedRuntimeKey,
656
- force,
657
- true,
658
- );
549
+ if (this.loading && !fromQueue) {
550
+ if (!requestedRuntimeKey && !force) return this.loading;
551
+
552
+ // 显式 key 或强制刷新不能被正在进行的其他加载吞掉,必须等它结束后重新核对。
553
+ const queued = this.loading
554
+ .catch(() => undefined)
555
+ .then(() => {
556
+ if (
557
+ !force &&
558
+ this.hasLoadedRuntime &&
559
+ this.loadedRuntimeKey === requestedRuntimeKey
560
+ ) {
561
+ return;
562
+ }
563
+ return this.ensureConfig(
564
+ requestedRuntimeKey,
565
+ force,
566
+ true,
567
+ );
568
+ });
569
+ let tracked: Promise<void>;
570
+ tracked = queued.finally(() => {
571
+ if (this.loading === tracked) this.loading = undefined;
572
+ });
573
+ this.loading = tracked;
574
+ return tracked;
575
+ }
576
+
577
+ if (
578
+ !force &&
579
+ this.hasLoadedRuntime &&
580
+ (!requestedRuntimeKey ||
581
+ requestedRuntimeKey === this.loadedRuntimeKey)
582
+ ) {
583
+ return Promise.resolve();
584
+ }
585
+
586
+ // 先创建局部 Promise,再赋给 `loading`,失败时也能在 finally 中放开重试。
587
+ const pending = (async () => {
588
+ this.runtimeLoad.begin();
589
+ 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,
598
+ );
599
+ const resolvedRuntimeKey = loaded.runtimeKey.trim();
600
+ if (!resolvedRuntimeKey) {
601
+ throw new Error("Resolved Runtime key must not be empty");
602
+ }
603
+ const context = this.createDefinitionContext({
604
+ value: loaded.config,
605
+ });
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));
612
+ let hooks = resolveDefinitionHooks(context);
613
+ if (this.role === "temporary") {
614
+ this.temporaryDispose = tools.dispose;
615
+ tools = this.applyTemporaryToolPolicy(tools);
616
+ hooks = await this.createTemporaryHooks();
617
+ }
618
+ const candidate = await assembleRuntimeSnapshot({
619
+ ctx: context,
620
+ config: loaded.config,
621
+ tools,
622
+ resources: loaded.resources,
623
+ hooks,
659
624
  });
625
+
626
+ await this.initCandidate(candidate);
627
+ this.hasLoadedRuntime = true;
628
+ this.loadedRuntimeKey = resolvedRuntimeKey;
629
+ this.runtimeLoad.complete();
630
+ // 提交之前 turnEvents 绑定还不存在,此前每一次广播都到不了 Host。
631
+ // Host 的会话列表把装配后的第一次投影当作「facet 已就绪」的信号。
632
+ await this.broadcastApprovals();
633
+ })().catch((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