@springbrand/agent-runtime 0.2.0-alpha.15 → 0.2.0-alpha.17

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/adapter/cloudflare/sandbox/adapter.ts +61 -36
  3. package/src/adapter/cloudflare/universal-agent/preparation.ts +0 -2
  4. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +23 -18
  5. package/src/db/index.ts +5 -0
  6. package/src/db/schema.ts +15 -0
  7. package/src/db/telemetry-outbox.repo.ts +151 -0
  8. package/src/index.ts +1 -0
  9. package/src/kernel/approval-lifecycle.ts +35 -3
  10. package/src/kernel/bindings.ts +2 -1
  11. package/src/kernel/interaction-lifecycle.ts +35 -6
  12. package/src/layers/orchestration/temporary-agent/workspace.ts +4 -4
  13. package/src/lib/prompt.ts +22 -15
  14. package/src/pi/assembly/context.ts +2 -2
  15. package/src/pi/message/conversion.ts +13 -1
  16. package/src/pi/runtime-adapter/assembly.ts +1 -0
  17. package/src/pi/runtime-adapter/execution.ts +63 -27
  18. package/src/pi/runtime-adapter/models.ts +144 -44
  19. package/src/pi/tool/ai-adapter.ts +2 -2
  20. package/src/pi/tool/base.ts +31 -25
  21. package/src/pi/tool/compiler.ts +6 -102
  22. package/src/pi/turn/tool-recovery.ts +11 -1
  23. package/src/runtime-agent.ts +24 -0
  24. package/src/runtime-assembler.ts +38 -19
  25. package/src/runtime-definition.ts +2 -0
  26. package/src/runtime.ts +362 -20
  27. package/src/telemetry/contract.ts +389 -0
  28. package/src/telemetry/coordinator.ts +143 -0
  29. package/src/telemetry/delivery.ts +138 -0
  30. package/src/telemetry/ids.ts +60 -0
  31. package/src/telemetry/index.ts +7 -0
  32. package/src/telemetry/recorder.ts +61 -0
  33. package/src/telemetry/runtime-telemetry.ts +484 -0
  34. package/src/telemetry/sanitize.ts +97 -0
  35. package/src/tool-registry.ts +18 -11
  36. package/src/lib/telemetry-dev.ts +0 -47
package/src/runtime.ts CHANGED
@@ -58,8 +58,10 @@ import {
58
58
  } from "./runtime-assembler";
59
59
  import type { RuntimeAgentHooks } from "./runtime-definition";
60
60
  import type { RuntimeTurnEventsPort } from "./kernel/bindings";
61
+ import {
62
+ RuntimeTelemetryCoordinator,
63
+ } from "./telemetry";
61
64
  import { connectConfiguredMcpServers } from "./lib/mcp";
62
- import { installConsoleSink } from "./lib/telemetry-dev";
63
65
  import {
64
66
  PiRuntimeAdapter,
65
67
  MODEL_STREAM_STALL_TIMEOUT_MS,
@@ -190,6 +192,8 @@ interface ActiveTurn {
190
192
  interface PlannedContinuationData {
191
193
  readonly submissionId: string;
192
194
  readonly requestId: string;
195
+ /** Identifies the yielded execution slice so retries dedupe without swallowing the next slice. */
196
+ readonly continuationId: string;
193
197
  }
194
198
 
195
199
  // 作用:把任意异常整理成可以持久化或发给客户端的文字。
@@ -339,6 +343,8 @@ function requestedCapabilitiesOf(metadata: unknown): RequestedCapability[] {
339
343
 
340
344
  interface PendingTemporaryAgentApproval {
341
345
  receipt: ApprovalReceipt;
346
+ submissionId?: string;
347
+ toolCallId: string;
342
348
  resolve(decision: TemporaryAgentApprovalDecision): void;
343
349
  }
344
350
 
@@ -375,6 +381,7 @@ export abstract class AgentRuntimeKernel<
375
381
  private runtimeGatewaySession?: RuntimeGatewaySession;
376
382
  private runtimeTurnEvents?: RuntimeTurnEventsPort;
377
383
  private runtimeHooks?: RuntimeAgentHooks;
384
+ private telemetryCoordinator?: RuntimeTelemetryCoordinator;
378
385
  private piAdapter?: PiRuntimeAdapter;
379
386
  private readonly transcript: PiRuntimeTranscript;
380
387
  private readonly submissions: SubmissionLifecycle<
@@ -422,6 +429,27 @@ export abstract class AgentRuntimeKernel<
422
429
  });
423
430
  }
