@springbrand/agent-runtime 0.1.3-alpha.8 → 0.2.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/package.json +4 -3
  2. package/src/adapter/cloudflare/index.ts +1 -0
  3. package/src/adapter/cloudflare/sandbox/adapter.ts +4 -0
  4. package/src/adapter/cloudflare/subagent/definition.ts +60 -5
  5. package/src/adapter/cloudflare/universal-agent/hooks.ts +23 -1
  6. package/src/adapter/cloudflare/universal-agent/preparation.ts +36 -2
  7. package/src/adapter/cloudflare/universal-agent/tools.ts +9 -12
  8. package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
  9. package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
  10. package/src/db/index.ts +4 -0
  11. package/src/db/runtime-event-outbox.repo.ts +34 -1
  12. package/src/db/schema.ts +42 -0
  13. package/src/db/submission-admission.repo.ts +127 -0
  14. package/src/db/submission.repo.ts +54 -3
  15. package/src/index.ts +5 -2
  16. package/src/kernel/bindings.ts +52 -0
  17. package/src/kernel/durable-lifecycle.ts +100 -0
  18. package/src/kernel/public-contracts.ts +11 -0
  19. package/src/kernel/receipts.ts +1 -0
  20. package/src/kernel/recoverable-chat-agent.ts +0 -19
  21. package/src/kernel/subagent-runtime.ts +137 -0
  22. package/src/kernel/submission-authority.ts +114 -0
  23. package/src/kernel/submission-lifecycle.ts +35 -10
  24. package/src/layers/context/budget/gate.ts +99 -0
  25. package/src/lib/prompt.ts +7 -3
  26. package/src/pi/message/projection.ts +2 -2
  27. package/src/pi/runtime-adapter/assembly.ts +4 -3
  28. package/src/pi/runtime-adapter/execution.ts +55 -16
  29. package/src/pi/runtime-adapter/index.ts +34 -0
  30. package/src/pi/runtime-adapter/models.ts +10 -1
  31. package/src/pi/runtime-adapter/recovery.ts +17 -17
  32. package/src/pi/runtime-adapter/transcript.ts +2 -2
  33. package/src/pi/tool/base.ts +113 -27
  34. package/src/pi/tool/compiler.ts +66 -6
  35. package/src/pi/tool/core-host.ts +50 -23
  36. package/src/pi/tool/core.ts +2 -23
  37. package/src/pi/tool/index.ts +1 -0
  38. package/src/pi/tool/mcp.ts +2 -2
  39. package/src/pi/tool/skill.ts +78 -3
  40. package/src/pi/tool/subagent.ts +142 -16
  41. package/src/pi/tool/workspace-revision.ts +64 -0
  42. package/src/pi/tool/workspace-sandbox.ts +4 -4
  43. package/src/pi/turn/tool-recovery.ts +7 -7
  44. package/src/runtime-agent-context.ts +1 -0
  45. package/src/runtime-agent.ts +8 -3
  46. package/src/runtime-assembler.ts +61 -9
  47. package/src/runtime-definition.ts +12 -0
  48. package/src/runtime.ts +485 -66
  49. package/src/tool-registry.ts +10 -1
  50. package/src/workspace-versioning.ts +46 -0
