@springbrand/agent-runtime 0.3.0-alpha.20 → 0.3.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.3.0-alpha.20",
3
+ "version": "0.3.0-alpha.21",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -19,6 +19,7 @@ import {
19
19
  isTerminalSubmissionStatus,
20
20
  SubmissionLifecycle,
21
21
  SubmissionAdmission,
22
+ ADMISSION_ATTEMPT_WINDOW_MS,
22
23
  SubmissionDelivery,
23
24
  SubmissionFinalizer,
24
25
  projectSubmissionReceipt,
@@ -33,6 +34,8 @@ import {
33
34
  type SubmissionReceipt,
34
35
  type SubmissionExecutionMode,
35
36
  type SubmissionHandle,
37
+ type StoredRuntimeKey,
38
+ type RuntimeKeyRewrite,
36
39
  type RuntimeUnattendedExecutionGrant,
37
40
  } from "../../submission";
38
41
  import type { ExecutionLevel } from "../../lib/execution-level";
@@ -43,7 +46,6 @@ import {
43
46
  RuntimeLoadTracker,
44
47
  prepareRuntimeCandidate,
45
48
  projectRuntimeAssembly,
46
- type PreparedPiRuntime,
47
49
  type RuntimeAssemblyInput,
48
50
  type RuntimeAssemblyView,
49
51
  type RuntimeCandidate,
@@ -88,7 +90,10 @@ import {
88
90
  type TemporaryAgentRequest,
89
91
  type TemporaryAgentRunContext,
90
92
  } from "../../tools";
91
- import { sha256Hex } from "../../lib/artifacts";
93
+ import {
94
+ ASK_USER_TOOL_NAME,
95
+ askUserPiToolCandidate,
96
+ } from "../../tools/assistant/ask-user";
92
97
  import { RuntimeDatabase } from "./database";
93
98
 
94
99
  /**
@@ -156,17 +161,22 @@ export abstract class AgentRuntimeKernel<
156
161
  };
157
162
 
158
163
  protected abstract ensureRuntimeReady(): Promise<void>;
159
- /** Prepare per-Submission Host state before the first assembly pin. */
164
+ /** Prepare per-Submission Host state before first execution. */
160
165
  protected prepareRuntimeForSubmission(_submissionId: string): Promise<void> {
161
166
  return this.ensureRuntimeReady();
162
167
  }
163
168
 
164
169
  /** Runs before a new Submission records its Runtime key. */
165
- protected prepareRuntimeForAdmission(): Promise<void> {
166
- return this.ensureRuntimeReady();
170
+ protected async prepareRuntimeForAdmission(): Promise<{
171
+ runtimeKey: string;
172
+ admission?: ActiveRuntime["admission"];
173
+ }> {
174
+ await this.ensureRuntimeReady();
175
+ const runtime = this.requireRuntime();
176
+ return { runtimeKey: runtime.runtimeKey, admission: runtime.admission };
167
177
  }
168
178
 
169
- protected ensureRuntimeVersion(_runtimeKey: string): Promise<void> {
179
+ protected ensureRuntimeVersion(_runtimeKey: string, _force = false): Promise<void> {
170
180
  return this.ensureRuntimeReady();
171
181
  }
172
182
  protected get submissionContext() {
@@ -176,6 +186,37 @@ export abstract class AgentRuntimeKernel<
176
186
  };
177
187
  }
178
188
 
189
+ /** Narrow storage seam for Host-validated, one-time Agent identity upgrades. */
190
+ protected savedRuntimeKeys(): readonly StoredRuntimeKey[] {
191
+ return this.db.submissions.listRuntimeKeys();
192
+ }
193
+
194
+ protected rewriteSavedRuntimeKeys(rewrites: readonly RuntimeKeyRewrite[]): void {
195
+ this.db.submissions.rewriteRuntimeKeys(rewrites);
196
+ }
197
+
198
+ /** One Runtime-owned fact for Host lifecycle guards; includes async preparation. */
199
+ protected hasUnfinishedRuntimeWork(): boolean {
200
+ return this.submissions.isBusy() ||
201
+ this.db.submissionAdmissions.countPending(Date.now() - ADMISSION_ATTEMPT_WINDOW_MS) > 0 ||
202
+ this.db.agentTools.hasRunning() ||
203
+ Boolean(
204
+ this.state.turn?.activeSubmissionId ||
205
+ this.state.approvals?.length ||
206
+ (this.state.activity?.activity && this.state.activity.activity !== "idle"),
207
+ );
208
+ }
209
+
210
+ /** True while a Submission owns this Chat's active execution slice. */
211
+ protected isSubmissionExecuting(): boolean {
212
+ return this.submissions.currentActive() !== undefined;
213
+ }
214
+
215
+ /** External Runtime changes share the same serial queue as Submission execution. */
216
+ protected runConfigControl<T>(run: () => Promise<T>): Promise<T> {
217
+ return this.submissions.runConfigControl(run);
218
+ }
219
+
179
220
  /** Let the Host prepare a validated client response before Tool settlement. */
180
221
  protected prepareToolInteractionResponse(
181
222
  input: ToolInteractionResponsePreparation,
@@ -318,7 +359,6 @@ export abstract class AgentRuntimeKernel<
318
359
  createExtensionHostBinding: (permissions, labels) =>
319
360
  this.createExtensionHostBinding(permissions, labels),
320
361
  advance: (phase) => this.runtimeLoad.advance(phase),
321
- isExecuting: () => Boolean(this.submissions.currentActive()),
322
362
  commit: (runtime) => {
323
363
  this.telemetry.configure(candidate.telemetry);
324
364
  this.activeRuntime = runtime;
@@ -516,57 +556,7 @@ export abstract class AgentRuntimeKernel<
516
556
 
517
557
  // #endregion
518
558
 
519
- // #region Submission 准入与配置固定
520
-
521
- /** Pin dynamic Context and execution policy before the first assembly is used. */
522
- private async admissionPin(fresh: boolean, submissionId: string): Promise<{
523
- revision: string;
524
- descriptor: string;
525
- }> {
526
- if (fresh) await this.prepareRuntimeForSubmission(submissionId);
527
- const submission = this.db.submissions.find(submissionId);
528
- if (!submission) {
529
- throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
530
- }
531
- return this.currentAssemblyPin(submission.origin !== "scheduled");
532
- }
533
-
534
- // initConfig can call this inside its serialized load; re-entering ensureRuntimeReady would deadlock.
535
- private async currentAssemblyPin(includeMemory = true): Promise<{
536
- revision: string;
537
- descriptor: string;
538
- }> {
539
- await this.runtimeTurnEvents.drain();
540
- const runtime = this.activeRuntime;
541
- if (!runtime) {
542
- throw new Error("Runtime revision is not initialized");
543
- }
544
- return this.pinAssembly(runtime.pi, runtime.revision, includeMemory);
545
- }
546
-
547
- private async pinAssembly(
548
- prepared: PreparedPiRuntime,
549
- baseRevision: string,
550
- includeMemory: boolean,
551
- ): Promise<{ revision: string; descriptor: string }> {
552
- const pinned = await this.pi.pin({
553
- prepared,
554
- baseRevision,
555
- includeMemory,
556
- readExtensionContext: ({ label }) =>
557
- this._hostGetContext(label),
558
- });
559
- if (pinned.degradations.length > 0) {
560
- console.warn(
561
- "[runtime-context:degraded]",
562
- json({ degradations: pinned.degradations }),
563
- );
564
- }
565
- return {
566
- revision: await sha256Hex(pinned.descriptor),
567
- descriptor: pinned.descriptor,
568
- };
569
- }
559
+ // #region Submission 准入与当前配置
570
560
 
571
561
  private get admission(): SubmissionAdmission {
572
562
  return this.submissionAdmission ??= new SubmissionAdmission({
@@ -574,10 +564,7 @@ export abstract class AgentRuntimeKernel<
574
564
  steers: this.db.steers,
575
565
  submissions: this.db.submissions,
576
566
  transaction: (run) => this.db.transaction(run),
577
- prepareRuntime: async () => {
578
- await this.prepareRuntimeForAdmission();
579
- return { runtimeKey: this.activeRuntime!.runtimeKey, admission: this.activeRuntime?.admission };
580
- },
567
+ prepareRuntime: () => this.prepareRuntimeForAdmission(),
581
568
  findUserMessage: (id) => this.transcript.findUserMessage(id),
582
569
  onAdmitted: (submission, hostAccepted) =>
583
570
  this.runtimeTurnEvents.submissionAdmitted(submission, hostAccepted),
@@ -783,27 +770,32 @@ export abstract class AgentRuntimeKernel<
783
770
  ): Promise<StoredSubmission> {
784
771
  const continuation = mode !== "fresh";
785
772
  const submissionId = submission.submissionId;
786
- submission = await this.admission.pin(
773
+ submission = await this.admission.prepareCurrentRuntime(
787
774
  submission,
788
- () => this.admissionPin(mode === "fresh", submissionId),
789
- (key) => this.ensureRuntimeVersion(key),
775
+ (key) => this.ensureRuntimeVersion(key, true),
790
776
  );
777
+ if (!continuation) await this.prepareRuntimeForSubmission(submissionId);
778
+ await this.runtimeTurnEvents.drain();
791
779
  const activeRuntime = this.requireRuntime();
792
780
  // A fresh provider Session per Submission keeps one failing upstream route from pinning later turns.
793
781
  const recovery = new ModelRecovery(submissionId);
794
- const harnessInput = this.pi.harnessInputs(
782
+ const harnessInput = await this.pi.harnessInputs(
795
783
  activeRuntime.pi,
796
- submission.assemblyDescriptor,
797
784
  {
798
- modelSessionId: () => recovery.sessionId(),
799
- assertRequestAllowed: () => {
800
- const current = this.db.submissions.find(submissionId);
801
- if (current?.abortReason || current?.unattendedFailure) throw new Error(current.abortReason ?? current.unattendedFailure!);
802
- },
803
- onGeneration: (event) => this.telemetry.generation(submissionId, event),
804
- onCompactionGeneration: (event) => this.runtimeTurnEvents.compactionGeneration(submissionId, event),
785
+ modelSessionId: () => recovery.sessionId(),
786
+ assertRequestAllowed: () => {
787
+ const current = this.db.submissions.find(submissionId);
788
+ if (current?.abortReason || current?.unattendedFailure) throw new Error(current.abortReason ?? current.unattendedFailure!);
789
+ },
790
+ onGeneration: (event) => this.telemetry.generation(submissionId, event),
791
+ onCompactionGeneration: (event) => this.runtimeTurnEvents.compactionGeneration(submissionId, event),
792
+ includeMemory: submission.origin !== "scheduled",
793
+ readExtensionContext: ({ label }) => this._hostGetContext(label),
805
794
  },
806
795
  );
796
+ if (harnessInput.degradations.length > 0) {
797
+ console.warn("[runtime-context:degraded]", json({ degradations: harnessInput.degradations }));
798
+ }
807
799
  const prepareToolInput = (toolName: string, input: unknown): unknown => {
808
800
  const prepare = harnessInput.candidates.find(({ tool }) =>
809
801
  tool.name === toolName
@@ -906,7 +898,6 @@ export abstract class AgentRuntimeKernel<
906
898
  this.db.submissions.transition(submissionId, "running", ["pending"]);
907
899
  this.telemetry.capture("turnStarted", {
908
900
  submissionId,
909
- assemblyRevision: submission.assemblyRevision,
910
901
  });
911
902
  submission = this.db.submissions.find(submissionId)!;
912
903
  }
@@ -1533,11 +1524,19 @@ export abstract class AgentRuntimeKernel<
1533
1524
  if (!submission || isTerminalSubmissionStatus(submission.status)) {
1534
1525
  return { ok: false };
1535
1526
  }
1536
- await this.ensureRuntimeReady();
1537
- const spec = this.pi.harnessInputs(
1538
- this.requireRuntime().pi,
1539
- submission.assemblyDescriptor,
1540
- ).candidates.find(({ tool }) => tool.name === pending.toolName)?.interaction;
1527
+ const active = this.submissions.currentActive();
1528
+ if (active && active.submissionId !== submission.submissionId) {
1529
+ return { ok: false };
1530
+ }
1531
+ // ask_user has a Config-independent validator. A parked answer must not
1532
+ // replace the Gateway; the continuation loads current Config instead.
1533
+ const askUser = pending.toolName === ASK_USER_TOOL_NAME;
1534
+ if (!askUser && !active) {
1535
+ await this.ensureRuntimeVersion(submission.runtimeKey, true);
1536
+ }
1537
+ const spec = askUser
1538
+ ? askUserPiToolCandidate().interaction
1539
+ : this.pi.interaction(this.requireRuntime().pi, pending.toolName);
1541
1540
  if (!spec) return { ok: false };
1542
1541
 
1543
1542
  const accepted = await this.humanDecisions.respond(
@@ -12,6 +12,8 @@ import type {
12
12
  MessageDelivery,
13
13
  MessageDispatchReceipt,
14
14
  SubmissionReceipt,
15
+ StoredRuntimeKey,
16
+ RuntimeKeyRewrite,
15
17
  } from "../../submission";
16
18
  import type { RuntimeState } from "../../conversation";
17
19
  import type {
@@ -31,7 +33,9 @@ import {
31
33
  } from "../../lib/execution-level";
32
34
  import {
33
35
  assembleRuntimeSnapshot,
36
+ verifyRuntimeCandidateGuards,
34
37
  type RuntimeCandidate,
38
+ type RuntimeSubmissionAdmissionHook,
35
39
  } from "../../config";
36
40
  import type {
37
41
  ResolvedResources,
@@ -71,6 +75,7 @@ export type {
71
75
  RuntimeAgentToolResult,
72
76
  RuntimeAssemblyContext,
73
77
  } from "../../config";
78
+ export type { RuntimeKeyRewrite, StoredRuntimeKey } from "../../submission";
74
79
 
75
80
  /** Host 一次性准备的完整 Runtime 加载结果。 */
76
81
  export interface LoadedRuntime<Config> {
@@ -99,9 +104,9 @@ interface RuntimeAgentDefinitionBase<
99
104
  /** Refresh Host-owned inputs before a new Submission records its Runtime key. */
100
105
  readonly beforeSubmission?: (
101
106
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
102
- ) => Promise<boolean>;
107
+ ) => Promise<void>;
103
108
 
104
- /** Prepare per-Submission Host state after admission and before its first assembly pin. */
109
+ /** Prepare per-Submission Host state after admission and before first execution. */
105
110
  readonly afterSubmission?: (
106
111
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
107
112
  ) => Promise<void>;
@@ -111,6 +116,12 @@ interface RuntimeAgentDefinitionBase<
111
116
  requestedRuntimeKey?: string,
112
117
  ) => Promise<LoadedRuntime<Config>>;
113
118
 
119
+ /** Resolve saved identities; unfinished work must still belong to the selected Agent. */
120
+ readonly migrateStoredRuntimeKeys?: (
121
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
122
+ keys: readonly StoredRuntimeKey[],
123
+ ) => Promise<readonly RuntimeKeyRewrite[]>;
124
+
114
125
  readonly tools: (
115
126
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
116
127
  config: Config,
@@ -321,6 +332,7 @@ declare class RuntimeAgentHostSeam {
321
332
  protected prepareToolInteractionResponse(
322
333
  input: import("./agent").ToolInteractionResponsePreparation,
323
334
  ): Promise<unknown | null>;
335
+ protected hasUnfinishedRuntimeWork(): boolean;
324
336
  }
325
337
 
326
338
  /**
@@ -364,6 +376,7 @@ const TERMINAL_AGENT_TOOL_STATUSES = [
364
376
  "interrupted",
365
377
  ] as const;
366
378
  const AGENT_TOOL_RETENTION_MS = 24 * 60 * 60 * 1_000;
379
+ const STABLE_RUNTIME_KEY_MIGRATION = "agent-runtime:stable-runtime-key-migration-v1";
367
380
 
368
381
  function parseTemporaryAgentLaunch(input: unknown): TemporaryAgentLaunchSelection {
369
382
  const record = input != null && typeof input === "object"
@@ -421,6 +434,8 @@ export function defineRuntimeAgent<
421
434
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
422
435
  ) => AgentTelemetryBinding | undefined | Promise<AgentTelemetryBinding | undefined>)(context);
423
436
  };
437
+ const migrateSavedKeys = definition.migrateStoredRuntimeKeys as
438
+ RuntimeAgentDefinitionBase<Env, Config, Command, Change>["migrateStoredRuntimeKeys"];
424
439
 
425
440
  const GeneratedRuntimeAgent = {
426
441
  [definition.name]: class extends AgentToolRuntimeKernel<Env> {
@@ -432,6 +447,68 @@ export function defineRuntimeAgent<
432
447
  private temporaryLaunch?: TemporaryAgentLaunchSelection;
433
448
  private temporaryDispose?: () => Promise<void>;
434
449
  private temporaryCleanup?: Promise<void>;
450
+ private storedKeyMigration?: Promise<void>;
451
+
452
+ async onStart(): Promise<void> {
453
+ await this.migrateStoredRuntimeKeys();
454
+ await super.onStart();
455
+ }
456
+
457
+ private async resolveSavedKeyRewrites(
458
+ keys: readonly StoredRuntimeKey[],
459
+ ): Promise<readonly RuntimeKeyRewrite[]> {
460
+ if (!migrateSavedKeys) {
461
+ return keys.map(({ runtimeKey }) => ({ from: runtimeKey, to: runtimeKey }));
462
+ }
463
+ const rewrites = await migrateSavedKeys(this.createDefinitionContext(), keys);
464
+ if (
465
+ rewrites.length !== keys.length ||
466
+ rewrites.some(({ from, to }, index) =>
467
+ from !== keys[index]?.runtimeKey || !to.trim())
468
+ ) {
469
+ throw new Error("Saved Runtime key migration returned an invalid rewrite plan");
470
+ }
471
+ return rewrites;
472
+ }
473
+
474
+ private migrateStoredRuntimeKeys(): Promise<void> {
475
+ if (!migrateSavedKeys) return Promise.resolve();
476
+ if (this.storedKeyMigration) return this.storedKeyMigration;
477
+ const migration = (async () => {
478
+ if (await this.ctx.storage.get<boolean>(STABLE_RUNTIME_KEY_MIGRATION)) return;
479
+ const savedLaunch = await this.ctx.storage.get<unknown>(TEMPORARY_AGENT_LAUNCH_KEY);
480
+ const launch = savedLaunch === undefined
481
+ ? undefined
482
+ : parseTemporaryAgentLaunch(savedLaunch);
483
+ const keysByValue = new Map(
484
+ this.savedRuntimeKeys().map((key) => [key.runtimeKey, key]),
485
+ );
486
+ if (launch) {
487
+ keysByValue.set(launch.runtimeKey, {
488
+ runtimeKey: launch.runtimeKey,
489
+ unfinished: keysByValue.get(launch.runtimeKey)?.unfinished ?? true,
490
+ });
491
+ }
492
+ const keys = [...keysByValue.values()];
493
+ if (keys.length > 0) {
494
+ const rewrites = await this.resolveSavedKeyRewrites(keys);
495
+ this.rewriteSavedRuntimeKeys(rewrites);
496
+ if (launch) {
497
+ const runtimeKey = rewrites.find(({ from }) => from === launch.runtimeKey)?.to;
498
+ if (!runtimeKey) throw new Error("Saved temporary Agent key was not migrated");
499
+ const currentLaunch = { ...launch, runtimeKey };
500
+ await this.ctx.storage.put(TEMPORARY_AGENT_LAUNCH_KEY, currentLaunch);
501
+ this.temporaryLaunch = currentLaunch;
502
+ }
503
+ }
504
+ await this.ctx.storage.put(STABLE_RUNTIME_KEY_MIGRATION, true);
505
+ })().catch((error) => {
506
+ this.storedKeyMigration = undefined;
507
+ throw error;
508
+ });
509
+ this.storedKeyMigration = migration;
510
+ return migration;
511
+ }
435
512
 
436
513
  protected get role(): RuntimeAgentRole {
437
514
  return this.parentPath.at(-1)?.className === definition.name
@@ -446,19 +523,20 @@ export function defineRuntimeAgent<
446
523
  return this.ensureConfig();
447
524
  }
448
525
 
449
- protected override ensureRuntimeVersion(runtimeKey: string): Promise<void> {
450
- return this.ensureConfig(runtimeKey);
526
+ protected override ensureRuntimeVersion(runtimeKey: string, force = false): Promise<void> {
527
+ return this.ensureConfig(runtimeKey, force);
451
528
  }
452
529
 
453
- protected async prepareRuntimeForAdmission(): Promise<void> {
530
+ protected async prepareRuntimeForAdmission(): Promise<{
531
+ runtimeKey: string;
532
+ admission?: RuntimeSubmissionAdmissionHook;
533
+ }> {
454
534
  const beforeSubmission = definition.beforeSubmission as RuntimeAgentDefinitionBase<Env, Config, Command, Change>["beforeSubmission"];
455
- try {
456
- const changed = await beforeSubmission?.(this.createDefinitionContext());
457
- await this.ensureConfig(undefined, changed === true);
458
- } catch (error) {
459
- this.hasLoadedRuntime = false;
460
- throw error;
461
- }
535
+ await beforeSubmission?.(this.createDefinitionContext());
536
+ const requestedKey = this.role === "temporary"
537
+ ? this.loadedRuntimeKey ?? this.temporaryLaunch?.runtimeKey
538
+ : undefined;
539
+ return this.inspectCandidate(requestedKey);
462
540
  }
463
541
 
464
542
  protected async prepareRuntimeForSubmission(submissionId: string): Promise<void> {
@@ -479,7 +557,7 @@ export function defineRuntimeAgent<
479
557
  runtimeKey?: string,
480
558
  options?: { force?: boolean },
481
559
  ): Promise<void> {
482
- await this.ensureConfig(runtimeKey, options?.force === true);
560
+ await this.runConfigControl(() => this.ensureConfig(runtimeKey, options?.force === true));
483
561
  }
484
562
 
485
563
  async updateConfig(
@@ -492,26 +570,36 @@ export function defineRuntimeAgent<
492
570
  if (!update) {
493
571
  return { change: undefined as Change, runtime: "unchanged" };
494
572
  }
495
- const result = await update(this.createDefinitionContext(), command);
496
- if (!result.changed) {
497
- return { change: result.change, runtime: "unchanged" };
498
- }
499
- try {
500
- await this.ensureConfig(undefined, true);
501
- return { change: result.change, runtime: "reloaded" };
502
- } catch (reloadError) {
503
- if (result.rollback) {
504
- try {
505
- await result.rollback();
506
- } catch (rollbackError) {
507
- throw new AggregateError(
508
- [reloadError, rollbackError],
509
- "Runtime Config rollback failed",
510
- );
573
+ const deferActivation = this.isSubmissionExecuting();
574
+ const apply = async (): Promise<RuntimeConfigUpdateResult<Change>> => {
575
+ const result = await update(this.createDefinitionContext(), command);
576
+ if (!result.changed) {
577
+ return { change: result.change, runtime: "unchanged" };
578
+ }
579
+ try {
580
+ if (deferActivation) {
581
+ await this.inspectCandidate();
582
+ return { change: result.change, runtime: "deferred" };
583
+ }
584
+ await this.ensureConfig(undefined, true);
585
+ return { change: result.change, runtime: "reloaded" };
586
+ } catch (reloadError) {
587
+ if (result.rollback) {
588
+ try {
589
+ await result.rollback();
590
+ } catch (rollbackError) {
591
+ throw new AggregateError(
592
+ [reloadError, rollbackError],
593
+ "Runtime Config rollback failed",
594
+ );
595
+ }
511
596
  }
597
+ return { change: result.change, runtime: "reload-failed" };
512
598
  }
513
- return { change: result.change, runtime: "reload-failed" };
514
- }
599
+ };
600
+ return deferActivation
601
+ ? apply()
602
+ : this.runConfigControl(apply);
515
603
  }
516
604
 
517
605
  async getRuntimeAssembly(): Promise<RuntimeAssemblyView> {
@@ -563,13 +651,105 @@ export function defineRuntimeAgent<
563
651
  if (this.role !== "temporary") {
564
652
  throw new Error("Agent Tool runs require a temporary child facet");
565
653
  }
566
- const launch = parseTemporaryAgentLaunch(input);
654
+ const parsed = parseTemporaryAgentLaunch(input);
655
+ const [rewrite] = await this.resolveSavedKeyRewrites([
656
+ { runtimeKey: parsed.runtimeKey, unfinished: true },
657
+ ]);
658
+ const runtimeKey = rewrite?.to;
659
+ if (!runtimeKey) throw new Error("Temporary Agent runtime key must not be empty");
660
+ const launch = { ...parsed, runtimeKey };
567
661
  this.temporaryLaunch = launch;
568
662
  await this.ctx.storage.put(TEMPORARY_AGENT_LAUNCH_KEY, launch);
569
663
  await this.reloadRuntime(launch.runtimeKey, { force: true });
570
664
  return launch.task;
571
665
  }
572
666
 
667
+ /** One definition load and assembly path for admission and execution. */
668
+ private async prepareCandidate(requestedRuntimeKey?: string): Promise<{
669
+ candidate: RuntimeCandidate;
670
+ runtimeKey: string;
671
+ dispose?: () => Promise<void>;
672
+ }> {
673
+ const loadContext = this.createDefinitionContext();
674
+ const loaded = await withRuntimeLoadTimeout(
675
+ "definition.load",
676
+ () => (
677
+ definition.load as (
678
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
679
+ requestedRuntimeKey?: string,
680
+ ) => Promise<LoadedRuntime<Config>>
681
+ )(loadContext, requestedRuntimeKey),
682
+ { timeoutMs: RUNTIME_LOAD_TIMEOUT_MS },
683
+ );
684
+ const runtimeKey = loaded.runtimeKey.trim();
685
+ if (!runtimeKey) {
686
+ throw new Error("Resolved Runtime key must not be empty");
687
+ }
688
+ if (requestedRuntimeKey && runtimeKey !== requestedRuntimeKey) {
689
+ throw new Error(
690
+ `Requested Runtime key ${requestedRuntimeKey} resolved to ${runtimeKey}`,
691
+ );
692
+ }
693
+ const context = this.createDefinitionContext({ value: loaded.config });
694
+ let tools = await withRuntimeLoadTimeout(
695
+ "definition.tools",
696
+ () => (
697
+ definition.tools as (
698
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
699
+ config: Config,
700
+ ) => ToolAssemblyResult | Promise<ToolAssemblyResult>
701
+ )(context, loaded.config),
702
+ { timeoutMs: RUNTIME_LOAD_TIMEOUT_MS },
703
+ );
704
+ const dispose = tools.dispose;
705
+ try {
706
+ let resources = loaded.resources;
707
+ let commitGuard = loaded.commitGuard;
708
+ if (!this.telemetryResolved) {
709
+ this.resolvedTelemetry = await resolveDefinitionTelemetry(context);
710
+ this.telemetryResolved = true;
711
+ }
712
+ const telemetry = this.resolvedTelemetry;
713
+ if (this.role === "temporary") {
714
+ tools = this.applyTemporaryToolPolicy(tools);
715
+ resources = await this.applyTemporaryResources(resources);
716
+ commitGuard = undefined;
717
+ }
718
+ const candidate = await assembleRuntimeSnapshot({
719
+ ctx: context,
720
+ tools,
721
+ resources,
722
+ ...(commitGuard ? { commitGuard } : {}),
723
+ ...(telemetry ? { telemetry } : {}),
724
+ });
725
+ return {
726
+ candidate: { ...candidate, runtimeKey },
727
+ runtimeKey,
728
+ ...(dispose ? { dispose } : {}),
729
+ };
730
+ } catch (error) {
731
+ await dispose?.();
732
+ throw error;
733
+ }
734
+ }
735
+
736
+ /** Validate a candidate without changing the active Runtime or its Gateway. */
737
+ private async inspectCandidate(requestedRuntimeKey?: string): Promise<{
738
+ runtimeKey: string;
739
+ admission?: RuntimeSubmissionAdmissionHook;
740
+ }> {
741
+ const prepared = await this.prepareCandidate(requestedRuntimeKey);
742
+ try {
743
+ await verifyRuntimeCandidateGuards(prepared.candidate, "admission");
744
+ return {
745
+ runtimeKey: prepared.runtimeKey,
746
+ admission: prepared.candidate.admission,
747
+ };
748
+ } finally {
749
+ await prepared.dispose?.();
750
+ }
751
+ }
752
+
573
753
  // 作用:把首次加载和按 key 重载合并成一个串行入口。
574
754
  // 调用:Runtime readiness、Session 配置和重载都会调用。
575
755
  // 原因:共享同一个 Promise 可合并并发首加载,显式切 key 则等待前序完成后再判断。
@@ -618,69 +798,16 @@ export function defineRuntimeAgent<
618
798
  // 先创建局部 Promise,再赋给 `loading`,失败时也能在 finally 中放开重试。
619
799
  const pending = (async () => {
620
800
  this.runtimeLoad.begin();
621
- const loadContext = this.createDefinitionContext();
622
- const loaded = await withRuntimeLoadTimeout(
623
- "definition.load",
624
- () => (
625
- definition.load as (
626
- context: RuntimeAgentPlanningContext<Env, Command, Change>,
627
- requestedRuntimeKey?: string,
628
- ) => Promise<LoadedRuntime<Config>>
629
- )(
630
- loadContext,
631
- requestedRuntimeKey,
632
- ),
633
- { timeoutMs: RUNTIME_LOAD_TIMEOUT_MS },
634
- );
635
- const resolvedRuntimeKey = loaded.runtimeKey.trim();
636
- if (!resolvedRuntimeKey) {
637
- throw new Error("Resolved Runtime key must not be empty");
638
- }
639
- if (requestedRuntimeKey && resolvedRuntimeKey !== requestedRuntimeKey) {
640
- throw new Error(
641
- `Requested Runtime key ${requestedRuntimeKey} resolved to ${resolvedRuntimeKey}`,
642
- );
643
- }
644
- const context = this.createDefinitionContext({
645
- value: loaded.config,
646
- });
647
- let tools = await withRuntimeLoadTimeout(
648
- "definition.tools",
649
- () => (
650
- definition.tools as (
651
- context: RuntimeAgentPlanningContext<Env, Command, Change>,
652
- config: Config,
653
- ) => ToolAssemblyResult | Promise<ToolAssemblyResult>
654
- )(context, loaded.config),
655
- { timeoutMs: RUNTIME_LOAD_TIMEOUT_MS },
656
- );
657
- let resources = loaded.resources;
658
- let commitGuard = loaded.commitGuard;
659
- if (!this.telemetryResolved) {
660
- this.resolvedTelemetry = await resolveDefinitionTelemetry(context);
661
- this.telemetryResolved = true;
662
- }
663
- const telemetry = this.resolvedTelemetry;
664
- if (this.role === "temporary") {
665
- this.temporaryDispose = tools.dispose;
666
- tools = this.applyTemporaryToolPolicy(tools);
667
- resources = await this.applyTemporaryResources(resources);
668
- commitGuard = undefined;
801
+ const prepared = await this.prepareCandidate(requestedRuntimeKey);
802
+ try {
803
+ await this.initCandidate(prepared.candidate);
804
+ } catch (error) {
805
+ await prepared.dispose?.();
806
+ throw error;
669
807
  }
670
- const candidate = await assembleRuntimeSnapshot({
671
- ctx: context,
672
- tools,
673
- resources,
674
- ...(commitGuard ? { commitGuard } : {}),
675
- ...(telemetry ? { telemetry } : {}),
676
- });
677
-
678
- await this.initCandidate({
679
- ...candidate,
680
- runtimeKey: resolvedRuntimeKey,
681
- });
808
+ if (this.role === "temporary") this.temporaryDispose = prepared.dispose;
682
809
  this.hasLoadedRuntime = true;
683
- this.loadedRuntimeKey = resolvedRuntimeKey;
810
+ this.loadedRuntimeKey = prepared.runtimeKey;
684
811
  this.runtimeLoad.complete();
685
812
  // 提交之前 turnEvents 绑定还不存在,此前每一次广播都到不了 Host。
686
813
  // Host 的会话列表把装配后的第一次投影当作「facet 已就绪」的信号。