@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
@@ -48,6 +48,11 @@ export interface StoredSubmission {
48
48
  regenerateMessageId: string | null;
49
49
  recoveryErrorCount: number;
50
50
  recoveryReason: SubmissionRecoveryReason | null;
51
+ runId: string | null;
52
+ accountId: string | null;
53
+ rateVersion: number | null;
54
+ slotIdentity: string | null;
55
+ retryable?: boolean;
51
56
  }
52
57
 
53
58
  export interface NewSubmission {
@@ -62,6 +67,10 @@ export interface NewSubmission {
62
67
  queuedUiMessageJson?: string | null;
63
68
  userMessageId?: string | null;
64
69
  regenerateMessageId?: string | null;
70
+ runId?: string | null;
71
+ accountId?: string | null;
72
+ rateVersion?: number | null;
73
+ slotIdentity?: string | null;
65
74
  }
66
75
 
67
76
  type SubmissionRow = {
@@ -83,6 +92,11 @@ type SubmissionRow = {
83
92
  regenerate_message_id: string | null;
84
93
  recovery_error_count: number;
85
94
  recovery_reason: string | null;
95
+ run_id: string | null;
96
+ account_id: string | null;
97
+ rate_version: number | null;
98
+ slot_identity: string | null;
99
+ admission_retryable: number | null;
86
100
  };
87
101
 
88
102
  // #endregion
@@ -110,6 +124,13 @@ function mapRow(row: SubmissionRow): StoredSubmission {
110
124
  regenerateMessageId: row.regenerate_message_id,
111
125
  recoveryErrorCount: row.recovery_error_count,
112
126
  recoveryReason: row.recovery_reason as SubmissionRecoveryReason | null,
127
+ runId: row.run_id,
128
+ accountId: row.account_id,
129
+ rateVersion: row.rate_version,
130
+ slotIdentity: row.slot_identity,
131
+ retryable: row.admission_retryable === null
132
+ ? undefined
133
+ : row.admission_retryable === 1,
113
134
  };
114
135
  }
115
136
 
@@ -130,7 +151,9 @@ export class SubmissionRepository {
130
151
  error, created_at, completed_at, assembly_revision,
131
152
  assembly_descriptor, assistant_message_id, abort_reason,
132
153
  queued_input_json, queued_ui_message_json, user_message_id,
133
- regenerate_message_id, recovery_error_count, recovery_reason
154
+ regenerate_message_id, recovery_error_count, recovery_reason,
155
+ run_id, account_id, rate_version, slot_identity,
156
+ admission_retryable
134
157
  FROM pi_submissions
135
158
  WHERE submission_id = ${id}
136
159
  `[0];
@@ -230,7 +253,8 @@ export class SubmissionRepository {
230
253
  submission_id, request_id, idempotency_key, status, accepted,
231
254
  created_at, assembly_revision, assembly_descriptor,
232
255
  assistant_message_id, queued_input_json, queued_ui_message_json,
233
- user_message_id, regenerate_message_id
256
+ user_message_id, regenerate_message_id,
257
+ run_id, account_id, rate_version, slot_identity
234
258
  ) VALUES (
235
259
  ${s.submissionId},
236
260
  ${s.requestId},
@@ -244,7 +268,34 @@ export class SubmissionRepository {
244
268
  ${s.queuedInputJson ?? null},
245
269
  ${s.queuedUiMessageJson ?? null},
246
270
  ${s.userMessageId ?? null},
247
- ${s.regenerateMessageId ?? null}
271
+ ${s.regenerateMessageId ?? null},
272
+ ${s.runId ?? s.submissionId},
273
+ ${s.accountId ?? null},
274
+ ${s.rateVersion ?? null},
275
+ ${s.slotIdentity ?? null}
276
+ )
277
+ `;
278
+ }
279
+
280
+ insertRejected(s: {
281
+ submissionId: string;
282
+ requestId: string;
283
+ idempotencyKey: string | null;
284
+ createdAt: number;
285
+ code: string;
286
+ retryable: boolean;
287
+ }): void {
288
+ this.sql`
289
+ INSERT INTO pi_submissions (
290
+ submission_id, request_id, idempotency_key, status, accepted,
291
+ error, created_at, completed_at, assembly_revision,
292
+ assembly_descriptor, assistant_message_id, abort_reason,
293
+ recovery_error_count, recovery_reason, run_id, account_id,
294
+ rate_version, slot_identity, admission_retryable
295
+ ) VALUES (
296
+ ${s.submissionId}, ${s.requestId}, ${s.idempotencyKey}, 'skipped', 0,
297
+ ${s.code}, ${s.createdAt}, ${s.createdAt}, '', '', '', NULL,
298
+ 0, NULL, ${s.submissionId}, NULL, NULL, NULL, ${s.retryable ? 1 : 0}
248
299
  )
249
300
  `;
250
301
  }
package/src/index.ts CHANGED
@@ -50,6 +50,7 @@ export * from "./kernel/runtime-config";
50
50
  export type { RuntimeAssemblyView } from "./kernel/runtime-assembly-view";
51
51
  export * from "./runtime-agent";
52
52
  export * from "./kernel/state";
53
+ export * from "./workspace-versioning";
53
54
  export * from "./lib/execution-level";
54
55
  export type {
55
56
  RuntimeAssemblyInput,
@@ -119,8 +120,10 @@ export {
119
120
  } from "./pi/tool";
120
121
  export {
121
122
  createSandboxTools,
123
+ createWorkspaceRevisionTools,
122
124
  createWorkspaceTools,
123
125
  sandboxPiToolCandidates,
126
+ workspaceRevisionPiToolCandidate,
124
127
  workspacePiToolCandidates,
125
128
  } from "./pi/tool";
126
129
  export {
@@ -2,6 +2,23 @@ import type { SkillSource } from "agents/skills";
2
2
  import type { ScheduleSpec } from "./receipts";
3
3
  import type { RuntimeActivityProjection } from "./state";
4
4
  import type { ExecutionLevel } from "../lib/execution-level";
5
+ import type {
6
+ RuntimeEventConfirmation,
7
+ RuntimeLifecycleFact,
8
+ RuntimeSubagentUsageEvent,
9
+ RuntimeSubagentUsageMetrics,
10
+ } from "./submission-authority";
11
+ export type {
12
+ RuntimeEventConfirmation,
13
+ RuntimeLifecycleFact,
14
+ RuntimeSubagentLifecyclePort,
15
+ RuntimeSubagentUsageEvent,
16
+ RuntimeSubagentUsageMetrics,
17
+ RuntimeSubmissionAdmissionHook,
18
+ RuntimeSubmissionAdmissionInput,
19
+ RuntimeSubmissionAdmissionResult,
20
+ RuntimeSubmissionAdmissionContext,
21
+ } from "./submission-authority";
5
22
 
6
23
  // #region Workspace 端口
7
24
 
@@ -194,6 +211,7 @@ export interface WorkspacePort {
194
211
  * Workspace;Tool Surface 只把这个已授权 Port 转成 `execute` Tool。
195
212
  */
196
213
  export interface RuntimeCodeExecutionPort {
214
+ readonly description: string;
197
215
  execute(input: { code: string }): Promise<unknown>;
198
216
  }
199
217
 
@@ -654,12 +672,16 @@ export type RuntimeSubagentRunResult =
654
672
  runId: string;
655
673
  output: unknown;
656
674
  error?: undefined;
675
+ usage?: RuntimeSubagentUsageMetrics;
676
+ childStillRunning?: boolean;
657
677
  }
658
678
  | {
659
679
  status: string;
660
680
  runId: string;
661
681
  output?: unknown;
662
682
  error?: string;
683
+ usage?: RuntimeSubagentUsageMetrics;
684
+ childStillRunning?: boolean;
663
685
  };
664
686
 
665
687
  /**
@@ -671,6 +693,15 @@ export type RuntimeSubagentRunResult =
671
693
  * 运行记录、后台通知和实际 Agent 路由由 Host 保持,Runtime 只传入类型与结构化输入。
672
694
  */
673
695
  export interface RuntimeSubagentPort {
696
+ reserve?(
697
+ agentType: string,
698
+ input: Readonly<Record<string, unknown>>,
699
+ options?: Readonly<{
700
+ parentRunId?: string;
701
+ submissionId?: string;
702
+ toolCallId?: string;
703
+ }>,
704
+ ): Promise<{ readonly runId: string }>;
674
705
  /**
675
706
  * 启动一次 Subagent 运行并返回当前结果。
676
707
  *
@@ -686,6 +717,12 @@ export interface RuntimeSubagentPort {
686
717
  detached?: boolean;
687
718
  maxBudgetMs?: number;
688
719
  notifySource?: string;
720
+ subagentRunId?: string;
721
+ parentRunId?: string;
722
+ submissionId?: string;
723
+ accountId?: string;
724
+ rateVersion?: number;
725
+ slotIdentity?: string;
689
726
  }>,
690
727
  ): Promise<RuntimeSubagentRunResult>;
691
728
  }
@@ -706,6 +743,10 @@ export interface RuntimeTurnMessage {
706
743
  export interface RuntimeModelUsageEvent {
707
744
  readonly eventId: string;
708
745
  readonly submissionId: string;
746
+ readonly runId?: string;
747
+ readonly accountId?: string;
748
+ readonly rateVersion?: number;
749
+ readonly slotIdentity?: string;
709
750
  readonly kind: "assistant" | "compaction";
710
751
  readonly api: string;
711
752
  readonly provider: string;
@@ -777,6 +818,14 @@ export interface RuntimeTurnEventsPort {
777
818
  ): Promise<void>;
778
819
  /** 投递 Assistant 或 Compaction 的原始 provider usage。 */
779
820
  onModelUsage?(event: RuntimeModelUsageEvent): Promise<void>;
821
+ /** Durable SubAgent usage callback. Runtime keeps the event pending until confirmation. */
822
+ onSubagentUsage?(
823
+ event: RuntimeSubagentUsageEvent,
824
+ ): Promise<RuntimeEventConfirmation | void>;
825
+ /** Durable lifecycle callback. Runtime keeps the fact pending until confirmation. */
826
+ onLifecycleFact?(
827
+ event: RuntimeLifecycleFact,
828
+ ): Promise<RuntimeEventConfirmation | void>;
780
829
  /**
781
830
  * 投递一次 Tool 开始执行的事件,带上它的参数。
782
831
  *
@@ -863,6 +912,8 @@ export interface RuntimeModelEndpoint {
863
912
  headers?: Readonly<Record<string, string>>;
864
913
  baseURL: string;
865
914
  models: readonly string[];
915
+ /** OpenRouter upstream provider pins keyed by model ID; pinned requests never fall back. */
916
+ openRouterProviderPins?: Readonly<Record<string, string>>;
866
917
  }
867
918
 
868
919
  /**
@@ -977,6 +1028,7 @@ export interface RuntimeBindings {
977
1028
  gateway?: RuntimeGatewayPort;
978
1029
  workspace?: WorkspacePort;
979
1030
  memory?: RuntimeMemoryPort;
1031
+ subagents?: RuntimeSubagentPort;
980
1032
  skills: RuntimeSkillBindings;
981
1033
  turnEvents?: RuntimeTurnEventsPort;
982
1034
  }
@@ -0,0 +1,100 @@
1
+ import type {
2
+ RuntimeEventConfirmation,
3
+ RuntimeLifecycleFact,
4
+ RuntimeSubagentUsageEvent,
5
+ } from "./submission-authority";
6
+
7
+ export interface RuntimeLifecycleSink {
8
+ save(
9
+ event: RuntimeLifecycleFact | RuntimeSubagentUsageEvent,
10
+ ): void | Promise<void>;
11
+ has(eventId: string): boolean;
12
+ hasSubagentUsage(submissionId: string, subagentRunId: string): boolean;
13
+ }
14
+
15
+ /**
16
+ * Small ordering guard used by Host adapters while they persist child facts.
17
+ * The sink is the Runtime outbox; this class owns no prompt or billing state.
18
+ */
19
+ export class RuntimeSubagentLifecycleRecorder {
20
+ constructor(
21
+ private readonly input: {
22
+ submissionId: string;
23
+ parentRunId: string;
24
+ accountId?: string;
25
+ rateVersion?: number;
26
+ slotIdentity?: string;
27
+ },
28
+ private readonly sink: RuntimeLifecycleSink,
29
+ ) {}
30
+
31
+ async register(subagentRunId: string): Promise<RuntimeEventConfirmation> {
32
+ if (!subagentRunId.trim()) throw new Error("SubAgent run ID is required");
33
+ const eventId = `${subagentRunId}:registered`;
34
+ if (this.sink.has(eventId)) return { confirmed: true };
35
+ if (this.sink.has(`${this.input.submissionId}:terminal`)) {
36
+ throw new Error("SubAgent registration is closed");
37
+ }
38
+ await this.sink.save({
39
+ kind: "subagent-run-registered",
40
+ eventId,
41
+ submissionId: this.input.submissionId,
42
+ subagentRunId,
43
+ parentRunId: this.input.parentRunId,
44
+ ...(this.input.accountId ? { accountId: this.input.accountId } : {}),
45
+ ...(this.input.rateVersion !== undefined ? { rateVersion: this.input.rateVersion } : {}),
46
+ ...(this.input.slotIdentity ? { slotIdentity: this.input.slotIdentity } : {}),
47
+ });
48
+ return { confirmed: true };
49
+ }
50
+
51
+ async usage(event: RuntimeSubagentUsageEvent): Promise<RuntimeEventConfirmation> {
52
+ if (
53
+ event.submissionId !== this.input.submissionId ||
54
+ event.parentRunId !== this.input.parentRunId
55
+ ) {
56
+ throw new Error("SubAgent usage identity does not match its lifecycle");
57
+ }
58
+ if (!this.sink.has(`${event.subagentRunId}:registered`)) {
59
+ throw new Error("SubAgent usage is outside its lifecycle");
60
+ }
61
+ if (
62
+ this.sink.has(`${event.subagentRunId}:terminal`) &&
63
+ !this.sink.has(event.eventId)
64
+ ) {
65
+ throw new Error("SubAgent usage is outside its lifecycle");
66
+ }
67
+ await this.sink.save(event);
68
+ return { confirmed: true };
69
+ }
70
+
71
+ async terminal(input: {
72
+ subagentRunId: string;
73
+ status: "completed" | "failed" | "aborted" | "skipped";
74
+ zeroUsage?: boolean;
75
+ }): Promise<RuntimeEventConfirmation> {
76
+ if (!this.sink.has(`${input.subagentRunId}:registered`)) {
77
+ throw new Error("SubAgent is not registered");
78
+ }
79
+ const usageSaved = this.sink.hasSubagentUsage(
80
+ this.input.submissionId,
81
+ input.subagentRunId,
82
+ );
83
+ if (!usageSaved && !input.zeroUsage) {
84
+ throw new Error("SubAgent terminal requires durable usage or zero usage");
85
+ }
86
+ await this.sink.save({
87
+ kind: "subagent-terminal",
88
+ eventId: `${input.subagentRunId}:terminal`,
89
+ submissionId: this.input.submissionId,
90
+ subagentRunId: input.subagentRunId,
91
+ parentRunId: this.input.parentRunId,
92
+ status: input.status,
93
+ zeroUsage: input.zeroUsage ?? !usageSaved,
94
+ ...(this.input.accountId ? { accountId: this.input.accountId } : {}),
95
+ ...(this.input.rateVersion !== undefined ? { rateVersion: this.input.rateVersion } : {}),
96
+ ...(this.input.slotIdentity ? { slotIdentity: this.input.slotIdentity } : {}),
97
+ });
98
+ return { confirmed: true };
99
+ }
100
+ }
@@ -17,3 +17,14 @@ export * from "../pi/message";
17
17
  export * from "./receipts";
18
18
  export * from "./state";
19
19
  export * from "../lib/execution-level";
20
+ export type {
21
+ RuntimeEventConfirmation,
22
+ RuntimeLifecycleFact,
23
+ RuntimeSubagentLifecyclePort,
24
+ RuntimeSubagentUsageEvent,
25
+ RuntimeSubagentUsageMetrics,
26
+ RuntimeSubmissionAdmissionHook,
27
+ RuntimeSubmissionAdmissionInput,
28
+ RuntimeSubmissionAdmissionResult,
29
+ RuntimeSubmissionAdmissionContext,
30
+ } from "./submission-authority";
@@ -86,6 +86,7 @@ export interface SubmissionReceipt {
86
86
  status: string;
87
87
  accepted: boolean;
88
88
  error?: string;
89
+ retryable?: boolean;
89
90
  createdAt: number;
90
91
  completedAt?: number;
91
92
  }
@@ -453,12 +453,6 @@ export abstract class RecoverableChatAgent<
453
453
  connection: Connection,
454
454
  probeId?: string,
455
455
  ): Promise<void> {
456
- console.info("[DEBUG-ws-old-9f3a] resume-request", {
457
- connectionId: connection.id,
458
- probeId,
459
- activeRequestId: this.resumableStream.activeRequestId,
460
- pending: [...this.pendingResumeConnections],
461
- });
462
456
  return this.resumeHandshake.handleResumeRequest(connection, probeId);
463
457
  }
464
458
 
@@ -474,12 +468,6 @@ export abstract class RecoverableChatAgent<
474
468
  connection: Connection,
475
469
  requestId: string,
476
470
  ): Promise<void> {
477
- console.info("[DEBUG-ws-old-9f3a] resume-ack", {
478
- connectionId: connection.id,
479
- requestId,
480
- activeRequestId: this.resumableStream.activeRequestId,
481
- pending: [...this.pendingResumeConnections],
482
- });
483
471
  return this.resumeHandshake.handleResumeAck(connection, requestId);
484
472
  }
485
473
 
@@ -504,13 +492,6 @@ export abstract class RecoverableChatAgent<
504
492
  readonly continuation?: boolean;
505
493
  } = {},
