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

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.
@@ -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
+ export 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 已就绪」的信号。
@@ -19,8 +19,6 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
19
19
  error TEXT,
20
20
  created_at INTEGER NOT NULL,
21
21
  completed_at INTEGER,
22
- assembly_revision TEXT NOT NULL,
23
- assembly_descriptor TEXT NOT NULL,
24
22
  assistant_message_id TEXT NOT NULL,
25
23
  abort_reason TEXT
26
24
  )`;
@@ -28,6 +26,12 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
28
26
  sql<{ name: string }>`PRAGMA table_info(pi_submissions)`
29
27
  .map((column) => column.name),
30
28
  );
29
+ if (submissionColumns.has("assembly_revision")) {
30
+ sql`ALTER TABLE pi_submissions DROP COLUMN assembly_revision`;
31
+ }
32
+ if (submissionColumns.has("assembly_descriptor")) {
33
+ sql`ALTER TABLE pi_submissions DROP COLUMN assembly_descriptor`;
34
+ }
31
35
  if (!submissionColumns.has("runtime_key")) {
32
36
  sql`ALTER TABLE pi_submissions ADD COLUMN runtime_key TEXT NOT NULL DEFAULT ''`;
33
37
  }
@@ -2,7 +2,7 @@ import { connectConfiguredMcpServers } from "../lib/mcp";
2
2
  import { sha256Hex } from "../lib/artifacts";
3
3
  import type { RuntimeCandidate, RuntimeSnapshot } from "./assembler";
4
4
  import type { HarnessRuntimeConfig } from "./harness-runtime";
5
- import type { PreparedPiRuntime, PreparePiRuntimeOptions } from "./pinning";
5
+ import type { PreparedPiRuntime, PreparePiRuntimeOptions } from "./prepared-runtime";
6
6
  import type { RuntimeGatewaySession, RuntimeSubmissionAdmissionHook } from "./ports";
7
7
  import { withRuntimeLoadTimeout } from "./runtime-load";
8
8
  import type { RuntimeLoadPhase } from "../conversation/state";
@@ -16,6 +16,16 @@ export interface ActiveRuntime {
16
16
  readonly admission?: RuntimeSubmissionAdmissionHook;
17
17
  }
18
18
 
19
+ /** Check the same candidate guards during admission and final activation. */
20
+ export async function verifyRuntimeCandidateGuards(
21
+ candidate: RuntimeCandidate,
22
+ phase: "admission" | "commit",
23
+ ): Promise<void> {
24
+ for (const [index, guard] of candidate.commitGuards.entries()) {
25
+ await withRuntimeLoadTimeout(`${phase}-guard:${index}`, guard);
26
+ }
27
+ }
28
+
19
29
  async function closeRuntimeGatewaySession(
20
30
  session: RuntimeGatewaySession | undefined,
21
31
  step: string,
@@ -33,7 +43,6 @@ interface RuntimeActivationOptions {
33
43
  readonly sessionId: string;
34
44
  readonly createExtensionHostBinding: PreparePiRuntimeOptions["createExtensionHostBinding"];
35
45
  advance(phase: RuntimeLoadPhase): void;
36
- isExecuting(): boolean;
37
46
  commit(runtime: ActiveRuntime): void;
38
47
  }
39
48
 
@@ -92,19 +101,10 @@ export async function activateRuntimeCandidate(
92
101
  throw error;
93
102
  }
94
103
  const revision = await sha256Hex(prepared.revisionDescriptor);
95
- const contested = Boolean(previous) &&
96
- revision !== previous?.revision &&
97
- options.isExecuting();
98
- if (contested) {
99
- await closeRuntimeGatewaySession(gatewaySession, "gateway.close.contested");
100
- throw new Error(
101
- "Cannot reload Runtime while a revision-pinned SpringBrand Turn is active",
102
- );
103
- }
104
104
  const next = Object.freeze({
105
105
  snapshot: candidateSnapshot,
106
106
  revision,
107
- runtimeKey: candidate.runtimeKey ?? revision,
107
+ runtimeKey: candidate.runtimeKey ?? "default",
108
108
  pi: prepared,
109
109
  ...(gatewaySession ? { gatewaySession } : {}),
110
110
  ...(candidate.admission
@@ -114,9 +114,7 @@ export async function activateRuntimeCandidate(
114
114
  let activated = false;
115
115
  const previousGatewaySession = previous?.gatewaySession;
116
116
  try {
117
- for (const [index, guard] of candidate.commitGuards.entries()) {
118
- await withRuntimeLoadTimeout(`commit-guard:${index}`, guard);
119
- }
117
+ await verifyRuntimeCandidateGuards(candidate, "commit");
120
118
  options.pi.activate(candidateSnapshot);
121
119
  activated = true;
122
120
  options.commit(next);
@@ -15,19 +15,17 @@ export {
15
15
  readModelStreamStallDetails,
16
16
  } from "../models";
17
17
  import {
18
- pinPiRuntime,
18
+ assembleCurrentPiContext,
19
19
  preparePiRuntime,
20
- readPinnedPiRuntime,
21
20
  readPreparedPiRuntime,
22
- type PinPiRuntimeOptions,
23
- type PinnedPiRuntime,
21
+ type CurrentPiContextOptions,
24
22
  type PreparePiRuntimeOptions,
25
23
  type PreparedPiRuntime,
26
- } from "./pinning";
24
+ } from "./prepared-runtime";
27
25
  import { refreshModelGuidance } from "./model-guidance";
28
26
 
29
27
  /**
30
- * Runtime 通过这个配置入口准备、固定并读取 Harness 的模型、提示和工具输入。
28
+ * Runtime 通过这个配置入口准备并读取当前 Harness 的模型、提示和工具输入。
31
29
  *
32
30
  * @remarks
33
31
  * Runtime、Pi、Turn、Tool Candidate 和 canonical transcript 等核心术语见包入口 `../../index.ts`。
@@ -37,9 +35,7 @@ import { refreshModelGuidance } from "./model-guidance";
37
35
  * 它包含已加载 Extension、Tool candidate 和快照,但 Runtime 只能读取 revision descriptor 与 degradation。
38
36
  * - revision descriptor 是影响执行行为的已排序 JSON 描述。
39
37
  * Runtime 会对它取哈希,用来识别当前 Prepared Runtime 的基础版本。
40
- * - Pinned Runtime 是 Submission 准入时固定的描述符。
41
- * 它在 Prepared Runtime 基础上加入动态 system context、approval mode 和 base revision。
42
- * 调用方:`AgentRuntimeKernel` 在配置加载、请求准入与打开 SessionHarness 前调用这些粗粒度能力。
38
+ * 调用方:`AgentRuntimeKernel` 在配置加载与打开 SessionHarness 前调用这些粗粒度能力。
43
39
  *
44
40
  * 执行位置仍由 `SessionHarness` 拥有,消息读写和投影由 `ConversationHistory` 拥有。
45
41
  */
@@ -86,23 +82,31 @@ export class HarnessRuntimeConfig {
86
82
  );
87
83
  }
88
84
 
89
- /** Read the immutable inputs consumed by the Session AgentHarness. */
90
- harnessInputs(
85
+ interaction(prepared: PreparedPiRuntime, name: string) {
86
+ return readPreparedPiRuntime(prepared, this.owner).candidates.find(
87
+ (candidate) => candidate.tool.name === name,
88
+ )?.interaction;
89
+ }
90
+
91
+ /** Read all executable inputs from the current Prepared Runtime. */
92
+ async harnessInputs(
91
93
  prepared: PreparedPiRuntime,
92
- pinnedDescriptor: string,
93
94
  request: {
94
95
  readonly modelSessionId?: string | (() => string);
95
96
  readonly onGeneration?: PiGenerationLifecycleObserver;
97
+ readonly includeMemory?: boolean;
98
+ readonly readExtensionContext?: CurrentPiContextOptions["readExtensionContext"];
96
99
  readonly assertRequestAllowed?: () => void;
97
100
  readonly onCompactionGeneration?: PiGenerationLifecycleObserver;
98
101
  } = {},
99
102
  ) {
100
- const { state, descriptor } = readPinnedPiRuntime(
103
+ const state = readPreparedPiRuntime(prepared, this.owner);
104
+ const context = await assembleCurrentPiContext({
101
105
  prepared,
102
- this.owner,
103
- pinnedDescriptor,
104
- );
105
- const pinnedModel = state.snapshot.pi.model;
106
+ includeMemory: request.includeMemory,
107
+ readExtensionContext: request.readExtensionContext,
108
+ }, this.owner);
109
+ const currentModel = state.snapshot.pi.model;
106
110
  let models!: MutableModels;
107
111
  const requestStream = (observer = request.onGeneration) => {
108
112
  const stream = withProviderBoundary(
@@ -121,8 +125,8 @@ export class HarnessRuntimeConfig {
121
125
  get: (target, property) => {
122
126
  if (property === "getModel") {
123
127
  return (provider: string, modelId: string) =>
124
- provider === pinnedModel.provider && modelId === pinnedModel.id
125
- ? pinnedModel
128
+ provider === currentModel.provider && modelId === currentModel.id
129
+ ? currentModel
126
130
  : target.getModel(provider, modelId);
127
131
  }
128
132
  if (property === "streamSimple") return requestStream();
@@ -147,7 +151,8 @@ export class HarnessRuntimeConfig {
147
151
  models,
148
152
  model: state.snapshot.pi.model,
149
153
  thinkingLevel: state.snapshot.pi.thinkingLevel,
150
- systemPrompt: descriptor.systemPrompt,
154
+ systemPrompt: context.systemPrompt,
155
+ degradations: context.degradations,
151
156
  candidates: state.candidates,
152
157
  workspace: state.snapshot.bindings.workspace,
153
158
  platform: state.snapshot.bindings.platform,
@@ -179,27 +184,12 @@ export class HarnessRuntimeConfig {
179
184
  }
180
185
  }
181
186
 
182
- /**
183
- * 在接受 Submission 前固定本次执行使用的动态上下文和审批模式。
184
- *
185
- * @remarks
186
- * 调用方:Runtime 的准入流程为每个新 Submission 调用,并把 descriptor 与其哈希一起持久化。
187
- *
188
- * 实现理由:memory、workspace 和 Extension context 可能随时间变化,所以把准入时的 Turn 字段保存在 descriptor 中。
189
- * 不要把 pin 推迟到 Turn 启动后,否则已持久化的 Submission 将失去可验证的执行基线。
190
- */
191
- pin(options: PinPiRuntimeOptions): Promise<PinnedPiRuntime> {
192
- return pinPiRuntime(options, this.owner);
193
- }
194
-
195
187
  }
196
188
 
197
189
  export type {
198
- PinPiRuntimeOptions,
199
- PinnedPiRuntime,
200
190
  PreparePiRuntimeOptions,
201
191
  PreparedPiRuntime,
202
- } from "./pinning";
192
+ } from "./prepared-runtime";
203
193
 
204
194
  /**
205
195
  * Input tokens above which a Turn compacts its history.
@@ -7,7 +7,7 @@ export * from "./assembly-view";
7
7
  export * from "./definition";
8
8
  export * from "./degradation";
9
9
  export * from "./harness-runtime";
10
- export * from "./pinning";
10
+ export * from "./prepared-runtime";
11
11
  export * from "./ports";
12
12
  export * from "./profile";
13
13
  export * from "./runtime-config";