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

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.2.0-alpha.68",
3
+ "version": "0.2.0-alpha.70",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -21,8 +21,8 @@
21
21
  "@cloudflare/sandbox": "0.12.4",
22
22
  "@cloudflare/shell": "0.4.3",
23
23
  "@cloudflare/think": "0.17.0",
24
- "@earendil-works/pi-agent-core": "0.83.0",
25
- "@earendil-works/pi-ai": "0.83.0",
24
+ "@earendil-works/pi-agent-core": "0.85.1",
25
+ "@earendil-works/pi-ai": "0.85.1",
26
26
  "@types/lodash-es": "^4.17.12",
27
27
  "cron-schedule": "6.0.0",
28
28
  "isomorphic-git": "1.38.6",
@@ -1,5 +1,7 @@
1
1
  import {
2
- Agent,
2
+ runAgentLoopContinue,
3
+ convertToLlm,
4
+ type AgentMessage,
3
5
  type AgentEvent,
4
6
  type AgentTool,
5
7
  type StreamFn,
@@ -105,18 +107,15 @@ export async function runCloudflareSubAgent(
105
107
  // 这里没有审批闸,也装不了:到这一步工具已经被 compilePiTools 编译成 AgentTool,
106
108
  // requiredExecutionLevel 只挂在编译前的 PiToolCandidate 上,这里读不到。「子 agent 不许挂需审批的工具」
107
109
  // 这条不变量由装配处 createCloudflareSubAgentTools 的启动期断言强制执行。
108
- const agent = new Agent({
109
- streamFn: constrainedStream ?? run.streamFn,
110
- initialState: {
111
- model: run.model,
112
- systemPrompt: assembleSubagentPrompt(run.type.persona),
113
- thinkingLevel: run.type.model === "main" ? "medium" : "off",
114
- messages: [],
115
- tools: run.tools,
116
- },
117
- });
110
+ const controller = new AbortController();
111
+ const signal = run.signal ? AbortSignal.any([run.signal, controller.signal]) : controller.signal;
112
+ const messages: AgentMessage[] = [{
113
+ role: "user",
114
+ content: taskPrompt(input.data, constrainedStream ? undefined : outputSchema),
115
+ timestamp: Date.now(),
116
+ }];
118
117
  let toolFailure: { name: string; error: string } | undefined;
119
- agent.subscribe(async (event, signal) => {
118
+ const emit = async (event: AgentEvent) => {
120
119
  if (event.type === "tool_execution_end" && event.isError) {
121
120
  toolFailure ??= {
122
121
  name: event.toolName,
@@ -124,36 +123,38 @@ export async function runCloudflareSubAgent(
124
123
  };
125
124
  }
126
125
  await run.onEvent?.(event, signal);
127
- });
126
+ };
128
127
 
129
128
  let timedOut = false;
130
- const abort = () => agent.abort();
131
- run.signal?.addEventListener("abort", abort, { once: true });
132
129
  const timeout = setTimeout(() => {
133
130
  timedOut = true;
134
- agent.abort();
131
+ controller.abort();
135
132
  }, run.timeoutMs ?? DEFAULT_TIMEOUT_MS);
136
133
 
134
+ let newMessages: AgentMessage[];
137
135
  try {
138
- await agent.prompt(taskPrompt(
139
- input.data,
140
- constrainedStream ? undefined : outputSchema,
141
- ));
136
+ newMessages = await runAgentLoopContinue(
137
+ { systemPrompt: assembleSubagentPrompt(run.type.persona), messages, tools: [...run.tools] },
138
+ { model: run.model, reasoning: run.type.model === "main" ? "medium" : undefined, toolExecution: "parallel", convertToLlm },
139
+ emit,
140
+ signal,
141
+ constrainedStream ?? run.streamFn,
142
+ );
142
143
  } finally {
143
144
  clearTimeout(timeout);
144
- run.signal?.removeEventListener("abort", abort);
145
145
  }
146
146
 
147
147
  if (timedOut) throw new Error("SubAgent run timed out");
148
148
  if (run.signal?.aborted) throw new Error("SubAgent run aborted");
149
149
 
150
- const assistant = agent.state.messages
150
+ const assistant = newMessages
151
151
  .slice()
152
152
  .reverse()
153
153
  .find(
154
154
  (message): message is AssistantMessage => message.role === "assistant",
155
155
  );
156
156
  if (!assistant) throw new Error("SubAgent produced no structured output");
157
+ if (assistant.stopReason === "deferred") throw new Error("SubAgent deferred responses are not supported");
157
158
  if (assistant.stopReason === "aborted") {
158
159
  throw new Error("SubAgent run aborted");
159
160
  }
@@ -184,7 +184,7 @@ function fetchCandidate(options: {
184
184
 
185
185
  /**
186
186
  * SubAgent 链路上没有审批通道:主链的审批闸在 Runtime 的 execution 适配器里
187
- * (requiresPiToolApproval → park → 等用户裁决),而子 agent 是 `new Agent({ tools })`
187
+ * (requiresPiToolApproval → park → 等用户裁决),而子 Agent 直接调用 `runAgentLoopContinue`
188
188
  * 直接跑,parked 的请求没有任何界面能落地、也没有人能批。
189
189
  *
190
190
  * 只有 `safe` Tool 可以挂到这条没有审批通道的链路;更高档位必须在装配时失败关闭。
@@ -229,13 +229,6 @@ export async function prepareSubagents<Env extends Cloudflare.Env>(
229
229
  inputPreview: {
230
230
  submissionId: options.submissionId,
231
231
  parentRunId: options.parentRunId,
232
- ...(options.accountId ? { accountId: options.accountId } : {}),
233
- ...(options.rateVersion !== undefined
234
- ? { rateVersion: options.rateVersion }
235
- : {}),
236
- ...(options.slotIdentity
237
- ? { slotIdentity: options.slotIdentity }
238
- : {}),
239
232
  },
240
233
  }
241
234
  : {}),
package/src/db/schema.ts CHANGED
@@ -35,15 +35,6 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
35
35
  if (!submissionColumns.has("run_id")) {
36
36
  sql`ALTER TABLE pi_submissions ADD COLUMN run_id TEXT`;
37
37
  }
38
- if (!submissionColumns.has("account_id")) {
39
- sql`ALTER TABLE pi_submissions ADD COLUMN account_id TEXT`;
40
- }
41
- if (!submissionColumns.has("rate_version")) {
42
- sql`ALTER TABLE pi_submissions ADD COLUMN rate_version INTEGER`;
43
- }
44
- if (!submissionColumns.has("slot_identity")) {
45
- sql`ALTER TABLE pi_submissions ADD COLUMN slot_identity TEXT`;
46
- }
47
38
  if (!submissionColumns.has("admission_retryable")) {
48
39
  sql`ALTER TABLE pi_submissions ADD COLUMN admission_retryable INTEGER`;
49
40
  }
@@ -146,9 +137,6 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
146
137
  idempotency_key TEXT UNIQUE,
147
138
  run_id TEXT NOT NULL,
148
139
  status TEXT NOT NULL CHECK (status IN ('pending', 'accepted', 'rejected')),
149
- account_id TEXT,
150
- rate_version INTEGER,
151
- slot_identity TEXT,
152
140
  code TEXT,
153
141
  retryable INTEGER,
154
142
  attempted_at INTEGER NOT NULL DEFAULT 0
@@ -5,9 +5,6 @@ export type StoredSubmissionAdmission = {
5
5
  idempotencyKey: string | null;
6
6
  runId: string;
7
7
  status: "pending" | "accepted" | "rejected";
8
- accountId: string | null;
9
- rateVersion: number | null;
10
- slotIdentity: string | null;
11
8
  code: string | null;
12
9
  retryable: boolean | null;
13
10
  attemptedAt: number;
@@ -18,9 +15,6 @@ type Row = {
18
15
  idempotency_key: string | null;
19
16
  run_id: string;
20
17
  status: StoredSubmissionAdmission["status"];
21
- account_id: string | null;
22
- rate_version: number | null;
23
- slot_identity: string | null;
24
18
  code: string | null;
25
19
  retryable: number | null;
26
20
  attempted_at: number;
@@ -31,9 +25,6 @@ const mapRow = (row: Row): StoredSubmissionAdmission => ({
31
25
  idempotencyKey: row.idempotency_key,
32
26
  runId: row.run_id,
33
27
  status: row.status,
34
- accountId: row.account_id,
35
- rateVersion: row.rate_version,
36
- slotIdentity: row.slot_identity,
37
28
  code: row.code,
38
29
  retryable: row.retryable === null ? null : row.retryable === 1,
39
30
  attemptedAt: row.attempted_at,
@@ -44,11 +35,11 @@ export class SubmissionAdmissionRepository {
44
35
 
45
36
  find(requestId: string, idempotencyKey?: string): StoredSubmissionAdmission | null {
46
37
  const byRequest = this.sql<Row>`SELECT request_id, idempotency_key, run_id, status,
47
- account_id, rate_version, slot_identity, code, retryable, attempted_at
38
+ code, retryable, attempted_at
48
39
  FROM pi_submission_admissions WHERE request_id = ${requestId}`[0];
49
40
  if (!idempotencyKey) return byRequest ? mapRow(byRequest) : null;
50
41
  const byKey = this.sql<Row>`SELECT request_id, idempotency_key, run_id, status,
51
- account_id, rate_version, slot_identity, code, retryable, attempted_at
42
+ code, retryable, attempted_at
52
43
  FROM pi_submission_admissions WHERE idempotency_key = ${idempotencyKey}`[0];
53
44
  if (byRequest && byKey && byRequest.run_id !== byKey.run_id) {
54
45
  throw new Error("Conflicting Submission admission identity");
@@ -66,10 +57,8 @@ export class SubmissionAdmissionRepository {
66
57
  const existing = this.find(requestId, idempotencyKey ?? undefined);
67
58
  if (existing) return existing;
68
59
  this.sql`INSERT INTO pi_submission_admissions
69
- (request_id, idempotency_key, run_id, status, account_id,
70
- rate_version, slot_identity, code, retryable, attempted_at)
71
- VALUES (${requestId}, ${idempotencyKey}, ${runId}, 'pending', NULL, NULL,
72
- NULL, NULL, NULL, ${attemptedAt})`;
60
+ (request_id, idempotency_key, run_id, status, code, retryable, attempted_at)
61
+ VALUES (${requestId}, ${idempotencyKey}, ${runId}, 'pending', NULL, NULL, ${attemptedAt})`;
73
62
  return this.find(requestId, idempotencyKey ?? undefined)!;
74
63
  }
75
64
 
@@ -88,13 +77,9 @@ export class SubmissionAdmissionRepository {
88
77
  saveAccepted(
89
78
  requestId: string,
90
79
  runId: string,
91
- accountId: string,
92
- rateVersion: number,
93
- slotIdentity: string,
94
80
  ): void {
95
81
  this.sql`UPDATE pi_submission_admissions
96
- SET run_id = ${runId}, status = 'accepted', account_id = ${accountId},
97
- rate_version = ${rateVersion}, slot_identity = ${slotIdentity}, code = NULL,
82
+ SET run_id = ${runId}, status = 'accepted', code = NULL,
98
83
  retryable = NULL
99
84
  WHERE request_id = ${requestId} AND run_id = ${runId} AND status = 'pending'`;
100
85
  }
@@ -51,9 +51,6 @@ export interface StoredSubmission {
51
51
  recoveryErrorCount: number;
52
52
  recoveryReason: SubmissionRecoveryReason | null;
53
53
  runId: string | null;
54
- accountId: string | null;
55
- rateVersion: number | null;
56
- slotIdentity: string | null;
57
54
  retryable?: boolean;
58
55
  /** 跨执行片累计的模型回合数,全局回合预算的唯一事实来源。 */
59
56
  modelTurns: number;
@@ -74,9 +71,6 @@ export interface NewSubmission {
74
71
  userMessageId?: string | null;
75
72
  regenerateMessageId?: string | null;
76
73
  runId?: string | null;
77
- accountId?: string | null;
78
- rateVersion?: number | null;
79
- slotIdentity?: string | null;
80
74
  }
81
75
 
82
76
  type SubmissionRow = {
@@ -99,9 +93,6 @@ type SubmissionRow = {
99
93
  recovery_error_count: number;
100
94
  recovery_reason: string | null;
101
95
  run_id: string | null;
102
- account_id: string | null;
103
- rate_version: number | null;
104
- slot_identity: string | null;
105
96
  admission_retryable: number | null;
106
97
  model_turns: number;
107
98
  continuation_id: string | null;
@@ -133,9 +124,6 @@ function mapRow(row: SubmissionRow): StoredSubmission {
133
124
  recoveryErrorCount: row.recovery_error_count,
134
125
  recoveryReason: row.recovery_reason as SubmissionRecoveryReason | null,
135
126
  runId: row.run_id,
136
- accountId: row.account_id,
137
- rateVersion: row.rate_version,
138
- slotIdentity: row.slot_identity,
139
127
  retryable: row.admission_retryable === null
140
128
  ? undefined
141
129
  : row.admission_retryable === 1,
@@ -162,7 +150,7 @@ export class SubmissionRepository {
162
150
  assembly_descriptor, assistant_message_id, abort_reason,
163
151
  queued_input_json, queued_ui_message_json, user_message_id,
164
152
  regenerate_message_id, recovery_error_count, recovery_reason,
165
- run_id, account_id, rate_version, slot_identity,
153
+ run_id,
166
154
  admission_retryable,
167
155
  model_turns, continuation_id
168
156
  FROM pi_submissions
@@ -265,7 +253,7 @@ export class SubmissionRepository {
265
253
  created_at, assembly_revision, assembly_descriptor,
266
254
  assistant_message_id, queued_input_json, queued_ui_message_json,
267
255
  user_message_id, regenerate_message_id,
268
- run_id, account_id, rate_version, slot_identity
256
+ run_id
269
257
  ) VALUES (
270
258
  ${s.submissionId},
271
259
  ${s.requestId},
@@ -280,10 +268,7 @@ export class SubmissionRepository {
280
268
  ${s.queuedUiMessageJson ?? null},
281
269
  ${s.userMessageId ?? null},
282
270
  ${s.regenerateMessageId ?? null},
283
- ${s.runId ?? s.submissionId},
284
- ${s.accountId ?? null},
285
- ${s.rateVersion ?? null},
286
- ${s.slotIdentity ?? null}
271
+ ${s.runId ?? s.submissionId}
287
272
  )
288
273
  `;
289
274
  }
@@ -301,12 +286,11 @@ export class SubmissionRepository {
301
286
  submission_id, request_id, idempotency_key, status, accepted,
302
287
  error, created_at, completed_at, assembly_revision,
303
288
  assembly_descriptor, assistant_message_id, abort_reason,
304
- recovery_error_count, recovery_reason, run_id, account_id,
305
- rate_version, slot_identity, admission_retryable
289
+ recovery_error_count, recovery_reason, run_id, admission_retryable
306
290
  ) VALUES (
307
291
  ${s.submissionId}, ${s.requestId}, ${s.idempotencyKey}, 'skipped', 0,
308
292
  ${s.code}, ${s.createdAt}, ${s.createdAt}, '', '', '', NULL,
309
- 0, NULL, ${s.submissionId}, NULL, NULL, NULL, ${s.retryable ? 1 : 0}
293
+ 0, NULL, ${s.submissionId}, ${s.retryable ? 1 : 0}
310
294
  )
311
295
  `;
312
296
  }
@@ -19,7 +19,6 @@ export type {
19
19
  RuntimeSubmissionAdmissionHook,
20
20
  RuntimeSubmissionAdmissionInput,
21
21
  RuntimeSubmissionAdmissionResult,
22
- RuntimeSubmissionAdmissionContext,
23
22
  } from "./submission-authority";
24
23
 
25
24
  // #region Workspace 端口
@@ -624,6 +623,11 @@ export interface RuntimeGatewayPort {
624
623
  open(runtimeId: string): Promise<RuntimeGatewaySession>;
625
624
  }
626
625
 
626
+ /** Safe catalog from an external Host; credentials and SDK clients stay there. */
627
+ export type RuntimeMcpHost = import("../pi/tool/mcp").PiMcpHost & {
628
+ readonly servers: readonly import("./profile").RuntimeMcpServer[];
629
+ };
630
+
627
631
  /**
628
632
  * 描述一条模型可见的定时任务。
629
633
  *
@@ -769,9 +773,6 @@ export interface RuntimeSubagentPort {
769
773
  subagentRunId?: string;
770
774
  parentRunId?: string;
771
775
  submissionId?: string;
772
- accountId?: string;
773
- rateVersion?: number;
774
- slotIdentity?: string;
775
776
  }>,
776
777
  ): Promise<RuntimeSubagentRunResult>;
777
778
  }
@@ -793,13 +794,12 @@ export interface RuntimeModelUsageEvent {
793
794
  readonly eventId: string;
794
795
  readonly submissionId: string;
795
796
  readonly runId?: string;
796
- readonly accountId?: string;
797
- readonly rateVersion?: number;
798
- readonly slotIdentity?: string;
799
797
  readonly kind: "assistant" | "compaction";
800
798
  readonly api: string;
801
799
  readonly provider: string;
800
+ /** Pinned request model ID, used to resolve the configured provider. */
802
801
  readonly model: string;
802
+ /** Uniform provider-reported model, when available; not a validated Rate Card meter. */
803
803
  readonly responseModel?: string;
804
804
  readonly stopReason?: string;
805
805
  readonly usage: {
@@ -1094,6 +1094,7 @@ export interface RuntimeBindings {
1094
1094
  codeExecution?: RuntimeCodeExecutionFactory;
1095
1095
  /** Secret-capability binding; implementations must keep Runtime Grant in closure state. */
1096
1096
  gateway?: RuntimeGatewayPort;
1097
+ mcp?: RuntimeMcpHost;
1097
1098
  workspace?: WorkspacePort;
1098
1099
  memory?: RuntimeMemoryPort;
1099
1100
  subagents?: RuntimeSubagentPort;
@@ -21,9 +21,6 @@ export class RuntimeSubagentLifecycleRecorder {
21
21
  private readonly input: {
22
22
  submissionId: string;
23
23
  parentRunId: string;
24
- accountId?: string;
25
- rateVersion?: number;
26
- slotIdentity?: string;
27
24
  },
28
25
  private readonly sink: RuntimeLifecycleSink,
29
26
  ) {}
@@ -41,9 +38,6 @@ export class RuntimeSubagentLifecycleRecorder {
41
38
  submissionId: this.input.submissionId,
42
39
  subagentRunId,
43
40
  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
41
  });
48
42
  return { confirmed: true };
49
43
  }
@@ -91,9 +85,6 @@ export class RuntimeSubagentLifecycleRecorder {
91
85
  parentRunId: this.input.parentRunId,
92
86
  status: input.status,
93
87
  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
88
  });
98
89
  return { confirmed: true };
99
90
  }
@@ -47,6 +47,9 @@ export interface RuntimeMemoryProfile {
47
47
  export interface RuntimeMcpServer {
48
48
  name: string;
49
49
  url: string;
50
+ id?: string;
51
+ /** Connection lifetime is owned by an external Host, not this Chat's SDK. */
52
+ hostManaged?: boolean;
50
53
  }
51
54
 
52
55
  /**
@@ -27,5 +27,4 @@ export type {
27
27
  RuntimeSubmissionAdmissionHook,
28
28
  RuntimeSubmissionAdmissionInput,
29
29
  RuntimeSubmissionAdmissionResult,
30
- RuntimeSubmissionAdmissionContext,
31
30
  } from "./submission-authority";
@@ -3,9 +3,6 @@ import type { RuntimeSubagentUsageMetrics } from "./submission-authority";
3
3
  export interface RuntimeSubagentRunMetadata {
4
4
  readonly submissionId: string;
5
5
  readonly parentRunId: string;
6
- readonly accountId?: string;
7
- readonly rateVersion?: number;
8
- readonly slotIdentity?: string;
9
6
  }
10
7
 
11
8
  export function emptySubagentUsage(): RuntimeSubagentUsageMetrics {
@@ -122,16 +119,9 @@ export function runtimeSubagentRunMetadata(
122
119
  const input = value as Record<string, unknown>;
123
120
  if (
124
121
  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))
122
+ typeof input.parentRunId !== "string" || !input.parentRunId.trim()
133
123
  ) {
134
124
  return null;
135
125
  }
136
- return input as unknown as RuntimeSubagentRunMetadata;
126
+ return { submissionId: input.submissionId, parentRunId: input.parentRunId };
137
127
  }
@@ -21,9 +21,6 @@ export interface RuntimeSubagentUsageEvent {
21
21
  readonly runId: string;
22
22
  readonly subagentRunId: string;
23
23
  readonly parentRunId: string;
24
- readonly accountId?: string;
25
- readonly rateVersion?: number;
26
- readonly slotIdentity?: string;
27
24
  readonly kind: "subagent";
28
25
  readonly status: "completed" | "failed" | "aborted" | "skipped";
29
26
  readonly usage: RuntimeSubagentUsageMetrics;
@@ -32,9 +29,6 @@ export interface RuntimeSubagentUsageEvent {
32
29
  export interface RuntimeSubagentLifecyclePort {
33
30
  readonly parentRunId: string;
34
31
  readonly submissionId: string;
35
- readonly accountId?: string;
36
- readonly rateVersion?: number;
37
- readonly slotIdentity?: string;
38
32
  onRegistered(subagentRunId: string): Promise<void> | void;
39
33
  onUsage(event: RuntimeSubagentUsageEvent): Promise<void> | void;
40
34
  onTerminal(input: {
@@ -50,14 +44,8 @@ export interface RuntimeSubmissionAdmissionInput {
50
44
  readonly runId: string;
51
45
  }
52
46
 
53
- export interface RuntimeSubmissionAdmissionContext {
54
- readonly accountId: string;
55
- readonly rateVersion: number;
56
- readonly slotIdentity: string;
57
- }
58
-
59
47
  export type RuntimeSubmissionAdmissionResult =
60
- | ({ readonly accepted: true } & RuntimeSubmissionAdmissionContext)
48
+ | { readonly accepted: true }
61
49
  | {
62
50
  readonly accepted: false;
63
51
  readonly code: string;
@@ -69,11 +57,12 @@ export type RuntimeSubmissionAdmissionHook = (
69
57
  ) => Promise<RuntimeSubmissionAdmissionResult>;
70
58
 
71
59
  export type RuntimeLifecycleFact =
72
- | ({ readonly kind: "submission-admitted" } & RuntimeSubmissionAdmissionContext & {
60
+ | {
61
+ readonly kind: "submission-admitted";
73
62
  readonly eventId: string;
74
63
  readonly submissionId: string;
75
64
  readonly runId: string;
76
- })
65
+ }
77
66
  | {
78
67
  readonly kind: "submission-terminal";
79
68
  readonly eventId: string;
@@ -82,9 +71,6 @@ export type RuntimeLifecycleFact =
82
71
  readonly status: "completed" | "aborted" | "skipped" | "error";
83
72
  readonly childRegistrationClosed: true;
84
73
  readonly zeroUsage: boolean;
85
- readonly accountId?: string;
86
- readonly rateVersion?: number;
87
- readonly slotIdentity?: string;
88
74
  }
89
75
  | {
90
76
  readonly kind: "subagent-run-registered";
@@ -92,9 +78,6 @@ export type RuntimeLifecycleFact =
92
78
  readonly submissionId: string;
93
79
  readonly subagentRunId: string;
94
80
  readonly parentRunId: string;
95
- readonly accountId?: string;
96
- readonly rateVersion?: number;
97
- readonly slotIdentity?: string;
98
81
  }
99
82
  | {
100
83
  readonly kind: "subagent-terminal";
@@ -104,9 +87,6 @@ export type RuntimeLifecycleFact =
104
87
  readonly parentRunId: string;
105
88
  readonly status: "completed" | "failed" | "aborted" | "skipped";
106
89
  readonly zeroUsage: boolean;
107
- readonly accountId?: string;
108
- readonly rateVersion?: number;
109
- readonly slotIdentity?: string;
110
90
  };
111
91
 
112
92
  export interface RuntimeEventConfirmation {
@@ -0,0 +1,52 @@
1
+ import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
2
+ import type { RuntimeModelUsageEvent } from "./kernel/bindings";
3
+
4
+ type ModelUsage = Pick<RuntimeModelUsageEvent,
5
+ "api" | "provider" | "model" | "responseModel" | "stopReason" | "usage"
6
+ >;
7
+
8
+ function reportedModel(response: AssistantMessage): string | undefined {
9
+ const model = response.responseModel ?? response.model;
10
+ return model && model !== "unknown" ? model : undefined;
11
+ }
12
+
13
+ /** Build usage facts without choosing a billing meter or changing Pi messages. */
14
+ export function summarizeModelUsage(
15
+ requested: Pick<Model<Api>, "id" | "api" | "provider">,
16
+ responses: readonly [AssistantMessage, ...AssistantMessage[]],
17
+ ): ModelUsage {
18
+ const [first, ...remaining] = responses;
19
+ const total = structuredClone(first.usage);
20
+ for (const response of remaining) {
21
+ const usage = response.usage;
22
+ total.input += usage.input;
23
+ total.output += usage.output;
24
+ total.cacheRead += usage.cacheRead;
25
+ total.cacheWrite += usage.cacheWrite;
26
+ if (usage.cacheWrite1h !== undefined) {
27
+ total.cacheWrite1h = (total.cacheWrite1h ?? 0) + usage.cacheWrite1h;
28
+ }
29
+ if (usage.reasoning !== undefined) {
30
+ total.reasoning = (total.reasoning ?? 0) + usage.reasoning;
31
+ }
32
+ total.totalTokens += usage.totalTokens;
33
+ total.cost.input += usage.cost.input;
34
+ total.cost.output += usage.cost.output;
35
+ total.cost.cacheRead += usage.cost.cacheRead;
36
+ total.cost.cacheWrite += usage.cost.cacheWrite;
37
+ total.cost.total += usage.cost.total;
38
+ }
39
+ const responseModel = reportedModel(first);
40
+ return {
41
+ api: requested.api,
42
+ provider: requested.provider,
43
+ model: requested.id,
44
+ // An aggregate must not attribute different responses to one reported model.
45
+ ...(responseModel && responseModel !== requested.id &&
46
+ remaining.every(response => reportedModel(response) === responseModel)
47
+ ? { responseModel } : {}),
48
+ ...(remaining.every(response => response.stopReason === first.stopReason)
49
+ ? { stopReason: first.stopReason } : {}),
50
+ usage: total,
51
+ };
52
+ }