506
494
  ): void {
507
- console.info("[DEBUG-ws-old-9f3a] broadcast", {
508
- requestId,
509
- done,
510
- activeRequestId: this.resumableStream.activeRequestId,
511
- pending: [...this.pendingResumeConnections],
512
- connections: Array.from(this.getConnections(), ({ id }) => id),
513
- });
514
495
  this.broadcast(
515
496
  json({
516
497
  type: MessageType.CF_AGENT_USE_CHAT_RESPONSE,
@@ -0,0 +1,137 @@
1
+ import type { RuntimeSubagentUsageMetrics } from "./submission-authority";
2
+
3
+ export interface RuntimeSubagentRunMetadata {
4
+ readonly submissionId: string;
5
+ readonly parentRunId: string;
6
+ readonly accountId?: string;
7
+ readonly rateVersion?: number;
8
+ readonly slotIdentity?: string;
9
+ }
10
+
11
+ export function emptySubagentUsage(): RuntimeSubagentUsageMetrics {
12
+ return {
13
+ input: 0,
14
+ output: 0,
15
+ cacheRead: 0,
16
+ cacheWrite: 0,
17
+ cacheWrite1h: 0,
18
+ reasoning: 0,
19
+ totalTokens: 0,
20
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
21
+ };
22
+ }
23
+
24
+ export function isZeroSubagentUsage(
25
+ usage: RuntimeSubagentUsageMetrics,
26
+ ): boolean {
27
+ return usage.totalTokens === 0 && usage.cost.total === 0;
28
+ }
29
+
30
+ export function subagentTerminalStatus(
31
+ status: string,
32
+ ): "completed" | "failed" | "aborted" | "skipped" {
33
+ if (status === "completed" || status === "aborted" || status === "skipped") {
34
+ return status;
35
+ }
36
+ return "failed";
37
+ }
38
+
39
+ export function addSubagentUsage(
40
+ total: RuntimeSubagentUsageMetrics,
41
+ usage: RuntimeSubagentUsageMetrics,
42
+ ): void {
43
+ const target = total as {
44
+ -readonly [Key in keyof RuntimeSubagentUsageMetrics]: RuntimeSubagentUsageMetrics[Key];
45
+ };
46
+ const cost = total.cost as {
47
+ -readonly [Key in keyof RuntimeSubagentUsageMetrics["cost"]]: number;
48
+ };
49
+ target.input += usage.input;
50
+ target.output += usage.output;
51
+ target.cacheRead += usage.cacheRead;
52
+ target.cacheWrite += usage.cacheWrite;
53
+ target.cacheWrite1h = (target.cacheWrite1h ?? 0) + (usage.cacheWrite1h ?? 0);
54
+ target.reasoning = (target.reasoning ?? 0) + (usage.reasoning ?? 0);
55
+ target.totalTokens += usage.totalTokens;
56
+ cost.input += usage.cost.input;
57
+ cost.output += usage.cost.output;
58
+ cost.cacheRead += usage.cost.cacheRead;
59
+ cost.cacheWrite += usage.cost.cacheWrite;
60
+ cost.total += usage.cost.total;
61
+ }
62
+
63
+ export function encodeSubagentCompletion(
64
+ output: unknown,
65
+ usage: RuntimeSubagentUsageMetrics,
66
+ ): string {
67
+ return JSON.stringify({ output, usage });
68
+ }
69
+
70
+ export function decodeSubagentCompletion(value: unknown): {
71
+ output: unknown;
72
+ usage: RuntimeSubagentUsageMetrics;
73
+ } | null {
74
+ if (typeof value !== "string") return null;
75
+ try {
76
+ const parsed = JSON.parse(value) as Record<string, unknown>;
77
+ const usage = parsed.usage as Record<string, unknown> | undefined;
78
+ const cost = usage?.cost as Record<string, unknown> | undefined;
79
+ const tokenFields = [
80
+ usage?.input,
81
+ usage?.output,
82
+ usage?.cacheRead,
83
+ usage?.cacheWrite,
84
+ usage?.totalTokens,
85
+ ];
86
+ const costFields = [
87
+ cost?.input,
88
+ cost?.output,
89
+ cost?.cacheRead,
90
+ cost?.cacheWrite,
91
+ cost?.total,
92
+ ];
93
+ if (
94
+ tokenFields.some((item) =>
95
+ !Number.isSafeInteger(item) || (item as number) < 0
96
+ ) ||
97
+ costFields.some((item) =>
98
+ typeof item !== "number" || !Number.isFinite(item) || item < 0
99
+ ) ||
100
+ (usage?.cacheWrite1h !== undefined &&
101
+ (!Number.isSafeInteger(usage.cacheWrite1h) ||
102
+ (usage.cacheWrite1h as number) < 0)) ||
103
+ (usage?.reasoning !== undefined &&
104
+ (!Number.isSafeInteger(usage.reasoning) ||
105
+ (usage.reasoning as number) < 0))
106
+ ) {
107
+ return null;
108
+ }
109
+ return {
110
+ output: parsed.output,
111
+ usage: usage as unknown as RuntimeSubagentUsageMetrics,
112
+ };
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ export function runtimeSubagentRunMetadata(
119
+ value: unknown,
120
+ ): RuntimeSubagentRunMetadata | null {
121
+ if (!value || typeof value !== "object") return null;
122
+ const input = value as Record<string, unknown>;
123
+ if (
124
+ typeof input.submissionId !== "string" || !input.submissionId.trim() ||
125
+ typeof input.parentRunId !== "string" || !input.parentRunId.trim() ||
126
+ (input.accountId !== undefined &&
127
+ (typeof input.accountId !== "string" || !input.accountId.trim())) ||
128
+ (input.slotIdentity !== undefined &&
129
+ (typeof input.slotIdentity !== "string" || !input.slotIdentity.trim())) ||
130
+ (input.rateVersion !== undefined &&
131
+ (!Number.isSafeInteger(input.rateVersion) ||
132
+ (input.rateVersion as number) <= 0))
133
+ ) {
134
+ return null;
135
+ }
136
+ return input as unknown as RuntimeSubagentRunMetadata;
137
+ }
@@ -0,0 +1,114 @@
1
+ export interface RuntimeSubagentUsageMetrics {
2
+ readonly input: number;
3
+ readonly output: number;
4
+ readonly cacheRead: number;
5
+ readonly cacheWrite: number;
6
+ readonly cacheWrite1h?: number;
7
+ readonly reasoning?: number;
8
+ readonly totalTokens: number;
9
+ readonly cost: {
10
+ readonly input: number;
11
+ readonly output: number;
12
+ readonly cacheRead: number;
13
+ readonly cacheWrite: number;
14
+ readonly total: number;
15
+ };
16
+ }
17
+
18
+ export interface RuntimeSubagentUsageEvent {
19
+ readonly eventId: string;
20
+ readonly submissionId: string;
21
+ readonly runId: string;
22
+ readonly subagentRunId: string;
23
+ readonly parentRunId: string;
24
+ readonly accountId?: string;
25
+ readonly rateVersion?: number;
26
+ readonly slotIdentity?: string;
27
+ readonly kind: "subagent";
28
+ readonly status: "completed" | "failed" | "aborted" | "skipped";
29
+ readonly usage: RuntimeSubagentUsageMetrics;
30
+ }
31
+
32
+ export interface RuntimeSubagentLifecyclePort {
33
+ readonly parentRunId: string;
34
+ readonly submissionId: string;
35
+ readonly accountId?: string;
36
+ readonly rateVersion?: number;
37
+ readonly slotIdentity?: string;
38
+ onRegistered(subagentRunId: string): Promise<void> | void;
39
+ onUsage(event: RuntimeSubagentUsageEvent): Promise<void> | void;
40
+ onTerminal(input: {
41
+ readonly subagentRunId: string;
42
+ readonly status: "completed" | "failed" | "aborted" | "skipped";
43
+ readonly zeroUsage?: boolean;
44
+ }): Promise<void> | void;
45
+ }
46
+
47
+ export interface RuntimeSubmissionAdmissionInput {
48
+ readonly requestId: string;
49
+ readonly idempotencyKey?: string;
50
+ readonly runId: string;
51
+ }
52
+
53
+ export interface RuntimeSubmissionAdmissionContext {
54
+ readonly accountId: string;
55
+ readonly rateVersion: number;
56
+ readonly slotIdentity: string;
57
+ }
58
+
59
+ export type RuntimeSubmissionAdmissionResult =
60
+ | ({ readonly accepted: true } & RuntimeSubmissionAdmissionContext)
61
+ | {
62
+ readonly accepted: false;
63
+ readonly code: string;
64
+ readonly retryable?: boolean;
65
+ };
66
+
67
+ export type RuntimeSubmissionAdmissionHook = (
68
+ input: RuntimeSubmissionAdmissionInput,
69
+ ) => Promise<RuntimeSubmissionAdmissionResult>;
70
+
71
+ export type RuntimeLifecycleFact =
72
+ | ({ readonly kind: "submission-admitted" } & RuntimeSubmissionAdmissionContext & {
73
+ readonly eventId: string;
74
+ readonly submissionId: string;
75
+ readonly runId: string;
76
+ })
77
+ | {
78
+ readonly kind: "submission-terminal";
79
+ readonly eventId: string;
80
+ readonly submissionId: string;
81
+ readonly runId: string;
82
+ readonly status: "completed" | "aborted" | "skipped" | "error";
83
+ readonly childRegistrationClosed: true;
84
+ readonly zeroUsage: boolean;
85
+ readonly accountId?: string;
86
+ readonly rateVersion?: number;
87
+ readonly slotIdentity?: string;
88
+ }
89
+ | {
90
+ readonly kind: "subagent-run-registered";
91
+ readonly eventId: string;
92
+ readonly submissionId: string;
93
+ readonly subagentRunId: string;
94
+ readonly parentRunId: string;
95
+ readonly accountId?: string;
96
+ readonly rateVersion?: number;
97
+ readonly slotIdentity?: string;
98
+ }
99
+ | {
100
+ readonly kind: "subagent-terminal";
101
+ readonly eventId: string;
102
+ readonly submissionId: string;
103
+ readonly subagentRunId: string;
104
+ readonly parentRunId: string;
105
+ readonly status: "completed" | "failed" | "aborted" | "skipped";
106
+ readonly zeroUsage: boolean;
107
+ readonly accountId?: string;
108
+ readonly rateVersion?: number;
109
+ readonly slotIdentity?: string;
110
+ };
111
+
112
+ export interface RuntimeEventConfirmation {
113
+ readonly confirmed: true;
114
+ }