424
431
 
432
+ private get telemetry(): RuntimeTelemetryCoordinator {
433
+ return this.telemetryCoordinator ??= new RuntimeTelemetryCoordinator({
434
+ outbox: () => this.db?.telemetry,
435
+ waitUntil: (promise) => this.ctx.waitUntil(promise),
436
+ scheduleRetry: async (delaySeconds, idempotent) => {
437
+ await this.schedule(
438
+ delaySeconds,
439
+ "_drainTelemetry",
440
+ undefined,
441
+ { idempotent },
442
+ );
443
+ },
444
+ onError: (error) => {
445
+ console.warn(
446
+ "[runtime-telemetry:degraded]",
447
+ json({ error: errorText(error) }),
448
+ );
449
+ },
450
+ });
451
+ }
452
+
425
453
  /**
426
454
  * 为一个 Cloudflare Durable Object 实例建立持久化与执行协作器。
427
455
  *
@@ -510,6 +538,27 @@ export abstract class AgentRuntimeKernel<
510
538
  );
511
539
  },
512
540
  onApprovalsChanged: () => this.broadcastApprovals(),
541
+ onTelemetryRequested: (approval) => {
542
+ this.telemetry.capture("approvalRequested", {
543
+ submissionId: approval.submissionId,
544
+ toolCallId: approval.toolCallId,
545
+ executionId: approval.executionId,
546
+ toolName: approval.toolName,
547
+ summary: approval.summary,
548
+ input: safeParseJson(approval.inputJson),
549
+ occurredAt: approval.createdAt,
550
+ });
551
+ },
552
+ onTelemetryDecided: (approval, decision) => {
553
+ this.telemetry.capture("approvalDecided", {
554
+ submissionId: approval.submissionId,
555
+ toolCallId: approval.toolCallId,
556
+ executionId: approval.executionId,
557
+ decision,
558
+ ...(approval.reason ? { reason: approval.reason } : {}),
559
+ occurredAt: approval.decidedAt ?? Date.now(),
560
+ });
561
+ },
513
562
  });
514
563
  this.interactions = new InteractionLifecycle({
515
564
  db: this.db,
@@ -526,6 +575,38 @@ export abstract class AgentRuntimeKernel<
526
575
  await this.submissions.recover(submissionId);
527
576
  },
528
577
  onInteractionsChanged: () => this.broadcastApprovals(),
578
+ onTelemetryRequested: (interaction) => {
579
+ this.telemetry.capture("interactionRequested", {
580
+ submissionId: interaction.submissionId,
581
+ toolCallId: interaction.toolCallId,
582
+ interactionId: interaction.interactionId,
583
+ toolName: interaction.toolName,
584
+ input: safeParseJson(interaction.inputJson),
585
+ occurredAt: interaction.createdAt,
586
+ });
587
+ },
588
+ onTelemetrySettled: (interaction) => {
589
+ if (interaction.status === "responded") {
590
+ this.telemetry.capture("interactionResponded", {
591
+ submissionId: interaction.submissionId,
592
+ toolCallId: interaction.toolCallId,
593
+ interactionId: interaction.interactionId,
594
+ ...(interaction.responseJson
595
+ ? { response: safeParseJson(interaction.responseJson) }
596
+ : {}),
597
+ occurredAt: interaction.respondedAt ?? Date.now(),
598
+ });
599
+ return;
600
+ }
601
+ if (interaction.status === "cancelled") {
602
+ this.telemetry.capture("interactionCancelled", {
603
+ submissionId: interaction.submissionId,
604
+ toolCallId: interaction.toolCallId,
605
+ interactionId: interaction.interactionId,
606
+ occurredAt: interaction.respondedAt ?? Date.now(),
607
+ });
608
+ }
609
+ },
529
610
  });
530
611
  }
531
612
 
@@ -671,9 +752,6 @@ export abstract class AgentRuntimeKernel<
671
752
  for (const [index, guard] of candidate.commitGuards.entries()) {
672
753
  await withRuntimeLoadTimeout(`commit-guard:${index}`, guard);
673
754
  }
674
- if (candidateSnapshot.bindings.platform.telemetryConsole) {
675
- installConsoleSink();
676
- }
677
755
  this.pi.activate(candidateSnapshot);
678
756
  activated = true;
679
757
  repin?.commit();
@@ -681,6 +759,7 @@ export abstract class AgentRuntimeKernel<
681
759
  this.runtimeRevision = revision;
682
760
  this.runtimePi = prepared;
683
761
  this.runtimeGatewaySession = gatewaySession;
762
+ this.telemetry.configure(candidate.telemetry);
684
763
  } catch (error) {
685
764
  repin?.abort();
686
765
  if (activated && previous) this.pi.activate(previous);
@@ -707,6 +786,11 @@ export abstract class AgentRuntimeKernel<
707
786
  }
708
787
  }
709
788
 
789
+ async _drainTelemetry(): Promise<void> {
790
+ await this.ensureRuntimeReady();
791
+ await this.telemetry.drainScheduled();
792
+ }
793
+
710
794
  // 作用:为某个已准入 Submission 建立一次 Pi Turn 执行适配器。
711
795
  // 调用:新 Turn、恢复 Turn 和审批后重试 Tool 都在开始 Pi 执行前调用。
712
796
  // 原因:在这里一次性固定 revision、持久化回调与终态回调,防止各执行路径绕过同一耐久性规则。
@@ -803,12 +887,43 @@ export abstract class AgentRuntimeKernel<
803
887
  settleTool: (call) =>
804
888
  this.settleTool(submission.submissionId, call),
805
889
  },
890
+ onGeneration: (event) => {
891
+ if (event.type === "started") {
892
+ this.telemetry.capture("generationStarted", {
893
+ submissionId: submission.submissionId,
894
+ generationId: event.generationId,
895
+ provider: event.provider,
896
+ model: event.model,
897
+ input: event.input,
898
+ occurredAt: event.timestamp,
899
+ });
900
+ return;
901
+ }
902
+ this.telemetry.capture("generationFinished", {
903
+ submissionId: submission.submissionId,
904
+ generationId: event.generationId,
905
+ outcome: event.outcome === "success"
906
+ ? "completed"
907
+ : event.outcome === "cancelled"
908
+ ? "cancelled"
909
+ : "failed",
910
+ durationMs: event.durationMs,
911
+ ...(event.usage
912
+ ? {
913
+ inputTokens: event.usage.input,
914
+ outputTokens: event.usage.output,
915
+ totalTokens: event.usage.totalTokens,
916
+ cost: event.usage.cost.total,
917
+ }
918
+ : {}),
919
+ ...(event.errorName ? { error: event.errorName } : {}),
920
+ ...(event.output ? { output: event.output } : {}),
921
+ occurredAt: event.timestamp,
922
+ });
923
+ },
806
924
  ...(toolExecutors && Object.keys(toolExecutors).length > 0
807
925
  ? { toolExecutors }
808
926
  : {}),
809
- onToolTelemetry: (event) => {
810
- this._emit("ua:tool" as never, { ...event });
811
- },
812
927
  transformContext: (messages, signal) =>
813
928
  this.transformPiContext(submission.submissionId, messages, signal),
814
929
  abortReason: () =>
@@ -911,6 +1026,7 @@ export abstract class AgentRuntimeKernel<
911
1026
  */