package/src/runtime.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  import { InteractionLifecycle } from "./kernel/interaction-lifecycle";
21
21
  import {
22
22
  SubmissionLifecycle,
23
+ MAX_PENDING_SUBMISSIONS,
23
24
  SubmissionQueueFullError,
24
25
  type SubmissionInput,
25
26
  type SubmissionStore,
@@ -88,10 +89,21 @@ import { serializeOutput } from "./lib/artifacts";
88
89
  import type {
89
90
  RuntimeModelUsageEvent,
90
91
  RuntimeToolSettlementEvent,
92
+ RuntimeLifecycleFact,
93
+ RuntimeSubagentUsageEvent,
91
94
  } from "./kernel/bindings";
95
+ import { RuntimeSubagentLifecycleRecorder } from "./kernel/durable-lifecycle";
96
+ import {
97
+ decodeSubagentCompletion,
98
+ isZeroSubagentUsage,
99
+ runtimeSubagentRunMetadata,
100
+ subagentTerminalStatus,
101
+ type RuntimeSubagentRunMetadata,
102
+ } from "./kernel/subagent-runtime";
92
103
  import {
93
104
  isTerminalSubmissionStatus,
94
105
  RuntimeDatabase,
106
+ type StoredSubmissionAdmission,
95
107
  type SubmissionStatus,
96
108
  } from "./db/index";
97
109
  import {
@@ -114,6 +126,11 @@ import {
114
126
 
115
127
  const SCHEDULED_STABLE_TIMEOUT_MS = 30_000;
116
128
  const TURN_EVENT_RETRY_SECONDS = 10;
129
+ // 一次 Host admission 调用最多被视为「在途」多久。停在 pending 的准入行永久保留
130
+ // —— 它固定了同一 identity 重投时必须复用的 runId,不能靠删行来让容量回收 ——
131
+ // 但一次远端失败或崩溃留下的行超过这个窗口后就不再占用 pending 容量,否则
132
+ // Host 侧的一次抖动会把 Session 永久卡在「队列已满」。
133
+ const ADMISSION_ATTEMPT_WINDOW_MS = 60_000;
117
134
  export const CHAT_RECOVERY_MAX_ATTEMPTS = 5;
118
135
  // stall 单独收窄。理由不是「stall 更不值得救」,而是它的重试**期望值和别的错不一样**:
119
136
  // 瞬时错(5xx / 断流)重跑一次往往就好了;stall 的重跑是拿同一份 transcript 让模型
@@ -129,7 +146,9 @@ type RuntimeEventOutboxPayload =
129
146
  | {
130
147
  readonly type: "tool-settlement";
131
148
  readonly event: RuntimeToolSettlementEvent;
132
- };
149
+ }
150
+ | { readonly type: "subagent-usage"; readonly event: RuntimeSubagentUsageEvent }
151
+ | { readonly type: "lifecycle"; readonly event: RuntimeLifecycleFact };
133
152
  interface StoredSubmission extends SubmissionReceipt {
134
153
  status: SubmissionStatus;
135
154
  requestId: string;
@@ -144,6 +163,10 @@ interface StoredSubmission extends SubmissionReceipt {
144
163
  regenerateMessageId?: string | null;
145
164
  recoveryErrorCount: number;
146
165
  recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
166
+ runId: string | null;
167
+ accountId: string | null;
168
+ rateVersion: number | null;
169
+ slotIdentity: string | null;
147
170
  }
148
171
 
149
172
  interface SubmitMessageOptions {
@@ -164,6 +187,11 @@ interface ActiveTurn {
164
187
  agent: PreparedPiTurnAdapter;
165
188
  }
166
189
 
190
+ interface PlannedContinuationData {
191
+ readonly submissionId: string;
192
+ readonly requestId: string;
193
+ }
194
+
167
195
  // 作用:把任意异常整理成可以持久化或发给客户端的文字。
168
196
  // 调用:Turn 执行、恢复和业务投影捕获 `unknown` 异常时调用。
169
197
  // 原因:错误边界不能假定抛出值一定是 `Error`。
@@ -178,6 +206,32 @@ function json(value: unknown): string {
178
206
  return JSON.stringify(value);
179
207
  }
180
208
 
209
+ function validateAdmissionResult(
210
+ result: import("./kernel/bindings").RuntimeSubmissionAdmissionResult,
211
+ ): import("./kernel/bindings").RuntimeSubmissionAdmissionResult {
212
+ if (!result || typeof result !== "object" || typeof result.accepted !== "boolean") {
213
+ throw new Error("Runtime admission result is invalid");
214
+ }
215
+ if (!result.accepted) {
216
+ if (
217
+ typeof result.code !== "string" || !result.code.trim() ||
218
+ (result.retryable !== undefined && typeof result.retryable !== "boolean")
219
+ ) {
220
+ throw new Error("Runtime admission rejection is invalid");
221
+ }
222
+ return result;
223
+ }
224
+ if (
225
+ typeof result.accountId !== "string" || !result.accountId.trim() ||
226
+ typeof result.slotIdentity !== "string" || !result.slotIdentity.trim() ||
227
+ !Number.isSafeInteger(result.rateVersion) ||
228
+ result.rateVersion <= 0
229
+ ) {
230
+ throw new Error("Runtime admission context is invalid");
231
+ }
232
+ return result;
233
+ }
234
+
181
235
  async function closeRuntimeGatewaySession(
182
236
  session: RuntimeGatewaySession | undefined,
183
237
  step: string,
@@ -192,10 +246,17 @@ function assistantUsageEvent(
192
246
  eventId: string,
193
247
  submissionId: string,
194
248
  message: AssistantMessage,
249
+ pinned?: Pick<StoredSubmission, "accountId" | "rateVersion" | "slotIdentity">,
195
250
  ): RuntimeModelUsageEvent {
196
251
  return {
197
252
  eventId,
198
253
  submissionId,
254
+ runId: submissionId,
255
+ ...(pinned?.accountId ? { accountId: pinned.accountId } : {}),
256
+ ...(pinned?.rateVersion !== null && pinned?.rateVersion !== undefined
257
+ ? { rateVersion: pinned.rateVersion }
258
+ : {}),
259
+ ...(pinned?.slotIdentity ? { slotIdentity: pinned.slotIdentity } : {}),
199
260
  kind: "assistant",
200
261
  api: message.api,
201
262
  provider: message.provider,
@@ -327,6 +388,10 @@ export abstract class AgentRuntimeKernel<
327
388
  private migratedSubmissionIds?: Set<string>;
328
389
  private db!: RuntimeDatabase;
329
390
  private readonly temporaryAgents = new TemporaryAgentCoordinator();
391
+ private readonly admissionsInFlight = new Map<
392
+ string,
393
+ Promise<import("./kernel/bindings").RuntimeSubmissionAdmissionResult>
394
+ >();
330
395
  private readonly temporaryAgentApprovals =
331
396
  new Map<string, PendingTemporaryAgentApproval>();
332
397
 
@@ -493,7 +558,7 @@ export abstract class AgentRuntimeKernel<
493
558
  status: "failed" as const,
494
559
  error:
495
560
  receipt.error ??
496
- `Pi recovery ended with status ${receipt.status}`,
561
+ `SpringBrand recovery ended with status ${receipt.status}`,
497
562
  };
498
563
  } catch (error) {
499
564
  if (
@@ -591,7 +656,7 @@ export abstract class AgentRuntimeKernel<
591
656
  if (contested && !parked) {
592
657
  await closeRuntimeGatewaySession(gatewaySession, "gateway.close.contested");
593
658
  throw new Error(
594
- "Cannot reload Runtime while a revision-pinned Pi Turn is active",
659
+ "Cannot reload Runtime while a revision-pinned SpringBrand Turn is active",
595
660
  );
596
661
  }
597
662
  const repin = parked
@@ -663,6 +728,37 @@ export abstract class AgentRuntimeKernel<
663
728
  } = {},
664
729
  ): PreparedPiTurnAdapter {
665
730
  const startedAt = output.startedAt ?? Date.now();
731
+ const snapshot = this.assembly();
732
+ const runMetadata: RuntimeSubagentRunMetadata = {
733
+ submissionId: submission.submissionId,
734
+ parentRunId: submission.runId ?? submission.submissionId,
735
+ ...(submission.accountId ? { accountId: submission.accountId } : {}),
736
+ ...(submission.rateVersion !== null
737
+ ? { rateVersion: submission.rateVersion }
738
+ : {}),
739
+ ...(submission.slotIdentity
740
+ ? { slotIdentity: submission.slotIdentity }
741
+ : {}),
742
+ };
743
+ const lifecycle = this.subagentLifecycle(runMetadata);
744
+ const toolExecutors = snapshot.bindings.subagents
745
+ ? this.pi.subagentToolExecutors(
746
+ snapshot.bindings.subagents,
747
+ snapshot.profile.enabledSubagents,
748
+ {
749
+ ...runMetadata,
750
+ onRegistered: async (runId) => {
751
+ await lifecycle.register(runId);
752
+ },
753
+ onUsage: async (event) => {
754
+ await lifecycle.usage(event);
755
+ },
756
+ onTerminal: async (terminal) => {
757
+ await lifecycle.terminal(terminal);
758
+ },
759
+ },
760
+ )
761
+ : undefined;
666
762
  return this.pi.createTurn({
667
763
  prepared: this.preparedPi(),
668
764
  pinnedDescriptor: submission.assemblyDescriptor,
@@ -707,6 +803,9 @@ export abstract class AgentRuntimeKernel<
707
803
  settleTool: (call) =>
708
804
  this.settleTool(submission.submissionId, call),
709
805
  },
806
+ ...(toolExecutors && Object.keys(toolExecutors).length > 0
807
+ ? { toolExecutors }
808
+ : {}),
710
809
  onToolTelemetry: (event) => {
711
810
  this._emit("ua:tool" as never, { ...event });
712
811
  },
@@ -732,6 +831,7 @@ export abstract class AgentRuntimeKernel<
732
831
  persisted.id,
733
832
  submission.submissionId,
734
833
  commit.message,
834
+ submission,
735
835
  );
736
836
  this.db.runtimeEvents.insert({
737
837
  eventId: event.eventId,
@@ -774,6 +874,29 @@ export abstract class AgentRuntimeKernel<
774
874
  });
775
875
  }
776
876
 
877
+ private subagentLifecycle(
878
+ input: RuntimeSubagentRunMetadata,
879
+ ): RuntimeSubagentLifecycleRecorder {
880
+ return new RuntimeSubagentLifecycleRecorder(input, {
881
+ save: async (event) => {
882
+ this.db.transaction(() => {
883
+ this.db.runtimeEvents.insert({
884
+ eventId: event.eventId,
885
+ body: json({
886
+ type: event.kind === "subagent" ? "subagent-usage" : "lifecycle",
887
+ event,
888
+ }),
889
+ createdAt: Date.now(),
890
+ });
891
+ });
892
+ await this.drainRuntimeEvents();
893
+ },
894
+ has: (eventId) => this.db.runtimeEvents.has(eventId),
895
+ hasSubagentUsage: (submissionId, subagentRunId) =>
896
+ this.db.runtimeEvents.hasSubagentUsage(submissionId, subagentRunId),
897
+ });
898
+ }
899
+
777
900
  /**
778
901
  * 在 Agent 实例启动或从休眠唤醒后恢复 Session 本地后台状态。
779
902
  *
@@ -822,9 +945,23 @@ export abstract class AgentRuntimeKernel<
822
945
  if (payload.type === "model-usage") {
823
946
  if (!turnEvents.onModelUsage) continue;
824
947
  await turnEvents.onModelUsage(payload.event);
825
- } else {
948
+ } else if (payload.type === "tool-settlement") {
826
949
  if (!turnEvents.onToolSettled) continue;
827
950
  await turnEvents.onToolSettled(payload.event);
951
+ } else if (payload.type === "subagent-usage") {
952
+ if (!turnEvents.onSubagentUsage) continue;
953
+ const confirmation = await turnEvents.onSubagentUsage(payload.event);
954
+ if (confirmation?.confirmed !== true) {
955
+ failed = true;
956
+ break;
957
+ }
958
+ } else {
959
+ if (!turnEvents.onLifecycleFact) continue;
960
+ const confirmation = await turnEvents.onLifecycleFact(payload.event);
961
+ if (confirmation?.confirmed !== true) {
962
+ failed = true;
963
+ break;
964
+ }
828
965
  }
829
966
  this.db.transaction(() =>
830
967
  this.db.runtimeEvents.markDelivered(row.eventId, Date.now())
@@ -835,6 +972,7 @@ export abstract class AgentRuntimeKernel<
835
972
  "[runtime-turn-event:degraded]",
836
973
  json({ eventId: row.eventId, error: errorText(error) }),
837
974
  );
975
+ break;
838
976
  }
839
977
  }
840
978
  if (!failed) return;
@@ -1053,7 +1191,7 @@ export abstract class AgentRuntimeKernel<
1053
1191
  private preparedPi(): PreparedPiRuntime {
1054
1192
  if (!this.runtimePi) {
1055
1193
  throw new Error(
1056
- "Pi Runtime must be prepared before starting a Turn",
1194
+ "SpringBrand Runtime must be prepared before starting a Turn",
1057
1195
  );
1058
1196
  }
1059
1197
  return this.runtimePi;
@@ -1255,7 +1393,23 @@ export abstract class AgentRuntimeKernel<
1255
1393
  onCompactionPersisted: (event) =>
1256
1394
  this.db.runtimeEvents.insert({
1257
1395
  eventId: event.eventId,
1258
- body: json({ type: "model-usage", event }),
1396
+ body: json({
1397
+ type: "model-usage",
1398
+ event: {
1399
+ ...event,
1400
+ runId: submissionId,
1401
+ ...(this.readSubmission(submissionId)?.accountId
1402
+ ? { accountId: this.readSubmission(submissionId)!.accountId! }
1403
+ : {}),
1404
+ ...(this.readSubmission(submissionId)?.rateVersion !== null &&
1405
+ this.readSubmission(submissionId)?.rateVersion !== undefined
1406
+ ? { rateVersion: this.readSubmission(submissionId)!.rateVersion! }
1407
+ : {}),
1408
+ ...(this.readSubmission(submissionId)?.slotIdentity
1409
+ ? { slotIdentity: this.readSubmission(submissionId)!.slotIdentity! }
1410
+ : {}),
1411
+ },
1412
+ }),
1259
1413
  createdAt: Date.now(),
1260
1414
  }),
1261
1415
  });
@@ -1303,31 +1457,147 @@ export abstract class AgentRuntimeKernel<
1303
1457
  ? { idempotencyKey: options.idempotencyKey }
1304
1458
  : {}),
1305
1459
  prepareAdmission: async () => {
1460
+ await this.ensureRuntimeReady();
1306
1461
  const regenerateEntry =
1307
1462
  options.regenerate && options.userMessageId
1308
1463
  ? await this.transcript.findUserMessage(options.userMessageId)
1309
1464
  : undefined;
1465
+ if (
1466
+ options.regenerate &&
1467
+ (!regenerateEntry ||
1468
+ userContentKey(regenerateEntry) !== userContentKey(userMessage))
1469
+ ) {
1470
+ throw new Error(
1471
+ "Regenerate request must match an existing user message",
1472
+ );
1473
+ }
1474
+ const hook = this.runtimeHooks?.onSubmissionAdmission;
1475
+ let existing: StoredSubmissionAdmission | null = null;
1476
+ if (hook) {
1477
+ const attemptedAt = Date.now();
1478
+ const reservedRunId = crypto.randomUUID();
1479
+ this.db.transaction(() => {
1480
+ const current = this.db.submissionAdmissions.find(
1481
+ options.requestId,
1482
+ options.idempotencyKey,
1483
+ );
1484
+ if (current) {
1485
+ // 同一 identity 重投:沿用已固定的 runId,只把这次尝试的时间推到现在,
1486
+ // 使它重新占住一个在途名额。
1487
+ if (current.status === "pending") {
1488
+ this.db.submissionAdmissions.recordAttempt(
1489
+ current.requestId,
1490
+ attemptedAt,
1491
+ );
1492
+ }
1493
+ return;
1494
+ }
1495
+ if (
1496
+ this.db.submissions.countPending() +
1497
+ this.db.submissionAdmissions.countPending(
1498
+ attemptedAt - ADMISSION_ATTEMPT_WINDOW_MS,
1499
+ ) >=
1500
+ MAX_PENDING_SUBMISSIONS
1501
+ ) {
1502
+ throw new SubmissionQueueFullError();
1503
+ }
1504
+ this.db.submissionAdmissions.begin(
1505
+ options.requestId,
1506
+ options.idempotencyKey ?? null,
1507
+ reservedRunId,
1508
+ attemptedAt,
1509
+ );
1510
+ });
1511
+ existing = this.db.submissionAdmissions.find(
1512
+ options.requestId,
1513
+ options.idempotencyKey,
1514
+ );
1515
+ }
1516
+ const runId = existing?.runId ?? crypto.randomUUID();
1517
+ const admissionRequestId = existing?.requestId ?? options.requestId;
1518
+ const admissionIdempotencyKey = existing
1519
+ ? existing.idempotencyKey ?? undefined
1520
+ : options.idempotencyKey;
1521
+ const admissionKey = admissionIdempotencyKey
1522
+ ? `idempotency:${admissionIdempotencyKey}`
1523
+ : `request:${admissionRequestId}`;
1524
+ const pendingAdmission = hook && existing?.status === "pending"
1525
+ ? this.admissionsInFlight.get(admissionKey)
1526
+ : undefined;
1527
+ const admissionPromise = pendingAdmission ?? (hook && (!existing || existing.status === "pending")
1528
+ ? hook({
1529
+ requestId: admissionRequestId,
1530
+ ...(admissionIdempotencyKey
1531
+ ? { idempotencyKey: admissionIdempotencyKey }
1532
+ : {}),
1533
+ runId,
1534
+ }).then(validateAdmissionResult).catch((error) => {
1535
+ this.admissionsInFlight.delete(admissionKey);
1536
+ throw error;
1537
+ })
1538
+ : undefined);
1539
+ if (hook && admissionPromise && !pendingAdmission) {
1540
+ this.admissionsInFlight.set(
1541
+ admissionKey,
1542
+ admissionPromise,
1543
+ );
1544
+ }
1545
+ let admission = existing?.status === "accepted"
1546
+ ? {
1547
+ accepted: true as const,
1548
+ accountId: existing.accountId!,
1549
+ rateVersion: existing.rateVersion!,
1550
+ slotIdentity: existing.slotIdentity!,
1551
+ }
1552
+ : existing?.status === "rejected"
1553
+ ? {
1554
+ accepted: false as const,
1555
+ code: existing.code ?? "admission_rejected",
1556
+ retryable: existing.retryable ?? false,
1557
+ }
1558
+ : admissionPromise
1559
+ ? await admissionPromise
1560
+ : undefined;
1310
1561
  return () => {
1311
- const submissionId = crypto.randomUUID();
1562
+ if (admission && !admission.accepted) {
1563
+ this.db.submissionAdmissions.saveRejected(
1564
+ admissionRequestId,
1565
+ runId,
1566
+ admission.code,
1567
+ admission.retryable ?? false,
1568
+ );
1569
+ const rejected = this.insertSkippedSubmission({
1570
+ submissionId: runId,
1571
+ requestId: admissionRequestId,
1572
+ idempotencyKey: admissionIdempotencyKey ?? null,
1573
+ createdAt: userMessage.timestamp,
1574
+ code: admission.code,
1575
+ retryable: admission.retryable ?? false,
1576
+ });
1577
+ this.admissionsInFlight.delete(admissionKey);
1578
+ return {
1579
+ ...rejected,
1580
+ admissionRejected: true,
1581
+ };
1582
+ }
1583
+ const submissionId = runId;
1312
1584
  const assistantMessageId = crypto.randomUUID();
1313
1585
  const createdAt = userMessage.timestamp;
1314
1586
  const userMessageId =
1315
1587
  options.userMessageId ?? crypto.randomUUID();
1316
- if (options.regenerate) {
1317
- if (
1318
- !regenerateEntry ||
1319
- userContentKey(regenerateEntry) !==
1320
- userContentKey(userMessage)
1321
- ) {
1322
- throw new Error(
1323
- "Regenerate request must match an existing user message",
1324
- );
1325
- }
1588
+ if (admission?.accepted) {
1589
+ this.db.submissionAdmissions.saveAccepted(
1590
+ admissionRequestId,
1591
+ runId,
1592
+ admission.accountId,
1593
+ admission.rateVersion,
1594
+ admission.slotIdentity,
1595
+ );
1326
1596
  }
1327
1597
  this.db.submissions.insert({
1328
1598
  submissionId,
1329
- requestId: options.requestId,
1330
- idempotencyKey: options.idempotencyKey ?? null,
1599
+ requestId: admissionRequestId,
1600
+ idempotencyKey: admissionIdempotencyKey ?? null,
1331
1601
  createdAt,
1332
1602
  assemblyRevision: "",
1333
1603
  assemblyDescriptor: "",
@@ -1340,7 +1610,28 @@ export abstract class AgentRuntimeKernel<
1340
1610
  regenerateMessageId: options.regenerate
1341
1611
  ? options.userMessageId ?? null
1342
1612
  : null,
1613
+ runId,
1614
+ accountId: admission?.accepted ? admission.accountId : null,
1615
+ rateVersion: admission?.accepted ? admission.rateVersion : null,
1616
+ slotIdentity: admission?.accepted ? admission.slotIdentity : null,
1343
1617
  });
1618
+ if (admission?.accepted) {
1619
+ const fact: RuntimeLifecycleFact = {
1620
+ kind: "submission-admitted",
1621
+ eventId: `${submissionId}:admitted`,
1622
+ submissionId,
1623
+ runId: submissionId,
1624
+ accountId: admission.accountId,
1625
+ rateVersion: admission.rateVersion,
1626
+ slotIdentity: admission.slotIdentity,
1627
+ };
1628
+ this.db.runtimeEvents.insert({
1629
+ eventId: fact.eventId,
1630
+ body: json({ type: "lifecycle", event: fact }),
1631
+ createdAt,
1632
+ });
1633
+ }
1634
+ if (hook) this.admissionsInFlight.delete(admissionKey);
1344
1635
  return this.readSubmission(submissionId)!;
1345
1636
  };
1346
1637
  },
@@ -1364,6 +1655,7 @@ export abstract class AgentRuntimeKernel<
1364
1655
  const submitted = await this.submissions.submit(
1365
1656
  this.submissionInput(userMessage, options),
1366
1657
  );
1658
+ await this.drainRuntimeEvents();
1367
1659
  await this.broadcastApprovals();
1368
1660
  return submitted;
1369
1661
  }
@@ -1380,7 +1672,10 @@ export abstract class AgentRuntimeKernel<
1380
1672
  this.submissionInput(userMessage, options),
1381
1673
  timeoutMs,
1382
1674
  );
1383
- if (submitted) await this.broadcastApprovals();
1675
+ if (submitted) {
1676
+ await this.drainRuntimeEvents();
1677
+ await this.broadcastApprovals();
1678
+ }
1384
1679
  return submitted;
1385
1680
  }
1386
1681
 
@@ -1405,11 +1700,51 @@ export abstract class AgentRuntimeKernel<
1405
1700
  );
1406
1701
  const submission = this.readSubmission(submissionId);
1407
1702
  if (!submission) {
1408
- throw new Error(`Unknown Pi submission: ${submissionId}`);
1703
+ throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
1409
1704
  }
1410
1705
  return submission;
1411
1706
  }
1412
1707
 
1708
+ private insertSubmissionTerminalFact(submission: StoredSubmission): void {
1709
+ const terminalFact: RuntimeLifecycleFact = {
1710
+ kind: "submission-terminal",
1711
+ eventId: `${submission.submissionId}:terminal`,
1712
+ submissionId: submission.submissionId,
1713
+ runId: submission.runId ?? submission.submissionId,
1714
+ status: submission.status as "completed" | "aborted" | "skipped" | "error",
1715
+ childRegistrationClosed: true,
1716
+ zeroUsage: !this.db.runtimeEvents.hasNonZeroModelUsage(
1717
+ submission.submissionId,
1718
+ ),
1719
+ ...(submission.accountId ? { accountId: submission.accountId } : {}),
1720
+ ...(submission.rateVersion !== null
1721
+ ? { rateVersion: submission.rateVersion }
1722
+ : {}),
1723
+ ...(submission.slotIdentity
1724
+ ? { slotIdentity: submission.slotIdentity }
1725
+ : {}),
1726
+ };
1727
+ this.db.runtimeEvents.insert({
1728
+ eventId: terminalFact.eventId,
1729
+ body: json({ type: "lifecycle", event: terminalFact }),
1730
+ createdAt: submission.completedAt ?? Date.now(),
1731
+ });
1732
+ }
1733
+
1734
+ private insertSkippedSubmission(input: {
1735
+ submissionId: string;
1736
+ requestId: string;
1737
+ idempotencyKey: string | null;
1738
+ createdAt: number;
1739
+ code: string;
1740
+ retryable: boolean;
1741
+ }): StoredSubmission {
1742
+ this.db.submissions.insertRejected(input);
1743
+ const skipped = this.readSubmission(input.submissionId)!;
1744
+ this.insertSubmissionTerminalFact(skipped);
1745
+ return skipped;
1746
+ }
1747
+
1413
1748
  // 作用:读取某个 Submission 已持久的全部 Pi 恢复里程碑。
1414
1749
  // 调用:恢复决策器每次检查或应用命令前调用。
1415
1750
  // 原因:决策只依赖持久事实,重启后才会得到同样结果。
@@ -1560,7 +1895,7 @@ export abstract class AgentRuntimeKernel<
1560
1895
  ): void {
1561
1896
  const submission = this.readSubmission(submissionId);
1562
1897
  if (!submission) {
1563
- throw new Error(`Unknown Pi submission: ${submissionId}`);
1898
+ throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
1564
1899
  }
1565
1900
  const existing = this.db.settlements.find(submissionId, call.toolCallId);
1566
1901
  const args = json(call.args);
@@ -1708,11 +2043,28 @@ export abstract class AgentRuntimeKernel<
1708
2043
  const latest = this.readSubmission(submission.submissionId);
1709
2044
  if (!latest) {
1710
2045
  throw new Error(
1711
- `Unknown Pi submission: ${submission.submissionId}`,
2046
+ `Unknown SpringBrand submission: ${submission.submissionId}`,
1712
2047
  );
1713
2048
  }
1714
2049
  if (isTerminalSubmissionStatus(latest.status)) return latest;
1715
2050
 
2051
+ for (const steer of this.db.steers.listForSubmission(latest.submissionId)) {
2052
+ const userMessage = JSON.parse(steer.canonicalJson) as PiCanonicalUserInput;
2053
+ const uiMessage = steer.uiMessageJson
2054
+ ? JSON.parse(steer.uiMessageJson) as UIMessage & { role: "user" }
2055
+ : undefined;
2056
+ const submitted = await this.submitMessage(userMessage, {
2057
+ requestId: steer.messageId,
2058
+ idempotencyKey: steer.messageId,
2059
+ userMessageId: steer.messageId,
2060
+ ...(uiMessage ? { userMessage: uiMessage } : {}),
2061
+ });
2062
+ this.ctx.waitUntil(submitted.completion);
2063
+ this.db.transaction(() =>
2064
+ this.db.steers.deleteByMessageId(steer.messageId)
2065
+ );
2066
+ }
2067
+
1716
2068
  const durableIntent = this.decidePiRecovery(latest).terminal;
1717
2069
  const effectiveOutcome = latest.abortReason
1718
2070
  ? "aborted"
@@ -1742,38 +2094,19 @@ export abstract class AgentRuntimeKernel<
1742
2094
  : effectiveOutcome === "aborted"
1743
2095
  ? "aborted"
1744
2096
  : "error";
1745
- for (const steer of this.db.steers.listForSubmission(
1746
- latest.submissionId,
1747
- )) {
1748
- if (!this.db.submissions.findByRequestId(steer.messageId)) {
1749
- this.db.submissions.insert({
1750
- submissionId: steer.steerId,
1751
- requestId: steer.messageId,
1752
- idempotencyKey: steer.messageId,
1753
- createdAt: steer.createdAt,
1754
- assemblyRevision: latest.assemblyRevision,
1755
- assemblyDescriptor: latest.assemblyDescriptor,
1756
- assistantMessageId: crypto.randomUUID(),
1757
- queuedInputJson: steer.canonicalJson,
1758
- queuedUiMessageJson: steer.uiMessageJson,
1759
- userMessageId: steer.messageId,
1760
- regenerateMessageId: null,
1761
- });
1762
- }
1763
- this.db.steers.deleteByMessageId(steer.messageId);
1764
- }
1765
2097
  const terminal = this.updateSubmission(
1766
2098
  latest.submissionId,
1767
2099
  status,
1768
2100
  effectiveMessage,
1769
2101
  );
2102
+ this.insertSubmissionTerminalFact(terminal);
1770
2103
  if (
1771
2104
  effectiveOutcome === "aborted" &&
1772
2105
  effectiveMessage === USER_STOP_REASON
1773
2106
  ) {
1774
2107
  if (terminal.completedAt == null) {
1775
2108
  throw new Error(
1776
- `Aborted Pi submission has no completion time: ${latest.submissionId}`,
2109
+ `Aborted SpringBrand submission has no completion time: ${latest.submissionId}`,
1777
2110
  );
1778
2111
  }
1779
2112
  this.transcript.appendTurnAbortedMarker(
@@ -1896,7 +2229,7 @@ export abstract class AgentRuntimeKernel<
1896
2229
  }
1897
2230
  }
1898
2231
  }
1899
- throw new Error("Pi recovery plan did not converge");
2232
+ throw new Error("SpringBrand recovery plan did not converge");
1900
2233
  }
1901
2234
 
1902
2235
  // #endregion
@@ -1925,7 +2258,7 @@ export abstract class AgentRuntimeKernel<
1925
2258
  typeof userMessage.timestamp !== "number" ||
1926
2259
  !submission.userMessageId
1927
2260
  ) {
1928
- throw new Error("Queued Pi user message is invalid");
2261
+ throw new Error("Queued SpringBrand user message is invalid");
1929
2262
  }
1930
2263
  const uiMessage = submission.queuedUiMessageJson
1931
2264
  ? JSON.parse(submission.queuedUiMessageJson) as UIMessage & {
@@ -1954,7 +2287,7 @@ export abstract class AgentRuntimeKernel<
1954
2287
  if (!latest || latest.status !== "pending") {
1955
2288
  if (!latest) {
1956
2289
  throw new Error(
1957
- `Unknown Pi submission: ${submission.submissionId}`,
2290
+ `Unknown SpringBrand submission: ${submission.submissionId}`,
1958
2291
  );
1959
2292
  }
1960
2293
  return latest;
@@ -1962,7 +2295,7 @@ export abstract class AgentRuntimeKernel<
1962
2295
  const running = this.db.submissions.findRunning();
1963
2296
  if (running) {
1964
2297
  throw new Error(
1965
- `Pi submission ${running.submissionId} is already running`,
2298
+ `SpringBrand submission ${running.submissionId} is already running`,
1966
2299
  );
1967
2300
  }
1968
2301
  if (submission.regenerateMessageId) {
@@ -1979,7 +2312,7 @@ export abstract class AgentRuntimeKernel<
1979
2312
  );
1980
2313
  if (!appended) {
1981
2314
  throw new Error(
1982
- `Pi Session message ${submission.userMessageId} already exists`,
2315
+ `SpringBrand Session message ${submission.userMessageId} already exists`,
1983
2316
  );
1984
2317
  }
1985
2318
  }
@@ -1989,7 +2322,7 @@ export abstract class AgentRuntimeKernel<
1989
2322
  ["pending"],
1990
2323
  )) {
1991
2324
  throw new Error(
1992
- `Pi submission ${submission.submissionId} could not activate`,
2325
+ `SpringBrand submission ${submission.submissionId} could not activate`,
1993
2326
  );
1994
2327
  }
1995
2328
  this.db.submissions.clearQueuedPayload(
@@ -2014,7 +2347,7 @@ export abstract class AgentRuntimeKernel<
2014
2347
  ): Promise<StoredSubmission> {
2015
2348
  const submission = this.readSubmission(submissionId);
2016
2349
  if (!submission) {
2017
- throw new Error(`Unknown Pi submission: ${submissionId}`);
2350
+ throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
2018
2351
  }
2019
2352
  if (isTerminalSubmissionStatus(submission.status)) {
2020
2353
  return submission;
@@ -2057,7 +2390,7 @@ export abstract class AgentRuntimeKernel<
2057
2390
  this.sendChatTerminal(
2058
2391
  submission.requestId,
2059
2392
  failed.status === "error"
2060
- ? failed.error ?? "Pi turn failed"
2393
+ ? failed.error ?? "SpringBrand turn failed"
2061
2394
  : undefined,
2062
2395
  recovery,
2063
2396
  );
@@ -2127,7 +2460,7 @@ export abstract class AgentRuntimeKernel<
2127
2460
 
2128
2461
  const ready = this.readSubmission(submissionId);
2129
2462
  if (!ready) {
2130
- throw new Error(`Unknown Pi submission: ${submissionId}`);
2463
+ throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
2131
2464
  }
2132
2465
  if (isTerminalSubmissionStatus(ready.status)) return ready;
2133
2466
  if (ready.abortReason) {
@@ -2189,6 +2522,7 @@ export abstract class AgentRuntimeKernel<
2189
2522
  agent: adapter,
2190
2523
  };
2191
2524
  let streamId: string | undefined;
2525
+ let runResult: import("./pi/runtime-adapter").PiTurnRunResult | undefined;
2192
2526
 
2193
2527
  const deactivate = this.submissions.activate(submission, turn);
2194
2528
  await this.broadcastApprovals();
@@ -2208,7 +2542,7 @@ export abstract class AgentRuntimeKernel<
2208
2542
  streamId = activeStreamId;
2209
2543
  this.streamBySubmission.set(submissionId, streamId);
2210
2544
  try {
2211
- await adapter.run({});
2545
+ runResult = await adapter.run({});
2212
2546
  } finally {
2213
2547
  this.streamBySubmission.delete(submissionId);
2214
2548
  }
@@ -2220,10 +2554,27 @@ export abstract class AgentRuntimeKernel<
2220
2554
  if (streamId) this.failRecoverableStream(streamId);
2221
2555
  return this.readSubmission(submissionId)!;
2222
2556
  }
2557
+ if (runResult?.kind === "yielded") {
2558
+ const latest = this.readSubmission(submissionId)!;
2559
+ if (latest.abortReason) {
2560
+ return this.submissions.finish(
2561
+ latest,
2562
+ "aborted",
2563
+ latest.abortReason,
2564
+ );
2565
+ }
2566
+ await this.schedule(
2567
+ 0,
2568
+ "_piPlannedContinuation",
2569
+ { submissionId, requestId: submission.requestId },
2570
+ { idempotent: true },
2571
+ );
2572
+ return latest;
2573
+ }
2223
2574
  const intent = terminalIntent ?? {
2224
2575
  outcome: "failed" as const,
2225
2576
  message:
2226
- "Pi turn ended without an authoritative assistant message",
2577
+ "SpringBrand turn ended without an authoritative assistant message",
2227
2578
  };
2228
2579
  const terminal = await this.submissions.finish(
2229
2580
  submission,
@@ -2385,7 +2736,7 @@ export abstract class AgentRuntimeKernel<
2385
2736
  this.sendChatTerminal(
2386
2737
  turn.requestId,
2387
2738
  submission.status === "error"
2388
- ? submission.error ?? "Pi turn failed"
2739
+ ? submission.error ?? "SpringBrand turn failed"
2389
2740
  : undefined,
2390
2741
  turn.continuation,
2391
2742
  );
@@ -3048,7 +3399,9 @@ export abstract class AgentRuntimeKernel<
3048
3399
  const reason = "Moved to active Turn";
3049
3400
  this.appendTerminalIntent(latest, "aborted", reason);
3050
3401
  this.db.submissions.updateAbortReason(submissionId, reason);
3051
- this.updateSubmission(submissionId, "aborted", reason);
3402
+ this.insertSubmissionTerminalFact(
3403
+ this.updateSubmission(submissionId, "aborted", reason),
3404
+ );
3052
3405
  return true;
3053
3406
  });
3054
3407
  if (!moved) {
@@ -3064,6 +3417,7 @@ export abstract class AgentRuntimeKernel<
3064
3417
  active.agent.steer(message, queued.userMessageId);
3065
3418
  }
3066
3419
  } finally {
3420
+ await this.drainRuntimeEvents();
3067
3421
  await this.broadcastApprovals();
3068
3422
  }
3069
3423
  return {
@@ -3101,13 +3455,22 @@ export abstract class AgentRuntimeKernel<
3101
3455
  SCHEDULED_STABLE_TIMEOUT_MS,
3102
3456
  );
3103
3457
  if (!submitted) {
3104
- return {
3105
- submissionId: crypto.randomUUID(),
3106
- status: "skipped",
3107
- accepted: false,
3108
- error: "Runtime not stable within timeout",
3109
- createdAt: Date.now(),
3110
- };
3458
+ const skipped = this.db.transaction(() => {
3459
+ const duplicate = options?.idempotencyKey
3460
+ ? this.db.submissions.findByIdempotencyKey(options.idempotencyKey)
3461
+ : this.db.submissions.findByRequestId(requestId);
3462
+ return duplicate ?? this.insertSkippedSubmission({
3463
+ submissionId: requestId,
3464
+ requestId,
3465
+ idempotencyKey: options?.idempotencyKey ?? null,
3466
+ createdAt: Date.now(),
3467
+ code: "Runtime not stable within timeout",
3468
+ retryable: true,
3469
+ });
3470
+ }) as StoredSubmission;
3471
+ await this.drainRuntimeEvents();
3472
+ await this.broadcastApprovals();
3473
+ return skipped;
3111
3474
  }
3112
3475
  this.ctx.waitUntil(submitted.completion.then(() => undefined));
3113
3476
  return submitted.receipt;
@@ -3222,9 +3585,9 @@ export abstract class AgentRuntimeKernel<
3222
3585
  const adapter = this.createSubmissionExecutionAdapter(submission);
3223
3586
  const spec = adapter.interactionSpec(pending.toolName);
3224
3587
  if (!spec) return { ok: false };
3225
- if (!spec.validateResponse(response)) return { ok: false };
3226
3588
 
3227
3589
  const input = safeParseJson(pending.inputJson);
3590
+ if (!spec.validateResponse(input, response)) return { ok: false };
3228
3591
  const settled = spec.settle
3229
3592
  ? spec.settle(input, response)
3230
3593
  : defaultInteractionResult(response);
@@ -3347,11 +3710,30 @@ export abstract class AgentRuntimeKernel<
3347
3710
  }
3348
3711
  }
3349
3712
 
3713
+ async _piPlannedContinuation(
3714
+ data?: PlannedContinuationData,
3715
+ ): Promise<void> {
3716
+ if (!data?.submissionId || !data.requestId) return;
3717
+ const submission = this.readSubmission(data.submissionId);
3718
+ if (
3719
+ !submission ||
3720
+ submission.requestId !== data.requestId ||
3721
+ isTerminalSubmissionStatus(submission.status)
3722
+ ) {
3723
+ return;
3724
+ }
3725
+ await this.submissions.recoverAfterCurrent(submission.submissionId);
3726
+ }
3727
+
3350
3728
  // 作用:Agent Tool 子运行开始后重算一次活动投影。
3351
3729
  // 调用:Agents SDK 在登记子运行后调用。
3352
3730
  // 原因:分离的子运行不进 Submission 表,不在这里重算,Host 的列表会显示成已经空闲。
3353
3731
  override async onAgentToolStart(run: AgentToolRunInfo): Promise<void> {
3354
3732
  await super.onAgentToolStart(run);
3733
+ const metadata = runtimeSubagentRunMetadata(run.inputPreview);
3734
+ if (metadata) {
3735
+ await this.subagentLifecycle(metadata).register(run.runId);
3736
+ }
3355
3737
  await this.broadcastApprovals();
3356
3738
  }
3357
3739
 
@@ -3363,6 +3745,40 @@ export abstract class AgentRuntimeKernel<
3363
3745
  result: AgentToolLifecycleResult,
3364
3746
  ): Promise<void> {
3365
3747
  await super.onAgentToolFinish(run, result);
3748
+ const metadata = runtimeSubagentRunMetadata(run.inputPreview);
3749
+ if (metadata) {
3750
+ const lifecycle = this.subagentLifecycle(metadata);
3751
+ await lifecycle.register(run.runId);
3752
+ if (!(result.status === "interrupted" && result.childStillRunning)) {
3753
+ const completion = decodeSubagentCompletion(result.summary);
3754
+ const hasUsage = completion && !isZeroSubagentUsage(completion.usage);
3755
+ const status = subagentTerminalStatus(result.status);
3756
+ if (completion && hasUsage) {
3757
+ await lifecycle.usage({
3758
+ eventId: `${run.runId}:usage`,
3759
+ submissionId: metadata.submissionId,
3760
+ runId: run.runId,
3761
+ subagentRunId: run.runId,
3762
+ parentRunId: metadata.parentRunId,
3763
+ ...(metadata.accountId ? { accountId: metadata.accountId } : {}),
3764
+ ...(metadata.rateVersion !== undefined
3765
+ ? { rateVersion: metadata.rateVersion }
3766
+ : {}),
3767
+ ...(metadata.slotIdentity
3768
+ ? { slotIdentity: metadata.slotIdentity }
3769
+ : {}),
3770
+ kind: "subagent",
3771
+ status,
3772
+ usage: completion.usage,
3773
+ });
3774
+ }
3775
+ await lifecycle.terminal({
3776
+ subagentRunId: run.runId,
3777
+ status,
3778
+ zeroUsage: !hasUsage,
3779
+ });
3780
+ }
3781
+ }
3366
3782
  await this.broadcastApprovals();
3367
3783
  }
3368
3784
 
@@ -3370,8 +3786,11 @@ export abstract class AgentRuntimeKernel<
3370
3786
  run: AgentToolRunInfo,
3371
3787
  result: AgentToolLifecycleResult,
3372
3788
  ): Promise<void> {
3789
+ const completion = decodeSubagentCompletion(result.summary);
3373
3790
  const outcome = result.status === "completed"
3374
- ? result.summary ?? "Completed without a result."
3791
+ ? completion
3792
+ ? JSON.stringify(completion.output)
3793
+ : result.summary ?? "Completed without a result."
3375
3794
  : result.error ?? `Background run ${result.status}.`;
3376
3795
  await this.submitPrompt(
3377
3796
  [