@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 +1 -1
- package/src/adapter/cloudflare/agent.ts +79 -80
- package/src/adapter/cloudflare/define-agent.ts +220 -93
- package/src/adapter/cloudflare/schema.ts +6 -2
- package/src/config/activation.ts +13 -15
- package/src/config/harness-runtime.ts +26 -36
- package/src/config/index.ts +1 -1
- package/src/config/{pinning.ts → prepared-runtime.ts} +20 -118
- package/src/config/runtime-config.ts +2 -1
- package/src/conversation/message/contract.ts +16 -4
- package/src/conversation/message/conversion.ts +43 -2
- package/src/harness/session-harness.ts +21 -6
- package/src/submission/admission.ts +8 -29
- package/src/submission/lifecycle.ts +28 -15
- package/src/submission/repository.ts +39 -30
- package/src/telemetry/contract.ts +0 -1
- package/src/telemetry/runtime-telemetry.ts +0 -4
- package/src/tools/assistant/ask-user.ts +3 -1
- package/src/tools/extension/index.ts +2 -2
- package/src/tools/web/search/api.ts +1 -1
package/package.json
CHANGED
|
@@ -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 {
|
|
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
|
|
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<
|
|
166
|
-
|
|
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:
|
|
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.
|
|
773
|
+
submission = await this.admission.prepareCurrentRuntime(
|
|
787
774
|
submission,
|
|
788
|
-
() => this.
|
|
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
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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<
|
|
107
|
+
) => Promise<void>;
|
|
103
108
|
|
|
104
|
-
/** Prepare per-Submission Host state after admission and before
|
|
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<
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
|
|
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 =
|
|
810
|
+
this.loadedRuntimeKey = prepared.runtimeKey;
|
|
684
811
|
this.runtimeLoad.complete();
|
|
685
812
|
// 提交之前 turnEvents 绑定还不存在,此前每一次广播都到不了 Host。
|
|
686
813
|
// Host 的会话列表把装配后的第一次投影当作「facet 已就绪」的信号。
|