912
1027
  async onStart(): Promise<void> {
913
1028
  this.runtimeLoad.reset();
1029
+ this.telemetry.recover(() => this.ensureRuntimeReady());
914
1030
  if (this.db.runtimeEvents.hasPending()) {
915
1031
  this.ctx.waitUntil(
916
1032
  this.ensureRuntimeReady().then(() => this.drainRuntimeEvents()),
@@ -1012,7 +1128,54 @@ export abstract class AgentRuntimeKernel<
1012
1128
  context: TemporaryAgentRunContext,
1013
1129
  execute: TemporaryAgentExecutor,
1014
1130
  ): Promise<string> {
1015
- return this.temporaryAgents.run(request, execute, context.signal);
1131
+ return this.temporaryAgents.run(
1132
+ request,
1133
+ async (normalized) => {
1134
+ const submission = this.findSubmissionByRequest(context.requestId);
1135
+ const subagentId = `temporary:${context.toolCallId}`;
1136
+ const startedAt = Date.now();
1137
+ if (submission) {
1138
+ this.telemetry.capture("subagentStarted", {
1139
+ submissionId: submission.submissionId,
1140
+ toolCallId: context.toolCallId,
1141
+ subagentId,
1142
+ agentType: normalized.subagentName,
1143
+ prompt: {
1144
+ instructions: normalized.instructions,
1145
+ task: normalized.task,
1146
+ },
1147
+ occurredAt: startedAt,
1148
+ });
1149
+ }
1150
+ try {
1151
+ const output = await execute(normalized);
1152
+ if (submission) {
1153
+ this.telemetry.capture("subagentFinished", {
1154
+ submissionId: submission.submissionId,
1155
+ toolCallId: context.toolCallId,
1156
+ subagentId,
1157
+ outcome: "completed",
1158
+ durationMs: Math.max(0, Date.now() - startedAt),
1159
+ output,
1160
+ });
1161
+ }
1162
+ return output;
1163
+ } catch (error) {
1164
+ if (submission) {
1165
+ this.telemetry.capture("subagentFinished", {
1166
+ submissionId: submission.submissionId,
1167
+ toolCallId: context.toolCallId,
1168
+ subagentId,
1169
+ outcome: context.signal.aborted ? "cancelled" : "failed",
1170
+ durationMs: Math.max(0, Date.now() - startedAt),
1171
+ error: errorText(error),
1172
+ });
1173
+ }
1174
+ throw error;
1175
+ }
1176
+ },
1177
+ context.signal,
1178
+ );
1016
1179
  }
1017
1180
 
1018
1181
  /**
@@ -1032,6 +1195,7 @@ export abstract class AgentRuntimeKernel<
1032
1195
  `temporary agent approval already pending: ${request.executionId}`,
1033
1196
  );
1034
1197
  }
1198
+ const submission = this.findSubmissionByRequest(request.requestId);
1035
1199
  return new Promise((resolve, reject) => {
1036
1200
  this.temporaryAgentApprovals.set(request.executionId, {
1037
1201
  receipt: {
@@ -1044,8 +1208,20 @@ export abstract class AgentRuntimeKernel<
1044
1208
  inputJson: JSON.stringify(request.input ?? null),
1045
1209
  requestId: request.requestId,
1046
1210
  },
1211
+ ...(submission ? { submissionId: submission.submissionId } : {}),
1212
+ toolCallId: request.toolCallId,
1047
1213
  resolve,
1048
1214
  });
1215
+ if (submission) {
1216
+ this.telemetry.capture("approvalRequested", {
1217
+ submissionId: submission.submissionId,
1218
+ toolCallId: request.toolCallId,
1219
+ executionId: request.executionId,
1220
+ toolName: request.toolName,
1221
+ summary: `${request.subagentName} requests ${request.toolName}`,
1222
+ input: request.input,
1223
+ });
1224
+ }
1049
1225
  void this.broadcastApprovals().catch((error) => {
1050
1226
  this.temporaryAgentApprovals.delete(request.executionId);
1051
1227
  reject(error);
@@ -1069,6 +1245,15 @@ export abstract class AgentRuntimeKernel<
1069
1245
  if (!pending) return { ok: false };
1070
1246
  this.temporaryAgentApprovals.delete(executionId);
1071
1247
  pending.resolve({ approved: false, reason });
1248
+ if (pending.submissionId) {
1249
+ this.telemetry.capture("approvalDecided", {
1250
+ submissionId: pending.submissionId!,
1251
+ toolCallId: pending.toolCallId,
1252
+ executionId,
1253
+ decision: "cancelled",
1254
+ reason,
1255
+ });
1256
+ }
1072
1257
  await this.broadcastApprovals();
1073
1258
  return { ok: true };
1074
1259
  }
@@ -1390,7 +1575,7 @@ export abstract class AgentRuntimeKernel<
1390
1575
  ),
1391
1576
  signal,
1392
1577
  submissionId,
1393
- onCompactionPersisted: (event) =>
1578
+ onCompactionPersisted: (event) => {
1394
1579
  this.db.runtimeEvents.insert({
1395
1580
  eventId: event.eventId,
1396
1581
  body: json({
@@ -1411,7 +1596,14 @@ export abstract class AgentRuntimeKernel<
1411
1596
  },
1412
1597
  }),
1413
1598
  createdAt: Date.now(),
1414
- }),
1599
+ });
1600
+ this.telemetry.capture("contextCompacted", {
1601
+ submissionId,
1602
+ compactionId: event.eventId,
1603
+ inputTokens: event.usage.input,
1604
+ outputTokens: event.usage.output,
1605
+ });
1606
+ },
1415
1607
  });
1416
1608
  await this.drainRuntimeEvents();
1417
1609
  return compacted;
@@ -1631,6 +1823,14 @@ export abstract class AgentRuntimeKernel<
1631
1823
  createdAt,
1632
1824
  });
1633
1825
  }
1826
+ this.telemetry.capture("turnAdmitted", {
1827
+ submissionId,
1828
+ requestId: admissionRequestId,
1829
+ ...(admissionIdempotencyKey
1830
+ ? { idempotencyKey: admissionIdempotencyKey }
1831
+ : {}),
1832
+ occurredAt: createdAt,
1833
+ });
1634
1834
  if (hook) this.admissionsInFlight.delete(admissionKey);
1635
1835
  return this.readSubmission(submissionId)!;
1636
1836
  };
@@ -1840,12 +2040,21 @@ export abstract class AgentRuntimeKernel<
1840
2040
  kind: "record-tool-input",
1841
2041
  record: input,
1842
2042
  });
1843
- const appended = this.db.transaction(() =>
1844
- this.applyPiRecoveryMutations(
2043
+ const appended = this.db.transaction(() => {
2044
+ const inserted = this.applyPiRecoveryMutations(
1845
2045
  submission,
1846
2046
  decision.mutations,
1847
- ),
1848
- );
2047
+ );
2048
+ if (inserted) {
2049
+ this.telemetry.capture("toolStarted", {
2050
+ submissionId: submission.submissionId,
2051
+ toolCallId: input.toolCallId,
2052
+ toolName: input.toolName,
2053
+ input: input.input,
2054
+ });
2055
+ }
2056
+ return inserted;
2057
+ });
1849
2058
  if (appended) this.announceToolStart(submission.submissionId, input);
1850
2059
  return appended;
1851
2060
  }
@@ -1934,6 +2143,26 @@ export abstract class AgentRuntimeKernel<
1934
2143
  body: json({ type: "tool-settlement", event }),
1935
2144
  createdAt,
1936
2145
  });
2146
+ const details = call.result.details as {
2147
+ kind?: string;
2148
+ bytes?: number;
2149
+ } | undefined;
2150
+ this.telemetry.capture("toolFinished", {
2151
+ submissionId,
2152
+ toolCallId: call.toolCallId,
2153
+ toolName: call.toolName,
2154
+ outcome: call.isError ? "failed" : "completed",
2155
+ outputBytes: details?.kind === "artifact_ref" &&
2156
+ typeof details.bytes === "number"
2157
+ ? details.bytes
2158
+ : result.length,
2159
+ spilled: details?.kind === "artifact_ref",
2160
+ output: call.result,
2161
+ ...(call.isError && call.cause
2162
+ ? { error: errorText(call.cause) }
2163
+ : {}),
2164
+ occurredAt: createdAt,
2165
+ });
1937
2166
  }
1938
2167
  const decision = this.decidePiRecovery(submission, {
1939
2168
  kind: "record-tool-result",
@@ -2030,6 +2259,12 @@ export abstract class AgentRuntimeKernel<
2030
2259
  ...(message === undefined ? {} : { message }),
2031
2260
  });
2032
2261
  this.applyPiRecoveryMutations(submission, decision.mutations);
2262
+ if (outcome === "aborted") {
2263
+ this.telemetry.capture("turnCancelled", {
2264
+ submissionId: submission.submissionId,
2265
+ ...(message ? { reason: message } : {}),
2266
+ });
2267
+ }
2033
2268
  }
2034
2269
 
2035
2270
  // 作用:把 Turn 的权威结果一次提交到 Submission、恢复里程碑和聊天续传标记。
@@ -2071,6 +2306,12 @@ export abstract class AgentRuntimeKernel<
2071
2306
  : durableIntent?.outcome ?? outcome;
2072
2307
  const effectiveMessage =
2073
2308
  latest.abortReason ?? durableIntent?.message ?? message;
2309
+ const turnOutput = (await this.transcript.storedMessages())
2310
+ .filter((entry) =>
2311
+ entry.submissionId === latest.submissionId &&
2312
+ entry.message.role === "assistant"
2313
+ )
2314
+ .at(-1)?.message;
2074
2315
  const terminal = this.db.transaction(() => {
2075
2316
  this.appendTerminalIntent(
2076
2317
  latest,
@@ -2126,6 +2367,21 @@ export abstract class AgentRuntimeKernel<
2126
2367
  latest,
2127
2368
  committed.mutations,
2128
2369
  );
2370
+ this.telemetry.capture("turnFinished", {
2371
+ submissionId: latest.submissionId,
2372
+ outcome: effectiveOutcome === "succeeded"
2373
+ ? "completed"
2374
+ : effectiveOutcome === "aborted"
2375
+ ? "cancelled"
2376
+ : "failed",
2377
+ durationMs: Math.max(
2378
+ 0,
2379
+ (terminal.completedAt ?? Date.now()) - latest.createdAt,
2380
+ ),
2381
+ ...(turnOutput === undefined ? {} : { output: turnOutput }),
2382
+ ...(effectiveMessage ? { error: effectiveMessage } : {}),
2383
+ occurredAt: terminal.completedAt ?? Date.now(),
2384
+ });
2129
2385
  return terminal;
2130
2386
  });
2131
2387
  await this.drainRuntimeEvents();
@@ -2240,12 +2496,18 @@ export abstract class AgentRuntimeKernel<
2240
2496
  submission: StoredSubmission,
2241
2497
  ): Promise<StoredSubmission> {
2242
2498
  if (!submission.queuedInputJson) {
2243
- this.db.submissions.transition(
2499
+ const started = this.db.submissions.transition(
2244
2500
  submission.submissionId,
2245
2501
  "running",
2246
2502
  ["pending"],
2247
2503
  );
2248
2504
  const activated = this.readSubmission(submission.submissionId)!;
2505
+ if (started) {
2506
+ this.telemetry.capture("turnStarted", {
2507
+ submissionId: activated.submissionId,
2508
+ assemblyRevision: activated.assemblyRevision,
2509
+ });
2510
+ }
2249
2511
  await this.broadcastApprovals();
2250
2512
  return activated;
2251
2513
  }
@@ -2328,7 +2590,13 @@ export abstract class AgentRuntimeKernel<
2328
2590
  this.db.submissions.clearQueuedPayload(
2329
2591
  submission.submissionId,
2330
2592
  );
2331
- return this.readSubmission(submission.submissionId)!;
2593
+ const started = this.readSubmission(submission.submissionId)!;
2594
+ this.telemetry.capture("turnStarted", {
2595
+ submissionId: started.submissionId,
2596
+ assemblyRevision: started.assemblyRevision,
2597
+ input: userMessage,
2598
+ });
2599
+ return started;
2332
2600
  });
2333
2601
  this.broadcast(json({
2334
2602
  type: MessageType.CF_AGENT_CHAT_MESSAGES,
@@ -2360,6 +2628,11 @@ export abstract class AgentRuntimeKernel<
2360
2628
  );
2361
2629
  }
2362
2630
  if (recovery) {
2631
+ this.telemetry.capture("turnRecovering", {
2632
+ submissionId,
2633
+ reason: submission.recoveryReason ?? "runtime_restart",
2634
+ attempt: Math.max(1, submission.recoveryErrorCount),
2635
+ });
2363
2636
  this.db.submissions.clearRecoveryReason(submissionId);
2364
2637
  await this.broadcastApprovals();
2365
2638
  await this.ensureRuntimeReady();
@@ -2471,7 +2744,12 @@ export abstract class AgentRuntimeKernel<
2471
2744
  );
2472
2745
  }
2473
2746
  if (ready.status === "pending") {
2474
- this.db.submissions.transition(submissionId, "running", ["pending"]);
2747
+ if (this.db.submissions.transition(submissionId, "running", ["pending"])) {
2748
+ this.telemetry.capture("turnStarted", {
2749
+ submissionId,
2750
+ assemblyRevision: ready.assemblyRevision,
2751
+ });
2752
+ }
2475
2753
  }
2476
2754
  const storedMessages = await this.transcript.storedMessages();
2477
2755
  const assistantOrdinal = storedMessages.filter(
@@ -2563,10 +2841,17 @@ export abstract class AgentRuntimeKernel<
2563
2841
  latest.abortReason,
2564
2842
  );
2565
2843
  }
2844
+ if (!streamId) {
2845
+ throw new Error("Yielded SpringBrand Turn has no recoverable stream");
2846
+ }
2566
2847
  await this.schedule(
2567
2848
  0,
2568
2849
  "_piPlannedContinuation",
2569
- { submissionId, requestId: submission.requestId },
2850
+ {
2851
+ submissionId,
2852
+ requestId: submission.requestId,
2853
+ continuationId: streamId,
2854
+ },
2570
2855
  { idempotent: true },
2571
2856
  );
2572
2857
  return latest;
@@ -3249,16 +3534,24 @@ export abstract class AgentRuntimeKernel<
3249
3534
  message: "The current Turn is waiting for approval",
3250
3535
  };
3251
3536
  }
3537
+ const steerId = crypto.randomUUID();
3252
3538
  const inserted = this.db.transaction(() => {
3253
3539
  if (this.db.steers.findByMessageId(message.id)) return false;
3254
3540
  this.db.steers.insert({
3255
- steerId: crypto.randomUUID(),
3541
+ steerId,
3256
3542
  submissionId: target.submissionId,
3257
3543
  messageId: message.id,
3258
3544
  canonicalJson: json(userMessage),
3259
3545
  uiMessageJson: json(message),
3260
3546
  createdAt: userMessage.timestamp,
3261
3547
  });
3548
+ this.telemetry.capture("turnSteered", {
3549
+ submissionId: target.submissionId,
3550
+ steerId,
3551
+ messageId: message.id,
3552
+ content: userMessage,
3553
+ occurredAt: userMessage.timestamp,
3554
+ });
3262
3555
  return true;
3263
3556
  });
3264
3557
  if (inserted) {
@@ -3376,6 +3669,7 @@ export abstract class AgentRuntimeKernel<
3376
3669
  };
3377
3670
  }
3378
3671
 
3672
+ const steerId = crypto.randomUUID();
3379
3673
  const moved = this.db.transaction(() => {
3380
3674
  const latest = this.readSubmission(submissionId);
3381
3675
  const current = this.db.submissions.findRunning() ??
@@ -3389,13 +3683,20 @@ export abstract class AgentRuntimeKernel<
3389
3683
  return false;
3390
3684
  }
3391
3685
  this.db.steers.insert({
3392
- steerId: crypto.randomUUID(),
3686
+ steerId,
3393
3687
  submissionId: target.submissionId,
3394
3688
  messageId: queued.userMessageId!,
3395
3689
  canonicalJson: queued.queuedInputJson!,
3396
3690
  uiMessageJson: queued.queuedUiMessageJson ?? null,
3397
3691
  createdAt: queued.createdAt,
3398
3692
  });
3693
+ this.telemetry.capture("turnSteered", {
3694
+ submissionId: target.submissionId,
3695
+ steerId,
3696
+ messageId: queued.userMessageId!,
3697
+ content: message,
3698
+ occurredAt: queued.createdAt,
3699
+ });
3399
3700
  const reason = "Moved to active Turn";
3400
3701
  this.appendTerminalIntent(latest, "aborted", reason);
3401
3702
  this.db.submissions.updateAbortReason(submissionId, reason);
@@ -3620,6 +3921,19 @@ export abstract class AgentRuntimeKernel<
3620
3921
  ? { approved: true }
3621
3922
  : { approved: false, reason: decision.reason },
3622
3923
  );
3924
+ if (temporary.submissionId) {
3925
+ this.telemetry.capture("approvalDecided", {
3926
+ submissionId: temporary.submissionId!,
3927
+ toolCallId: temporary.toolCallId,
3928
+ executionId,
3929
+ decision: decision.decision === "allow_once"
3930
+ ? "approved"
3931
+ : "rejected",
3932
+ ...(decision.decision === "deny" && decision.reason
3933
+ ? { reason: decision.reason }
3934
+ : {}),
3935
+ });
3936
+ }
3623
3937
  await this.broadcastApprovals();
3624
3938
  return { ok: true };
3625
3939
  }
@@ -3733,6 +4047,15 @@ export abstract class AgentRuntimeKernel<
3733
4047
  const metadata = runtimeSubagentRunMetadata(run.inputPreview);
3734
4048
  if (metadata) {
3735
4049
  await this.subagentLifecycle(metadata).register(run.runId);
4050
+ this.telemetry.capture("subagentStarted", {
4051
+ submissionId: metadata.submissionId,
4052
+ ...(run.parentToolCallId
4053
+ ? { toolCallId: run.parentToolCallId }
4054
+ : {}),
4055
+ subagentId: run.runId,
4056
+ agentType: run.agentType,
4057
+ occurredAt: run.startedAt,
4058
+ });
3736
4059
  }
3737
4060
  await this.broadcastApprovals();
3738
4061
  }
@@ -3777,6 +4100,25 @@ export abstract class AgentRuntimeKernel<
3777
4100
  status,
3778
4101
  zeroUsage: !hasUsage,
3779
4102
  });
4103
+ this.telemetry.capture("subagentFinished", {
4104
+ submissionId: metadata.submissionId,
4105
+ ...(run.parentToolCallId
4106
+ ? { toolCallId: run.parentToolCallId }
4107
+ : {}),
4108
+ subagentId: run.runId,
4109
+ outcome: status === "completed"
4110
+ ? "completed"
4111
+ : status === "aborted" || status === "skipped"
4112
+ ? "cancelled"
4113
+ : "failed",
4114
+ durationMs: Math.max(
4115
+ 0,
4116
+ (run.completedAt ?? Date.now()) - run.startedAt,
4117
+ ),
4118
+ ...(completion ? { output: completion.output } : {}),
4119
+ ...(result.error ? { error: result.error } : {}),
4120
+ occurredAt: run.completedAt ?? Date.now(),
4121
+ });
3780
4122
  }
3781
4123
  }
3782
4124
  await this.broadcastApprovals();