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

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 (40) 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 +34 -0
  7. package/src/adapter/cloudflare/universal-agent/tools.ts +7 -7
  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 +3 -0
  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 +17 -5
  24. package/src/lib/prompt.ts +3 -0
  25. package/src/pi/runtime-adapter/assembly.ts +2 -1
  26. package/src/pi/runtime-adapter/execution.ts +8 -1
  27. package/src/pi/runtime-adapter/index.ts +33 -0
  28. package/src/pi/runtime-adapter/models.ts +9 -0
  29. package/src/pi/tool/core-host.ts +5 -1
  30. package/src/pi/tool/core.ts +1 -10
  31. package/src/pi/tool/index.ts +1 -0
  32. package/src/pi/tool/mcp.ts +2 -2
  33. package/src/pi/tool/subagent.ts +142 -16
  34. package/src/pi/tool/workspace-revision.ts +64 -0
  35. package/src/runtime-agent-context.ts +1 -0
  36. package/src/runtime-assembler.ts +16 -1
  37. package/src/runtime-definition.ts +12 -0
  38. package/src/runtime.ts +427 -46
  39. package/src/tool-registry.ts +2 -0
  40. 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 {
@@ -178,6 +201,32 @@ function json(value: unknown): string {
178
201
  return JSON.stringify(value);
179
202
  }
180
203
 
204
+ function validateAdmissionResult(
205
+ result: import("./kernel/bindings").RuntimeSubmissionAdmissionResult,
206
+ ): import("./kernel/bindings").RuntimeSubmissionAdmissionResult {
207
+ if (!result || typeof result !== "object" || typeof result.accepted !== "boolean") {
208
+ throw new Error("Runtime admission result is invalid");
209
+ }
210
+ if (!result.accepted) {
211
+ if (
212
+ typeof result.code !== "string" || !result.code.trim() ||
213
+ (result.retryable !== undefined && typeof result.retryable !== "boolean")
214
+ ) {
215
+ throw new Error("Runtime admission rejection is invalid");
216
+ }
217
+ return result;
218
+ }
219
+ if (
220
+ typeof result.accountId !== "string" || !result.accountId.trim() ||
221
+ typeof result.slotIdentity !== "string" || !result.slotIdentity.trim() ||
222
+ !Number.isSafeInteger(result.rateVersion) ||
223
+ result.rateVersion <= 0
224
+ ) {
225
+ throw new Error("Runtime admission context is invalid");
226
+ }
227
+ return result;
228
+ }
229
+
181
230
  async function closeRuntimeGatewaySession(
182
231
  session: RuntimeGatewaySession | undefined,
183
232
  step: string,
@@ -192,10 +241,17 @@ function assistantUsageEvent(
192
241
  eventId: string,
193
242
  submissionId: string,
194
243
  message: AssistantMessage,
244
+ pinned?: Pick<StoredSubmission, "accountId" | "rateVersion" | "slotIdentity">,
195
245
  ): RuntimeModelUsageEvent {
196
246
  return {
197
247
  eventId,
198
248
  submissionId,
249
+ runId: submissionId,
250
+ ...(pinned?.accountId ? { accountId: pinned.accountId } : {}),
251
+ ...(pinned?.rateVersion !== null && pinned?.rateVersion !== undefined
252
+ ? { rateVersion: pinned.rateVersion }
253
+ : {}),
254
+ ...(pinned?.slotIdentity ? { slotIdentity: pinned.slotIdentity } : {}),
199
255
  kind: "assistant",
200
256
  api: message.api,
201
257
  provider: message.provider,
@@ -327,6 +383,10 @@ export abstract class AgentRuntimeKernel<
327
383
  private migratedSubmissionIds?: Set<string>;
328
384
  private db!: RuntimeDatabase;
329
385
  private readonly temporaryAgents = new TemporaryAgentCoordinator();
386
+ private readonly admissionsInFlight = new Map<
387
+ string,
388
+ Promise<import("./kernel/bindings").RuntimeSubmissionAdmissionResult>
389
+ >();
330
390
  private readonly temporaryAgentApprovals =
331
391
  new Map<string, PendingTemporaryAgentApproval>();
332
392
 
@@ -663,6 +723,37 @@ export abstract class AgentRuntimeKernel<
663
723
  } = {},
664
724
  ): PreparedPiTurnAdapter {
665
725
  const startedAt = output.startedAt ?? Date.now();
726
+ const snapshot = this.assembly();
727
+ const runMetadata: RuntimeSubagentRunMetadata = {
728
+ submissionId: submission.submissionId,
729
+ parentRunId: submission.runId ?? submission.submissionId,
730
+ ...(submission.accountId ? { accountId: submission.accountId } : {}),
731
+ ...(submission.rateVersion !== null
732
+ ? { rateVersion: submission.rateVersion }
733
+ : {}),
734
+ ...(submission.slotIdentity
735
+ ? { slotIdentity: submission.slotIdentity }
736
+ : {}),
737
+ };
738
+ const lifecycle = this.subagentLifecycle(runMetadata);
739
+ const toolExecutors = snapshot.bindings.subagents
740
+ ? this.pi.subagentToolExecutors(
741
+ snapshot.bindings.subagents,
742
+ snapshot.profile.enabledSubagents,
743
+ {
744
+ ...runMetadata,
745
+ onRegistered: async (runId) => {
746
+ await lifecycle.register(runId);
747
+ },
748
+ onUsage: async (event) => {
749
+ await lifecycle.usage(event);
750
+ },
751
+ onTerminal: async (terminal) => {
752
+ await lifecycle.terminal(terminal);
753
+ },
754
+ },
755
+ )
756
+ : undefined;
666
757
  return this.pi.createTurn({
667
758
  prepared: this.preparedPi(),
668
759
  pinnedDescriptor: submission.assemblyDescriptor,
@@ -707,6 +798,9 @@ export abstract class AgentRuntimeKernel<
707
798
  settleTool: (call) =>
708
799
  this.settleTool(submission.submissionId, call),
709
800
  },
801
+ ...(toolExecutors && Object.keys(toolExecutors).length > 0
802
+ ? { toolExecutors }
803
+ : {}),
710
804
  onToolTelemetry: (event) => {
711
805
  this._emit("ua:tool" as never, { ...event });
712
806
  },
@@ -732,6 +826,7 @@ export abstract class AgentRuntimeKernel<
732
826
  persisted.id,
733
827
  submission.submissionId,
734
828
  commit.message,
829
+ submission,
735
830
  );
736
831
  this.db.runtimeEvents.insert({
737
832
  eventId: event.eventId,
@@ -774,6 +869,29 @@ export abstract class AgentRuntimeKernel<
774
869
  });
775
870
  }
776
871
 
872
+ private subagentLifecycle(
873
+ input: RuntimeSubagentRunMetadata,
874
+ ): RuntimeSubagentLifecycleRecorder {
875
+ return new RuntimeSubagentLifecycleRecorder(input, {
876
+ save: async (event) => {
877
+ this.db.transaction(() => {
878
+ this.db.runtimeEvents.insert({
879
+ eventId: event.eventId,
880
+ body: json({
881
+ type: event.kind === "subagent" ? "subagent-usage" : "lifecycle",
882
+ event,
883
+ }),
884
+ createdAt: Date.now(),
885
+ });
886
+ });
887
+ await this.drainRuntimeEvents();
888
+ },
889
+ has: (eventId) => this.db.runtimeEvents.has(eventId),
890
+ hasSubagentUsage: (submissionId, subagentRunId) =>
891
+ this.db.runtimeEvents.hasSubagentUsage(submissionId, subagentRunId),
892
+ });
893
+ }
894
+
777
895
  /**
778
896
  * 在 Agent 实例启动或从休眠唤醒后恢复 Session 本地后台状态。
779
897
  *
@@ -822,9 +940,23 @@ export abstract class AgentRuntimeKernel<
822
940
  if (payload.type === "model-usage") {
823
941
  if (!turnEvents.onModelUsage) continue;
824
942
  await turnEvents.onModelUsage(payload.event);
825
- } else {
943
+ } else if (payload.type === "tool-settlement") {
826
944
  if (!turnEvents.onToolSettled) continue;
827
945
  await turnEvents.onToolSettled(payload.event);
946
+ } else if (payload.type === "subagent-usage") {
947
+ if (!turnEvents.onSubagentUsage) continue;
948
+ const confirmation = await turnEvents.onSubagentUsage(payload.event);
949
+ if (confirmation?.confirmed !== true) {
950
+ failed = true;
951
+ break;
952
+ }
953
+ } else {
954
+ if (!turnEvents.onLifecycleFact) continue;
955
+ const confirmation = await turnEvents.onLifecycleFact(payload.event);
956
+ if (confirmation?.confirmed !== true) {
957
+ failed = true;
958
+ break;
959
+ }
828
960
  }
829
961
  this.db.transaction(() =>
830
962
  this.db.runtimeEvents.markDelivered(row.eventId, Date.now())
@@ -835,6 +967,7 @@ export abstract class AgentRuntimeKernel<
835
967
  "[runtime-turn-event:degraded]",
836
968
  json({ eventId: row.eventId, error: errorText(error) }),
837
969
  );
970
+ break;
838
971
  }
839
972
  }
840
973
  if (!failed) return;
@@ -1255,7 +1388,23 @@ export abstract class AgentRuntimeKernel<
1255
1388
  onCompactionPersisted: (event) =>
1256
1389
  this.db.runtimeEvents.insert({
1257
1390
  eventId: event.eventId,
1258
- body: json({ type: "model-usage", event }),
1391
+ body: json({
1392
+ type: "model-usage",
1393
+ event: {
1394
+ ...event,
1395
+ runId: submissionId,
1396
+ ...(this.readSubmission(submissionId)?.accountId
1397
+ ? { accountId: this.readSubmission(submissionId)!.accountId! }
1398
+ : {}),
1399
+ ...(this.readSubmission(submissionId)?.rateVersion !== null &&
1400
+ this.readSubmission(submissionId)?.rateVersion !== undefined
1401
+ ? { rateVersion: this.readSubmission(submissionId)!.rateVersion! }
1402
+ : {}),
1403
+ ...(this.readSubmission(submissionId)?.slotIdentity
1404
+ ? { slotIdentity: this.readSubmission(submissionId)!.slotIdentity! }
1405
+ : {}),
1406
+ },
1407
+ }),
1259
1408
  createdAt: Date.now(),
1260
1409
  }),
1261
1410
  });
@@ -1303,31 +1452,147 @@ export abstract class AgentRuntimeKernel<
1303
1452
  ? { idempotencyKey: options.idempotencyKey }
1304
1453
  : {}),
1305
1454
  prepareAdmission: async () => {
1455
+ await this.ensureRuntimeReady();
1306
1456
  const regenerateEntry =
1307
1457
  options.regenerate && options.userMessageId
1308
1458
  ? await this.transcript.findUserMessage(options.userMessageId)
1309
1459
  : undefined;
1460
+ if (
1461
+ options.regenerate &&
1462
+ (!regenerateEntry ||
1463
+ userContentKey(regenerateEntry) !== userContentKey(userMessage))
1464
+ ) {
1465
+ throw new Error(
1466
+ "Regenerate request must match an existing user message",
1467
+ );
1468
+ }
1469
+ const hook = this.runtimeHooks?.onSubmissionAdmission;
1470
+ let existing: StoredSubmissionAdmission | null = null;
1471
+ if (hook) {
1472
+ const attemptedAt = Date.now();
1473
+ const reservedRunId = crypto.randomUUID();
1474
+ this.db.transaction(() => {
1475
+ const current = this.db.submissionAdmissions.find(
1476
+ options.requestId,
1477
+ options.idempotencyKey,
1478
+ );
1479
+ if (current) {
1480
+ // 同一 identity 重投:沿用已固定的 runId,只把这次尝试的时间推到现在,
1481
+ // 使它重新占住一个在途名额。
1482
+ if (current.status === "pending") {
1483
+ this.db.submissionAdmissions.recordAttempt(
1484
+ current.requestId,
1485
+ attemptedAt,
1486
+ );
1487
+ }
1488
+ return;
1489
+ }
1490
+ if (
1491
+ this.db.submissions.countPending() +
1492
+ this.db.submissionAdmissions.countPending(
1493
+ attemptedAt - ADMISSION_ATTEMPT_WINDOW_MS,
1494
+ ) >=
1495
+ MAX_PENDING_SUBMISSIONS
1496
+ ) {
1497
+ throw new SubmissionQueueFullError();
1498
+ }
1499
+ this.db.submissionAdmissions.begin(
1500
+ options.requestId,
1501
+ options.idempotencyKey ?? null,
1502
+ reservedRunId,
1503
+ attemptedAt,
1504
+ );
1505
+ });
1506
+ existing = this.db.submissionAdmissions.find(
1507
+ options.requestId,
1508
+ options.idempotencyKey,
1509
+ );
1510
+ }
1511
+ const runId = existing?.runId ?? crypto.randomUUID();
1512
+ const admissionRequestId = existing?.requestId ?? options.requestId;
1513
+ const admissionIdempotencyKey = existing
1514
+ ? existing.idempotencyKey ?? undefined
1515
+ : options.idempotencyKey;
1516
+ const admissionKey = admissionIdempotencyKey
1517
+ ? `idempotency:${admissionIdempotencyKey}`
1518
+ : `request:${admissionRequestId}`;
1519
+ const pendingAdmission = hook && existing?.status === "pending"
1520
+ ? this.admissionsInFlight.get(admissionKey)
1521
+ : undefined;
1522
+ const admissionPromise = pendingAdmission ?? (hook && (!existing || existing.status === "pending")
1523
+ ? hook({
1524
+ requestId: admissionRequestId,
1525
+ ...(admissionIdempotencyKey
1526
+ ? { idempotencyKey: admissionIdempotencyKey }
1527
+ : {}),
1528
+ runId,
1529
+ }).then(validateAdmissionResult).catch((error) => {
1530
+ this.admissionsInFlight.delete(admissionKey);
1531
+ throw error;
1532
+ })
1533
+ : undefined);
1534
+ if (hook && admissionPromise && !pendingAdmission) {
1535
+ this.admissionsInFlight.set(
1536
+ admissionKey,
1537
+ admissionPromise,
1538
+ );
1539
+ }
1540
+ let admission = existing?.status === "accepted"
1541
+ ? {
1542
+ accepted: true as const,
1543
+ accountId: existing.accountId!,
1544
+ rateVersion: existing.rateVersion!,
1545
+ slotIdentity: existing.slotIdentity!,
1546
+ }
1547
+ : existing?.status === "rejected"
1548
+ ? {
1549
+ accepted: false as const,
1550
+ code: existing.code ?? "admission_rejected",
1551
+ retryable: existing.retryable ?? false,
1552
+ }
1553
+ : admissionPromise
1554
+ ? await admissionPromise
1555
+ : undefined;
1310
1556
  return () => {
1311
- const submissionId = crypto.randomUUID();
1557
+ if (admission && !admission.accepted) {
1558
+ this.db.submissionAdmissions.saveRejected(
1559
+ admissionRequestId,
1560
+ runId,
1561
+ admission.code,
1562
+ admission.retryable ?? false,
1563
+ );
1564
+ const rejected = this.insertSkippedSubmission({
1565
+ submissionId: runId,
1566
+ requestId: admissionRequestId,
1567
+ idempotencyKey: admissionIdempotencyKey ?? null,
1568
+ createdAt: userMessage.timestamp,
1569
+ code: admission.code,
1570
+ retryable: admission.retryable ?? false,
1571
+ });
1572
+ this.admissionsInFlight.delete(admissionKey);
1573
+ return {
1574
+ ...rejected,
1575
+ admissionRejected: true,
1576
+ };
1577
+ }
1578
+ const submissionId = runId;
1312
1579
  const assistantMessageId = crypto.randomUUID();
1313
1580
  const createdAt = userMessage.timestamp;
1314
1581
  const userMessageId =
1315
1582
  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
- }
1583
+ if (admission?.accepted) {
1584
+ this.db.submissionAdmissions.saveAccepted(
1585
+ admissionRequestId,
1586
+ runId,
1587
+ admission.accountId,
1588
+ admission.rateVersion,
1589
+ admission.slotIdentity,
1590
+ );
1326
1591
  }
1327
1592
  this.db.submissions.insert({
1328
1593
  submissionId,
1329
- requestId: options.requestId,
1330
- idempotencyKey: options.idempotencyKey ?? null,
1594
+ requestId: admissionRequestId,
1595
+ idempotencyKey: admissionIdempotencyKey ?? null,
1331
1596
  createdAt,
1332
1597
  assemblyRevision: "",
1333
1598
  assemblyDescriptor: "",
@@ -1340,7 +1605,28 @@ export abstract class AgentRuntimeKernel<
1340
1605
  regenerateMessageId: options.regenerate
1341
1606
  ? options.userMessageId ?? null
1342
1607
  : null,
1608
+ runId,
1609
+ accountId: admission?.accepted ? admission.accountId : null,
1610
+ rateVersion: admission?.accepted ? admission.rateVersion : null,
1611
+ slotIdentity: admission?.accepted ? admission.slotIdentity : null,
1343
1612
  });
1613
+ if (admission?.accepted) {
1614
+ const fact: RuntimeLifecycleFact = {
1615
+ kind: "submission-admitted",
1616
+ eventId: `${submissionId}:admitted`,
1617
+ submissionId,
1618
+ runId: submissionId,
1619
+ accountId: admission.accountId,
1620
+ rateVersion: admission.rateVersion,
1621
+ slotIdentity: admission.slotIdentity,
1622
+ };
1623
+ this.db.runtimeEvents.insert({
1624
+ eventId: fact.eventId,
1625
+ body: json({ type: "lifecycle", event: fact }),
1626
+ createdAt,
1627
+ });
1628
+ }
1629
+ if (hook) this.admissionsInFlight.delete(admissionKey);
1344
1630
  return this.readSubmission(submissionId)!;
1345
1631
  };
1346
1632
  },
@@ -1364,6 +1650,7 @@ export abstract class AgentRuntimeKernel<
1364
1650
  const submitted = await this.submissions.submit(
1365
1651
  this.submissionInput(userMessage, options),
1366
1652
  );
1653
+ await this.drainRuntimeEvents();
1367
1654
  await this.broadcastApprovals();
1368
1655
  return submitted;
1369
1656
  }
@@ -1380,7 +1667,10 @@ export abstract class AgentRuntimeKernel<
1380
1667
  this.submissionInput(userMessage, options),
1381
1668
  timeoutMs,
1382
1669
  );
1383
- if (submitted) await this.broadcastApprovals();
1670
+ if (submitted) {
1671
+ await this.drainRuntimeEvents();
1672
+ await this.broadcastApprovals();
1673
+ }
1384
1674
  return submitted;
1385
1675
  }
1386
1676
 
@@ -1410,6 +1700,46 @@ export abstract class AgentRuntimeKernel<
1410
1700
  return submission;
1411
1701
  }
1412
1702
 
1703
+ private insertSubmissionTerminalFact(submission: StoredSubmission): void {
1704
+ const terminalFact: RuntimeLifecycleFact = {
1705
+ kind: "submission-terminal",
1706
+ eventId: `${submission.submissionId}:terminal`,
1707
+ submissionId: submission.submissionId,
1708
+ runId: submission.runId ?? submission.submissionId,
1709
+ status: submission.status as "completed" | "aborted" | "skipped" | "error",
1710
+ childRegistrationClosed: true,
1711
+ zeroUsage: !this.db.runtimeEvents.hasNonZeroModelUsage(
1712
+ submission.submissionId,
1713
+ ),
1714
+ ...(submission.accountId ? { accountId: submission.accountId } : {}),
1715
+ ...(submission.rateVersion !== null
1716
+ ? { rateVersion: submission.rateVersion }
1717
+ : {}),
1718
+ ...(submission.slotIdentity
1719
+ ? { slotIdentity: submission.slotIdentity }
1720
+ : {}),
1721
+ };
1722
+ this.db.runtimeEvents.insert({
1723
+ eventId: terminalFact.eventId,
1724
+ body: json({ type: "lifecycle", event: terminalFact }),
1725
+ createdAt: submission.completedAt ?? Date.now(),
1726
+ });
1727
+ }
1728
+
1729
+ private insertSkippedSubmission(input: {
1730
+ submissionId: string;
1731
+ requestId: string;
1732
+ idempotencyKey: string | null;
1733
+ createdAt: number;
1734
+ code: string;
1735
+ retryable: boolean;
1736
+ }): StoredSubmission {
1737
+ this.db.submissions.insertRejected(input);
1738
+ const skipped = this.readSubmission(input.submissionId)!;
1739
+ this.insertSubmissionTerminalFact(skipped);
1740
+ return skipped;
1741
+ }
1742
+
1413
1743
  // 作用:读取某个 Submission 已持久的全部 Pi 恢复里程碑。
1414
1744
  // 调用:恢复决策器每次检查或应用命令前调用。
1415
1745
  // 原因:决策只依赖持久事实,重启后才会得到同样结果。
@@ -1713,6 +2043,23 @@ export abstract class AgentRuntimeKernel<
1713
2043
  }
1714
2044
  if (isTerminalSubmissionStatus(latest.status)) return latest;
1715
2045
 
2046
+ for (const steer of this.db.steers.listForSubmission(latest.submissionId)) {
2047
+ const userMessage = JSON.parse(steer.canonicalJson) as PiCanonicalUserInput;
2048
+ const uiMessage = steer.uiMessageJson
2049
+ ? JSON.parse(steer.uiMessageJson) as UIMessage & { role: "user" }
2050
+ : undefined;
2051
+ const submitted = await this.submitMessage(userMessage, {
2052
+ requestId: steer.messageId,
2053
+ idempotencyKey: steer.messageId,
2054
+ userMessageId: steer.messageId,
2055
+ ...(uiMessage ? { userMessage: uiMessage } : {}),
2056
+ });
2057
+ this.ctx.waitUntil(submitted.completion);
2058
+ this.db.transaction(() =>
2059
+ this.db.steers.deleteByMessageId(steer.messageId)
2060
+ );
2061
+ }
2062
+
1716
2063
  const durableIntent = this.decidePiRecovery(latest).terminal;
1717
2064
  const effectiveOutcome = latest.abortReason
1718
2065
  ? "aborted"
@@ -1742,31 +2089,12 @@ export abstract class AgentRuntimeKernel<
1742
2089
  : effectiveOutcome === "aborted"
1743
2090
  ? "aborted"
1744
2091
  : "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
2092
  const terminal = this.updateSubmission(
1766
2093
  latest.submissionId,
1767
2094
  status,
1768
2095
  effectiveMessage,
1769
2096
  );
2097
+ this.insertSubmissionTerminalFact(terminal);
1770
2098
  if (
1771
2099
  effectiveOutcome === "aborted" &&
1772
2100
  effectiveMessage === USER_STOP_REASON
@@ -3048,7 +3376,9 @@ export abstract class AgentRuntimeKernel<
3048
3376
  const reason = "Moved to active Turn";
3049
3377
  this.appendTerminalIntent(latest, "aborted", reason);
3050
3378
  this.db.submissions.updateAbortReason(submissionId, reason);
3051
- this.updateSubmission(submissionId, "aborted", reason);
3379
+ this.insertSubmissionTerminalFact(
3380
+ this.updateSubmission(submissionId, "aborted", reason),
3381
+ );
3052
3382
  return true;
3053
3383
  });
3054
3384
  if (!moved) {
@@ -3064,6 +3394,7 @@ export abstract class AgentRuntimeKernel<
3064
3394
  active.agent.steer(message, queued.userMessageId);
3065
3395
  }
3066
3396
  } finally {
3397
+ await this.drainRuntimeEvents();
3067
3398
  await this.broadcastApprovals();
3068
3399
  }
3069
3400
  return {
@@ -3101,13 +3432,22 @@ export abstract class AgentRuntimeKernel<
3101
3432
  SCHEDULED_STABLE_TIMEOUT_MS,
3102
3433
  );
3103
3434
  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
- };
3435
+ const skipped = this.db.transaction(() => {
3436
+ const duplicate = options?.idempotencyKey
3437
+ ? this.db.submissions.findByIdempotencyKey(options.idempotencyKey)
3438
+ : this.db.submissions.findByRequestId(requestId);
3439
+ return duplicate ?? this.insertSkippedSubmission({
3440
+ submissionId: requestId,
3441
+ requestId,
3442
+ idempotencyKey: options?.idempotencyKey ?? null,
3443
+ createdAt: Date.now(),
3444
+ code: "Runtime not stable within timeout",
3445
+ retryable: true,
3446
+ });
3447
+ }) as StoredSubmission;
3448
+ await this.drainRuntimeEvents();
3449
+ await this.broadcastApprovals();
3450
+ return skipped;
3111
3451
  }
3112
3452
  this.ctx.waitUntil(submitted.completion.then(() => undefined));
3113
3453
  return submitted.receipt;
@@ -3352,6 +3692,10 @@ export abstract class AgentRuntimeKernel<
3352
3692
  // 原因:分离的子运行不进 Submission 表,不在这里重算,Host 的列表会显示成已经空闲。
3353
3693
  override async onAgentToolStart(run: AgentToolRunInfo): Promise<void> {
3354
3694
  await super.onAgentToolStart(run);
3695
+ const metadata = runtimeSubagentRunMetadata(run.inputPreview);
3696
+ if (metadata) {
3697
+ await this.subagentLifecycle(metadata).register(run.runId);
3698
+ }
3355
3699
  await this.broadcastApprovals();
3356
3700
  }
3357
3701
 
@@ -3363,6 +3707,40 @@ export abstract class AgentRuntimeKernel<
3363
3707
  result: AgentToolLifecycleResult,
3364
3708
  ): Promise<void> {
3365
3709
  await super.onAgentToolFinish(run, result);
3710
+ const metadata = runtimeSubagentRunMetadata(run.inputPreview);
3711
+ if (metadata) {
3712
+ const lifecycle = this.subagentLifecycle(metadata);
3713
+ await lifecycle.register(run.runId);
3714
+ if (!(result.status === "interrupted" && result.childStillRunning)) {
3715
+ const completion = decodeSubagentCompletion(result.summary);
3716
+ const hasUsage = completion && !isZeroSubagentUsage(completion.usage);
3717
+ const status = subagentTerminalStatus(result.status);
3718
+ if (completion && hasUsage) {
3719
+ await lifecycle.usage({
3720
+ eventId: `${run.runId}:usage`,
3721
+ submissionId: metadata.submissionId,
3722
+ runId: run.runId,
3723
+ subagentRunId: run.runId,
3724
+ parentRunId: metadata.parentRunId,
3725
+ ...(metadata.accountId ? { accountId: metadata.accountId } : {}),
3726
+ ...(metadata.rateVersion !== undefined
3727
+ ? { rateVersion: metadata.rateVersion }
3728
+ : {}),
3729
+ ...(metadata.slotIdentity
3730
+ ? { slotIdentity: metadata.slotIdentity }
3731
+ : {}),
3732
+ kind: "subagent",
3733
+ status,
3734
+ usage: completion.usage,
3735
+ });
3736
+ }
3737
+ await lifecycle.terminal({
3738
+ subagentRunId: run.runId,
3739
+ status,
3740
+ zeroUsage: !hasUsage,
3741
+ });
3742
+ }
3743
+ }
3366
3744
  await this.broadcastApprovals();
3367
3745
  }
3368
3746
 
@@ -3370,8 +3748,11 @@ export abstract class AgentRuntimeKernel<
3370
3748
  run: AgentToolRunInfo,
3371
3749
  result: AgentToolLifecycleResult,
3372
3750
  ): Promise<void> {
3751
+ const completion = decodeSubagentCompletion(result.summary);
3373
3752
  const outcome = result.status === "completed"
3374
- ? result.summary ?? "Completed without a result."
3753
+ ? completion
3754
+ ? JSON.stringify(completion.output)
3755
+ : result.summary ?? "Completed without a result."
3375
3756
  : result.error ?? `Background run ${result.status}.`;
3376
3757
  await this.submitPrompt(
3377
3758
  [