@springbrand/agent-runtime 0.2.0-alpha.68 → 0.2.0-alpha.69

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.
@@ -1,5 +1,5 @@
1
1
  import type {
2
- AgentOptions,
2
+ AgentLoopConfig,
3
3
  AgentTool,
4
4
  AgentToolResult,
5
5
  } from "@earendil-works/pi-agent-core";
@@ -88,7 +88,7 @@ const governanceState = Symbol("PiToolGovernanceState");
88
88
 
89
89
  /** 向 Pi Agent 提供工具后置钩子,并为编译器保留同一 Turn 的治理状态。 */
90
90
  export interface PiToolGovernance {
91
- readonly afterToolCall: NonNullable<AgentOptions["afterToolCall"]>;
91
+ readonly afterToolCall: NonNullable<AgentLoopConfig["afterToolCall"]>;
92
92
  readonly wrapUpRequested: () => boolean;
93
93
  readonly [governanceState]: PiToolGovernanceState;
94
94
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  createWorkspaceStateBackend,
3
+ STATE_TYPES,
3
4
  type StateBackend,
4
5
  type WorkspaceFsLike,
5
6
  } from "@cloudflare/shell";
@@ -24,6 +25,7 @@ import type {
24
25
  import {
25
26
  WorkspaceStateConnector,
26
27
  WORKSPACE_PAGING_INSTRUCTIONS,
28
+ WORKSPACE_STATE_TYPES,
27
29
  } from "./workspace-state-connector";
28
30
  import type { RuntimeCodeExecutionFactory } from "../../kernel/bindings";
29
31
  import { serializeOutput } from "../../lib/artifacts";
@@ -384,6 +386,10 @@ export function createWorkspaceCodeExecutionFactory(options: {
384
386
  return {
385
387
  description: [
386
388
  description,
389
+ ...(state ? [
390
+ "## Workspace type reference\nOnly methods exposed by connector discovery are callable.\n\n```ts\n" +
391
+ (workspaceState ? WORKSPACE_STATE_TYPES : STATE_TYPES) + "\n```",
392
+ ] : []),
387
393
  ...(workspaceState ? [WORKSPACE_PAGING_INSTRUCTIONS] : []),
388
394
  SPRINGBRAND_CODE_MODE_POLICY,
389
395
  ].join("\n\n"),
@@ -1,4 +1,3 @@
1
- import type { MCPClientManager } from "agents/mcp/client";
2
1
  import type { RuntimeMcpServer } from "../../kernel/profile";
3
2
  import type { PiToolCandidate } from "./compiler";
4
3
  import { createPiDeclaredToolCandidate } from "./declared";
@@ -51,7 +50,8 @@ export interface PiMcpHost {
51
50
  }>;
52
51
  };
53
52
  /** 提供原始工具发现,以及对指定 MCP Server 的一次工具调用。 */
54
- readonly mcp: Pick<MCPClientManager, "listTools"> & {
53
+ readonly mcp: {
54
+ listTools(filter?: { serverId: string[]; state: 'ready' }): PiDiscoveredMcpTool[];
55
55
  /**
56
56
  * 调用一个已发现的 MCP 工具。
57
57
  *
@@ -59,7 +59,7 @@ export interface PiMcpHost {
59
59
  * 参数和中止信号必须原样交给 Agents SDK,避免另造一套传输生命周期。
60
60
  */
61
61
  callTool(
62
- params: Parameters<MCPClientManager["callTool"]>[0],
62
+ params: { serverId: string; name: string; arguments?: Record<string, unknown> },
63
63
  options?: { signal?: AbortSignal },
64
64
  ): Promise<unknown>;
65
65
  };
@@ -68,8 +68,12 @@ export interface PiMcpHost {
68
68
  // 作用:给一个远端 MCP 工具生成 Pi 模型可见的稳定名称。
69
69
  // 调用:候选工厂在映射每个已发现工具时调用。
70
70
  // 原因:服务器前缀隔离同名工具,去掉连字符则保持现有 Pi 工具名格式。
71
- function modelVisibleName(serverId: string, toolName: string): string {
72
- return `tool_${serverId.replace(/-/g, "")}_${toolName}`;
71
+ async function modelVisibleName(serverId: string, toolName: string): Promise<string> {
72
+ const name = `tool_${serverId.replace(/-/g, "")}_${toolName}`;
73
+ if (/^[A-Za-z0-9_-]{1,64}$/.test(name)) return name;
74
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify([serverId, toolName])));
75
+ const hash = Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
76
+ return `mcp_${toolName.replace(/[^A-Za-z0-9_]/g, '_').slice(0, 26)}_${hash.slice(0, 32)}`;
73
77
  }
74
78
 
75
79
  // 作用:把任意 MCP 返回值尽量转换成可展示文本。
@@ -245,6 +249,9 @@ export function createPiMcpToolCandidate(
245
249
  requiredExecutionLevel: policy.requiredExecutionLevel,
246
250
  requiredExecutionLevelForInput: policy.requiredExecutionLevelForInput,
247
251
  source: "action",
252
+ // Human approval must park a first-class Pi Tool, not a nested Code Mode
253
+ // RPC whose execution timer continues while the person is deciding.
254
+ exposureMode: 'direct',
248
255
  project: (reply) => {
249
256
  const result = normalizeCallResult(reply);
250
257
  if (result.isError) throw new Error(errorMessage(result));
@@ -279,16 +286,17 @@ export function createPiMcpToolCandidate(
279
286
  *
280
287
  * `Host`、`Pi`、`Runtime Snapshot` 与“候选工具”见 `../../index.ts`。
281
288
  */
282
- export function createPiMcpToolCandidates(
289
+ export async function createPiMcpToolCandidates(
283
290
  host: PiMcpHost,
284
291
  configured: readonly RuntimeMcpServer[],
285
- ): PiToolCandidate[] {
292
+ ): Promise<PiToolCandidate[]> {
286
293
  // TODO(待确认):Runtime 配置目前只 trim URL,而 agents@0.19.0 会用
287
294
  // new URL(url).href 保存连接地址;`https://x` 与 `https://x/`
288
295
  // 可能因此精确匹配失败。
289
296
  const configuredUrls = new Set(
290
- configured.map((server) => server.url),
297
+ configured.filter(server => !server.id).map((server) => server.url),
291
298
  );
299
+ const configuredIds = new Map(configured.filter(server => server.id).map(server => [server.id!, server.url]));
292
300
  const selectedUrls = new Set<string>();
293
301
  const selectedServerIds: string[] = [];
294
302
 
@@ -297,24 +305,24 @@ export function createPiMcpToolCandidates(
297
305
  )) {
298
306
  if (
299
307
  server.state !== "ready" ||
300
- !configuredUrls.has(server.server_url) ||
301
- selectedUrls.has(server.server_url)
308
+ !(configuredIds.get(serverId) === server.server_url || configuredUrls.has(server.server_url)) ||
309
+ (!configuredIds.has(serverId) && selectedUrls.has(server.server_url))
302
310
  ) {
303
311
  continue;
304
312
  }
305
- selectedUrls.add(server.server_url);
313
+ if (!configuredIds.has(serverId)) selectedUrls.add(server.server_url);
306
314
  selectedServerIds.push(serverId);
307
315
  }
308
316
  if (selectedServerIds.length === 0) return [];
309
317
 
310
318
  const selectedIds = new Set(selectedServerIds);
311
- return host.mcp
319
+ return Promise.all(host.mcp
312
320
  .listTools({
313
321
  serverId: selectedServerIds,
314
322
  state: "ready",
315
323
  })
316
324
  .filter((tool) => selectedIds.has(tool.serverId))
317
- .map((mcpTool) => createPiMcpToolCandidate(
325
+ .map(async (mcpTool) => createPiMcpToolCandidate(
318
326
  mcpTool,
319
327
  (args, signal) => host.mcp.callTool(
320
328
  {
@@ -326,10 +334,10 @@ export function createPiMcpToolCandidates(
326
334
  ),
327
335
  {
328
336
  owner: `mcp:${mcpTool.serverId}`,
329
- modelName: modelVisibleName(mcpTool.serverId, mcpTool.name),
337
+ modelName: await modelVisibleName(mcpTool.serverId, mcpTool.name),
330
338
  // Remote MCP annotations are untrusted; all user-configured methods
331
339
  // retain the existing high-risk approval policy.
332
340
  requiredExecutionLevel: "high",
333
341
  },
334
- ));
342
+ )));
335
343
  }
@@ -82,9 +82,6 @@ async function recordSubagentResult(
82
82
  runId: run.runId,
83
83
  subagentRunId: run.runId,
84
84
  parentRunId: lifecycle.parentRunId,
85
- ...(lifecycle.accountId ? { accountId: lifecycle.accountId } : {}),
86
- ...(lifecycle.rateVersion !== undefined ? { rateVersion: lifecycle.rateVersion } : {}),
87
- ...(lifecycle.slotIdentity ? { slotIdentity: lifecycle.slotIdentity } : {}),
88
85
  kind: "subagent",
89
86
  status,
90
87
  usage: run.usage,
@@ -157,9 +154,6 @@ export function subagentPiToolCandidates(
157
154
  parentRunId: lifecycle.parentRunId,
158
155
  submissionId: lifecycle.submissionId,
159
156
  ...(reserved ? { subagentRunId: reserved.runId } : {}),
160
- ...(lifecycle.accountId ? { accountId: lifecycle.accountId } : {}),
161
- ...(lifecycle.rateVersion !== undefined ? { rateVersion: lifecycle.rateVersion } : {}),
162
- ...(lifecycle.slotIdentity ? { slotIdentity: lifecycle.slotIdentity } : {}),
163
157
  }
164
158
  : undefined;
165
159
  const run = lifecycle && reserved
@@ -239,9 +233,6 @@ export function subagentPiToolCandidates(
239
233
  parentRunId: lifecycle.parentRunId,
240
234
  submissionId: lifecycle.submissionId,
241
235
  ...(reserved ? { subagentRunId: reserved.runId } : {}),
242
- ...(lifecycle.accountId ? { accountId: lifecycle.accountId } : {}),
243
- ...(lifecycle.rateVersion !== undefined ? { rateVersion: lifecycle.rateVersion } : {}),
244
- ...(lifecycle.slotIdentity ? { slotIdentity: lifecycle.slotIdentity } : {}),
245
236
  }
246
237
  : {}),
247
238
  };
@@ -1,3 +1,4 @@
1
+ import { STATE_TYPES } from "@cloudflare/shell";
1
2
  import type { StateBackend } from "@cloudflare/shell/workers";
2
3
  import { StateConnector } from "@cloudflare/shell/workers";
3
4
  import type { ConnectorTools } from "@cloudflare/codemode";
@@ -11,6 +12,11 @@ type WorkspaceFilePage =
11
12
  | { kind: "csv"; size: number; nextOffset: number | null; columns: string[] | null; rows: string[][] };
12
13
  `;
13
14
 
15
+ export const WORKSPACE_STATE_TYPES = `${READ_FILE_PAGE_TYPES}\n${STATE_TYPES.replace(
16
+ /\n\};$/u,
17
+ '\n /** CSV pages have columns and rows; never data. Omit offset first, then pass nextOffset until null. */\n readFilePage(args: { path: string; offset?: number }): Promise<WorkspaceFilePage | null>;\n};',
18
+ )}`;
19
+
14
20
  export const WORKSPACE_PAGING_INSTRUCTIONS = `For CSV files, do not make a separate probe read or return page contents. Inspect columns and aggregate rows in the same execution with this loop shape:
15
21
 
16
22
  let page = await state.readFilePage({ path });
@@ -97,12 +103,7 @@ ${WORKSPACE_PAGING_INSTRUCTIONS}`;
97
103
  }
98
104
 
99
105
  override async getTypeScriptTypes(): Promise<string> {
100
- const types = await super.getTypeScriptTypes();
101
- const addition = [
102
- " /** CSV pages have columns and rows; never data. Omit offset first, then pass nextOffset until null. */",
103
- " readFilePage(args: { path: string; offset?: number }): Promise<WorkspaceFilePage | null>;",
104
- ].join("\n");
105
- return `${READ_FILE_PAGE_TYPES}\n${types.replace(/\n\};$/u, `\n${addition}\n};`)}`;
106
+ return WORKSPACE_STATE_TYPES;
106
107
  }
107
108
  }
108
109
 
@@ -96,6 +96,11 @@ interface RuntimeAgentDefinitionBase<
96
96
  /** 必须等于 Worker 导出名(Wrangler `class_name`)。 */
97
97
  readonly name: string;
98
98
 
99
+ /** Refresh Host-owned inputs before a new Submission is pinned. Never runs on continuation/recovery. */
100
+ readonly beforeSubmission?: (
101
+ context: RuntimeAgentPlanningContext<Env, Command, Change>,
102
+ ) => Promise<boolean>;
103
+
99
104
  readonly load: (
100
105
  context: RuntimeAgentPlanningContext<Env, Command, Change>,
101
106
  requestedRuntimeKey?: string,
@@ -444,6 +449,12 @@ export function defineRuntimeAgent<
444
449
  return this.ensureConfig();
445
450
  }
446
451
 
452
+ protected async prepareRuntimeForSubmission(): Promise<void> {
453
+ const beforeSubmission = definition.beforeSubmission as RuntimeAgentDefinitionBase<Env, Config, Command, Change>["beforeSubmission"];
454
+ const changed = await beforeSubmission?.(this.createDefinitionContext());
455
+ await this.ensureConfig(undefined, changed === true);
456
+ }
457
+
447
458
  // 作用:加载默认 Runtime,或切换到调用方指定的 key。
448
459
  // 调用:RPC 控制面在业务配置需要刷新时调用。
449
460
  // 原因:复用 `ensureConfig`,让手动重载遵守和首次启动相同的原子规则。
@@ -1,6 +1,7 @@
1
1
  import type { SkillSource } from "agents/skills";
2
2
  import type {
3
3
  RuntimeGatewayPort,
4
+ RuntimeMcpHost,
4
5
  RuntimeMemoryPort,
5
6
  RuntimePlatformPort,
6
7
  RuntimeProviderPort,
@@ -95,6 +96,7 @@ export interface RuntimeAssemblyInput {
95
96
  readonly skills: readonly RuntimeSkillContribution[];
96
97
  readonly connectors: readonly RuntimeMcpServer[];
97
98
  readonly gateway?: RuntimeGatewayPort;
99
+ readonly managedMcp?: RuntimeMcpHost;
98
100
  readonly extensions: readonly RuntimeExtensionContribution[];
99
101
  readonly subagents?: RuntimeSubagentPort;
100
102
  readonly schedule?: RuntimeSchedulePort;
@@ -368,6 +370,7 @@ class RuntimeBuilder {
368
370
  private provider?: RuntimeProviderPort;
369
371
  private platform?: RuntimePlatformPort;
370
372
  private gateway?: RuntimeGatewayPort;
373
+ private managedMcp?: RuntimeMcpHost;
371
374
  private workspace?: WorkspacePort;
372
375
  private codeExecution?: RuntimeCodeExecutionFactory;
373
376
  private sandbox?: RuntimeSandboxPort;
@@ -416,6 +419,10 @@ class RuntimeBuilder {
416
419
  for (const skill of input.skills) this.addSkill(skill);
417
420
  for (const connector of input.connectors) this.addConnector(connector);
418
421
  this.gateway = input.gateway;
422
+ this.managedMcp = input.managedMcp;
423
+ for (const server of input.managedMcp?.servers ?? []) {
424
+ this.addConnector({ ...server, hostManaged: true });
425
+ }
419
426
  for (const { name, extension } of input.extensions) {
420
427
  this.addExtension(name, extension);
421
428
  }
@@ -478,13 +485,15 @@ class RuntimeBuilder {
478
485
 
479
486
  private addConnector(server: RuntimeMcpServer): void {
480
487
  const name = requiredName(server.name, "Connector");
481
- if (this.connectors.has(name)) {
488
+ const identity = server.id ?? name;
489
+ if (this.connectors.has(identity)) {
482
490
  throw new Error(`Duplicate Runtime Connector: ${name}`);
483
491
  }
484
492
  if (!server.url.trim()) {
485
493
  throw new Error(`Runtime Connector "${name}" URL must not be empty`);
486
494
  }
487
- this.connectors.set(name, Object.freeze({
495
+ this.connectors.set(identity, Object.freeze({
496
+ ...server,
488
497
  name,
489
498
  url: server.url.trim(),
490
499
  }));
@@ -644,6 +653,7 @@ class RuntimeBuilder {
644
653
  platform: Object.freeze({ ...this.platform }),
645
654
  ...(this.codeExecution ? { codeExecution: this.codeExecution } : {}),
646
655
  ...(this.gateway ? { gateway: this.gateway } : {}),
656
+ ...(this.managedMcp ? { mcp: this.managedMcp } : {}),
647
657
  ...(this.workspace ? { workspace: this.workspace } : {}),
648
658
  ...(this.memoryProfile.enabled && this.memory
649
659
  ? { memory: this.memory }
@@ -805,6 +815,7 @@ export async function assembleRuntimeSnapshot<
805
815
  ? [...BUILT_IN_RUNTIME_SKILLS, ...resources.skills]
806
816
  : resources.skills,
807
817
  connectors: resources.connectors.servers,
818
+ managedMcp: resources.connectors.managed,
808
819
  ...(resources.connectors.gateway
809
820
  ? { gateway: resources.connectors.gateway }
810
821
  : {}),
@@ -2,6 +2,7 @@ import type { SkillSource } from "agents/skills";
2
2
  import type { ExecutionLevel } from "./lib/execution-level";
3
3
  import type {
4
4
  RuntimeGatewayPort,
5
+ RuntimeMcpHost,
5
6
  RuntimePlatformPort,
6
7
  RuntimeProviderPort,
7
8
  RuntimeSkillScriptPolicy,
@@ -79,6 +80,7 @@ export type { RuntimeAgentContext, RuntimeAssemblyContext } from "./runtime-agen
79
80
  export interface ResolvedConnectors {
80
81
  readonly servers: readonly RuntimeMcpServer[];
81
82
  readonly gateway?: RuntimeGatewayPort;
83
+ readonly managed?: RuntimeMcpHost;
82
84
  readonly degradations: readonly RuntimeDegradation[];
83
85
  }
84
86
 
package/src/runtime.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { summarizeModelUsage } from "./model-usage";
1
2
  import type {
2
3
  AgentToolLifecycleResult,
3
4
  AgentToolRunInfo,
@@ -181,9 +182,6 @@ interface StoredSubmission extends SubmissionReceipt {
181
182
  recoveryErrorCount: number;
182
183
  recoveryReason?: SubmissionRecoveryReason;
183
184
  runId: string | null;
184
- accountId: string | null;
185
- rateVersion: number | null;
186
- slotIdentity: string | null;
187
185
  /** 跨执行片累计的模型回合数,全局回合预算据此判断。 */
188
186
  modelTurns: number;
189
187
  /** 当前让步执行片的标识;没有未完成让步时为 null。 */
@@ -281,14 +279,6 @@ function validateAdmissionResult(
281
279
  }
282
280
  return result;
283
281
  }
284
- if (
285
- typeof result.accountId !== "string" || !result.accountId.trim() ||
286
- typeof result.slotIdentity !== "string" || !result.slotIdentity.trim() ||
287
- !Number.isSafeInteger(result.rateVersion) ||
288
- result.rateVersion <= 0
289
- ) {
290
- throw new Error("Runtime admission context is invalid");
291
- }
292
282
  return result;
293
283
  }
294
284
 
@@ -306,24 +296,14 @@ function assistantUsageEvent(
306
296
  eventId: string,
307
297
  submissionId: string,
308
298
  message: AssistantMessage,
309
- pinned?: Pick<StoredSubmission, "accountId" | "rateVersion" | "slotIdentity">,
299
+ requestedModel: RuntimeSnapshot["pi"]["model"],
310
300
  ): RuntimeModelUsageEvent {
311
301
  return {
312
302
  eventId,
313
303
  submissionId,
314
304
  runId: submissionId,
315
- ...(pinned?.accountId ? { accountId: pinned.accountId } : {}),
316
- ...(pinned?.rateVersion !== null && pinned?.rateVersion !== undefined
317
- ? { rateVersion: pinned.rateVersion }
318
- : {}),
319
- ...(pinned?.slotIdentity ? { slotIdentity: pinned.slotIdentity } : {}),
320
305
  kind: "assistant",
321
- api: message.api,
322
- provider: message.provider,
323
- model: message.model,
324
- ...(message.responseModel ? { responseModel: message.responseModel } : {}),
325
- stopReason: message.stopReason,
326
- usage: message.usage,
306
+ ...summarizeModelUsage(requestedModel, [message]),
327
307
  };
328
308
  }
329
309
 
@@ -437,6 +417,10 @@ export abstract class AgentRuntimeKernel<
437
417
  };
438
418
 
439
419
  protected abstract ensureRuntimeReady(): Promise<void>;
420
+ /** Runs only before the first pin of a newly activated Submission. */
421
+ protected prepareRuntimeForSubmission(): Promise<void> {
422
+ return this.ensureRuntimeReady();
423
+ }
440
424
 
441
425
  /** Let the Host prepare a validated client response before Tool settlement. */
442
426
  protected prepareToolInteractionResponse(
@@ -579,7 +563,7 @@ export abstract class AgentRuntimeKernel<
579
563
  interruptActive: (turn) => turn.agent.interrupt(),
580
564
  });
581
565
  this.transcript = this.pi.createTranscript({
582
- sql: this.sql.bind(this),
566
+ storage: this.ctx.storage,
583
567
  durability: {
584
568
  transaction: (run) => this.db.transaction(run),
585
569
  upsertUserSidecar: (id, body) =>
@@ -763,7 +747,7 @@ export abstract class AgentRuntimeKernel<
763
747
  this.runtimeLoad.advance("mcp");
764
748
  await connectConfiguredMcpServers(
765
749
  this,
766
- candidateSnapshot.profile.mcpServers,
750
+ candidateSnapshot.profile.mcpServers.filter(server => !server.hostManaged),
767
751
  );
768
752
  const gatewayDegradations = [];
769
753
  let gatewaySession: RuntimeGatewaySession | undefined;
@@ -812,7 +796,8 @@ export abstract class AgentRuntimeKernel<
812
796
  // 新能力,同一轮接着跑」所依赖的那一步。真正在执行的 Turn 仍然被挡住。
813
797
  const contested = Boolean(previous) &&
814
798
  revision !== previous?.revision &&
815
- this.submissions.isBusy();
799
+ this.submissions.isBusy() &&
800
+ this.db.submissions.listUnfinishedIds().some(id => Boolean(this.readSubmission(id)?.assemblyRevision));
816
801
  const parked = contested && this.db.everyUnfinishedSubmissionParked();
817
802
  if (contested && !parked) {
818
803
  await closeRuntimeGatewaySession(gatewaySession, "gateway.close.contested");
@@ -902,13 +887,6 @@ export abstract class AgentRuntimeKernel<
902
887
  const runMetadata: RuntimeSubagentRunMetadata = {
903
888
  submissionId: submission.submissionId,
904
889
  parentRunId: submission.runId ?? submission.submissionId,
905
- ...(submission.accountId ? { accountId: submission.accountId } : {}),
906
- ...(submission.rateVersion !== null
907
- ? { rateVersion: submission.rateVersion }
908
- : {}),
909
- ...(submission.slotIdentity
910
- ? { slotIdentity: submission.slotIdentity }
911
- : {}),
912
890
  };
913
891
  const lifecycle = this.subagentLifecycle(runMetadata);
914
892
  const toolExecutors = snapshot.bindings.subagents
@@ -1077,7 +1055,7 @@ export abstract class AgentRuntimeKernel<
1077
1055
  persisted.id,
1078
1056
  submission.submissionId,
1079
1057
  commit.message,
1080
- submission,
1058
+ snapshot.pi.model,
1081
1059
  );
1082
1060
  this.db.runtimeEvents.insert({
1083
1061
  eventId: event.eventId,
@@ -1107,7 +1085,6 @@ export abstract class AgentRuntimeKernel<
1107
1085
  : undefined;
1108
1086
  this.transcript.append(commit.id, commit.message, {
1109
1087
  submissionId: submission.submissionId,
1110
- createdAt: commit.message.timestamp,
1111
1088
  ...(uiMessage ? { userMessage: uiMessage } : {}),
1112
1089
  });
1113
1090
  this.db.steers.deleteByMessageId(commit.id);
@@ -1558,11 +1535,12 @@ export abstract class AgentRuntimeKernel<
1558
1535
  // 作用:为新 Submission 生成包含当前 Context 的不可变装配描述。
1559
1536
  // 调用:Submission 通过去重和忙碌检查后,在真正写入前调用。
1560
1537
  // 原因:把动态 Context 和执行档位固定在准入时,恢复时才能按原配置继续。
1561
- private async admissionPin(): Promise<{
1538
+ private async admissionPin(fresh: boolean): Promise<{
1562
1539
  revision: string;
1563
1540
  descriptor: string;
1564
1541
  }> {
1565
- await this.ensureRuntimeReady();
1542
+ if (fresh) await this.prepareRuntimeForSubmission();
1543
+ else await this.ensureRuntimeReady();
1566
1544
  return this.currentAssemblyPin();
1567
1545
  }
1568
1546
 
@@ -1606,6 +1584,7 @@ export abstract class AgentRuntimeKernel<
1606
1584
 
1607
1585
  private async pinSubmissionAssembly(
1608
1586
  submission: StoredSubmission,
1587
+ mode: SubmissionExecutionMode,
1609
1588
  ): Promise<StoredSubmission> {
1610
1589
  if (submission.assemblyRevision && submission.assemblyDescriptor) {
1611
1590
  return submission;
@@ -1613,7 +1592,7 @@ export abstract class AgentRuntimeKernel<
1613
1592
  if (submission.assemblyRevision || submission.assemblyDescriptor) {
1614
1593
  throw new Error("Submission assembly pin is incomplete");
1615
1594
  }
1616
- const { revision, descriptor } = await this.admissionPin();
1595
+ const { revision, descriptor } = await this.admissionPin(mode === 'fresh');
1617
1596
  this.db.submissions.pinAssembly(
1618
1597
  submission.submissionId,
1619
1598
  revision,
@@ -1744,7 +1723,7 @@ export abstract class AgentRuntimeKernel<
1744
1723
  model: snapshot.pi.model,
1745
1724
  signal,
1746
1725
  submissionId,
1747
- onCompactionPersisted: (event) => {
1726
+ onCompactionUsage: (event, didCompact) => {
1748
1727
  this.db.runtimeEvents.insert({
1749
1728
  eventId: event.eventId,
1750
1729
  body: json({
@@ -1752,21 +1731,11 @@ export abstract class AgentRuntimeKernel<
1752
1731
  event: {
1753
1732
  ...event,
1754
1733
  runId: submissionId,
1755
- ...(this.readSubmission(submissionId)?.accountId
1756
- ? { accountId: this.readSubmission(submissionId)!.accountId! }
1757
- : {}),
1758
- ...(this.readSubmission(submissionId)?.rateVersion !== null &&
1759
- this.readSubmission(submissionId)?.rateVersion !== undefined
1760
- ? { rateVersion: this.readSubmission(submissionId)!.rateVersion! }
1761
- : {}),
1762
- ...(this.readSubmission(submissionId)?.slotIdentity
1763
- ? { slotIdentity: this.readSubmission(submissionId)!.slotIdentity! }
1764
- : {}),
1765
1734
  },
1766
1735
  }),
1767
1736
  createdAt: Date.now(),
1768
1737
  });
1769
- this.telemetry.capture("contextCompacted", {
1738
+ if (didCompact) this.telemetry.capture("contextCompacted", {
1770
1739
  submissionId,
1771
1740
  compactionId: event.eventId,
1772
1741
  inputTokens: event.usage.input,
@@ -1907,9 +1876,6 @@ export abstract class AgentRuntimeKernel<
1907
1876
  let admission = existing?.status === "accepted"
1908
1877
  ? {
1909
1878
  accepted: true as const,
1910
- accountId: existing.accountId!,
1911
- rateVersion: existing.rateVersion!,
1912
- slotIdentity: existing.slotIdentity!,
1913
1879
  }
1914
1880
  : existing?.status === "rejected"
1915
1881
  ? {
@@ -1951,9 +1917,6 @@ export abstract class AgentRuntimeKernel<
1951
1917
  this.db.submissionAdmissions.saveAccepted(
1952
1918
  admissionRequestId,
1953
1919
  runId,
1954
- admission.accountId,
1955
- admission.rateVersion,
1956
- admission.slotIdentity,
1957
1920
  );
1958
1921
  }
1959
1922
  this.db.submissions.insert({
@@ -1973,9 +1936,6 @@ export abstract class AgentRuntimeKernel<
1973
1936
  ? options.userMessageId ?? null
1974
1937
  : null,
1975
1938
  runId,
1976
- accountId: admission?.accepted ? admission.accountId : null,
1977
- rateVersion: admission?.accepted ? admission.rateVersion : null,
1978
- slotIdentity: admission?.accepted ? admission.slotIdentity : null,
1979
1939
  });
1980
1940
  if (admission?.accepted) {
1981
1941
  const fact: RuntimeLifecycleFact = {
@@ -1983,9 +1943,6 @@ export abstract class AgentRuntimeKernel<
1983
1943
  eventId: `${submissionId}:admitted`,
1984
1944
  submissionId,
1985
1945
  runId: submissionId,
1986
- accountId: admission.accountId,
1987
- rateVersion: admission.rateVersion,
1988
- slotIdentity: admission.slotIdentity,
1989
1946
  };
1990
1947
  this.db.runtimeEvents.insert({
1991
1948
  eventId: fact.eventId,
@@ -2086,13 +2043,6 @@ export abstract class AgentRuntimeKernel<
2086
2043
  zeroUsage: !this.db.runtimeEvents.hasNonZeroModelUsage(
2087
2044
  submission.submissionId,
2088
2045
  ),
2089
- ...(submission.accountId ? { accountId: submission.accountId } : {}),
2090
- ...(submission.rateVersion !== null
2091
- ? { rateVersion: submission.rateVersion }
2092
- : {}),
2093
- ...(submission.slotIdentity
2094
- ? { slotIdentity: submission.slotIdentity }
2095
- : {}),
2096
2046
  };
2097
2047
  this.db.runtimeEvents.insert({
2098
2048
  eventId: terminalFact.eventId,
@@ -2859,7 +2809,6 @@ export abstract class AgentRuntimeKernel<
2859
2809
  userMessage,
2860
2810
  {
2861
2811
  submissionId: submission.submissionId,
2862
- createdAt: userMessage.timestamp,
2863
2812
  ...(uiMessage ? { userMessage: uiMessage } : {}),
2864
2813
  },
2865
2814
  );
@@ -2991,7 +2940,7 @@ export abstract class AgentRuntimeKernel<
2991
2940
  submission.abortReason,
2992
2941
  );
2993
2942
  }
2994
- submission = await this.pinSubmissionAssembly(submission);
2943
+ submission = await this.pinSubmissionAssembly(submission, mode);
2995
2944
  await this.assertPinnedSubmission(submission);
2996
2945
  if (submission.status === "pending" && mode === "fresh") {
2997
2946
  submission = await this.activateQueuedSubmission(submission);
@@ -3667,7 +3616,9 @@ export abstract class AgentRuntimeKernel<
3667
3616
  private async hydrationMessages(): Promise<UIMessage[]> {
3668
3617
  const messages = await this.getMessages();
3669
3618
  const running = this.db.submissions.findRunning();
3670
- return running?.assistantMessageId
3619
+ // After a restart there is no in-memory replay stream. In particular, a
3620
+ // parked approval cannot recreate one until the user can see and decide it.
3621
+ return running?.assistantMessageId && this.streamBySubmission.has(running.submissionId)
3671
3622
  ? messages.filter((message) =>
3672
3623
  message.role !== "assistant" ||
3673
3624
  (message.id !== running.assistantMessageId &&
@@ -3804,9 +3755,7 @@ export abstract class AgentRuntimeKernel<
3804
3755
  await this.broadcastApprovals();
3805
3756
  return;
3806
3757
  }
3807
- this.transcript.append(crypto.randomUUID(), message, {
3808
- createdAt: message.timestamp,
3809
- });
3758
+ this.transcript.append(crypto.randomUUID(), message);
3810
3759
  }
3811
3760
 
3812
3761
  // 作用:返回扩展可见的当前 Session 摘要。
@@ -4260,7 +4209,6 @@ export abstract class AgentRuntimeKernel<
4260
4209
  : undefined;
4261
4210
  this.transcript.append(steer.messageId, message, {
4262
4211
  submissionId: current.submissionId,
4263
- createdAt: steer.createdAt,
4264
4212
  ...(userMessage ? { userMessage } : {}),
4265
4213
  });
4266
4214
  this.db.steers.deleteByMessageId(steer.messageId);
@@ -4613,13 +4561,6 @@ export abstract class AgentRuntimeKernel<
4613
4561
  runId: run.runId,
4614
4562
  subagentRunId: run.runId,
4615
4563
  parentRunId: metadata.parentRunId,
4616
- ...(metadata.accountId ? { accountId: metadata.accountId } : {}),
4617
- ...(metadata.rateVersion !== undefined
4618
- ? { rateVersion: metadata.rateVersion }
4619
- : {}),
4620
- ...(metadata.slotIdentity
4621
- ? { slotIdentity: metadata.slotIdentity }
4622
- : {}),
4623
4564
  kind: "subagent",
4624
4565
  status,
4625
4566
  usage: completion.usage,