@sixb/pg 0.1.2 → 0.1.4

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/dist/agents/agent-storage.d.ts +5 -3
  2. package/dist/agents/agent-storage.d.ts.map +1 -1
  3. package/dist/agents/checkpoints.d.ts +16 -0
  4. package/dist/agents/checkpoints.d.ts.map +1 -0
  5. package/dist/agents/index.d.ts +1 -0
  6. package/dist/agents/index.d.ts.map +1 -1
  7. package/dist/agents/messages.d.ts +2 -1
  8. package/dist/agents/messages.d.ts.map +1 -1
  9. package/dist/agents/rows.d.ts +18 -1
  10. package/dist/agents/rows.d.ts.map +1 -1
  11. package/dist/index.d.ts +3 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1028 -35
  14. package/dist/index.js.map +15 -13
  15. package/dist/migrations.d.ts.map +1 -1
  16. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  17. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  18. package/dist/pg-ai-cost-storage.d.ts +12 -0
  19. package/dist/pg-ai-cost-storage.d.ts.map +1 -0
  20. package/dist/pg-ai-usage-storage.d.ts +2 -1
  21. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  22. package/dist/pg-object-storage.d.ts +4 -3
  23. package/dist/pg-object-storage.d.ts.map +1 -1
  24. package/package.json +3 -3
  25. package/src/agents/agent-storage.ts +6 -3
  26. package/src/agents/checkpoints.ts +204 -0
  27. package/src/agents/index.ts +1 -0
  28. package/src/agents/messages.ts +71 -13
  29. package/src/agents/rows.ts +45 -0
  30. package/src/index.ts +7 -1
  31. package/src/migrations/026-ai-cost-accounting.sql +53 -0
  32. package/src/migrations/027-agent-context-checkpoints.sql +34 -0
  33. package/src/migrations/028-object-override-edit-times.sql +3 -0
  34. package/src/migrations.ts +10 -0
  35. package/src/ontology-storage/materialization-state.ts +2 -0
  36. package/src/ontology-storage/materialization-writer.ts +5 -3
  37. package/src/pg-ai-cost-storage.ts +707 -0
  38. package/src/pg-ai-usage-storage.ts +15 -0
  39. package/src/pg-object-query-compiler.ts +81 -1
  40. package/src/pg-object-storage.ts +109 -13
package/dist/index.js CHANGED
@@ -16,8 +16,14 @@ import {
16
16
  throwNestedStorageTransaction
17
17
  } from "@sixb/core/storage";
18
18
 
19
- // src/agents/messages.ts
20
- import { AGENT_MESSAGE_CONTENT_VERSION } from "@sixb/core/internal/agents";
19
+ // src/agents/checkpoints.ts
20
+ import {
21
+ agentContextCheckpointMatchesCreateInput,
22
+ assertAgentContextCheckpointAnchors,
23
+ assertAgentContextCheckpointAuthority,
24
+ assertAgentContextCheckpointReplayState,
25
+ assertCreateAgentContextCheckpointInput
26
+ } from "@sixb/core/internal/agent-run-storage-provider";
21
27
  import {
22
28
  AgentStorageError
23
29
  } from "@sixb/core/storage";
@@ -149,6 +155,28 @@ function rowToMessageRecord(row) {
149
155
  completedAt: row.completed_at ? new Date(row.completed_at) : undefined
150
156
  };
151
157
  }
158
+ function rowToContextCheckpointRecord(row) {
159
+ const summaryFormatVersion = Number(row.summary_format_version);
160
+ if (summaryFormatVersion !== 1) {
161
+ throw new Error(`[SixbPg] Unsupported agent context checkpoint summary format version '${summaryFormatVersion}'.`);
162
+ }
163
+ return {
164
+ id: row.id,
165
+ projectId: row.project_id,
166
+ threadId: row.thread_id,
167
+ createdByRunId: row.created_by_run_id,
168
+ previousCheckpointId: row.previous_checkpoint_id ?? undefined,
169
+ reason: row.reason,
170
+ summary: row.summary,
171
+ summaryFormatVersion: 1,
172
+ summarizedThroughSeq: Number(row.summarized_through_seq),
173
+ observedHeadSeq: Number(row.observed_head_seq),
174
+ estimatedInputTokensBefore: Number(row.estimated_input_tokens_before),
175
+ estimatedInputTokensAfter: Number(row.estimated_input_tokens_after),
176
+ summaryModelId: row.summary_model_id,
177
+ createdAt: new Date(row.created_at)
178
+ };
179
+ }
152
180
  function principalFromColumns(type, id) {
153
181
  return type && id ? { type, id } : undefined;
154
182
  }
@@ -184,7 +212,162 @@ async function queryAgentList(input) {
184
212
  return { rows, total, hasMore: offset + rows.length < total };
185
213
  }
186
214
 
215
+ // src/agents/checkpoints.ts
216
+ var PG_CHECKPOINT_RUN_ID_BATCH_SIZE = 1e4;
217
+
218
+ class PgAgentContextCheckpointStore {
219
+ sql;
220
+ constructor(sql) {
221
+ this.sql = sql;
222
+ }
223
+ async create(input) {
224
+ assertCreateAgentContextCheckpointInput(input, "SixbPg");
225
+ try {
226
+ return await runPgTransaction(this.sql, async (tx) => {
227
+ const [runRow] = await tx`
228
+ SELECT * FROM agent_runs
229
+ WHERE project_id = ${input.projectId} AND id = ${input.createdByRunId}
230
+ FOR UPDATE
231
+ `;
232
+ const [threadRow] = await tx`
233
+ SELECT * FROM agent_threads
234
+ WHERE project_id = ${input.projectId} AND id = ${input.threadId}
235
+ FOR UPDATE
236
+ `;
237
+ assertAgentContextCheckpointAuthority({
238
+ create: input,
239
+ run: runRow ? rowToRunRecord(runRow) : null,
240
+ thread: threadRow ? rowToThreadRecord(threadRow) : null,
241
+ prefix: "SixbPg"
242
+ });
243
+ const [latestRow] = await tx`
244
+ SELECT * FROM agent_context_checkpoints
245
+ WHERE project_id = ${input.projectId} AND thread_id = ${input.threadId}
246
+ ORDER BY summarized_through_seq DESC, created_at DESC, id DESC
247
+ LIMIT 1
248
+ `;
249
+ const latest = latestRow ? rowToContextCheckpointRecord(latestRow) : null;
250
+ const [headRow] = await tx`
251
+ SELECT COALESCE(MAX(seq), 0) AS seq FROM agent_messages
252
+ WHERE project_id = ${input.projectId} AND thread_id = ${input.threadId}
253
+ `;
254
+ const headSeq = Number(headRow?.seq ?? 0);
255
+ const [existingForRunRow] = await tx`
256
+ SELECT * FROM agent_context_checkpoints
257
+ WHERE project_id = ${input.projectId} AND created_by_run_id = ${input.createdByRunId}
258
+ `;
259
+ if (existingForRunRow) {
260
+ const record = rowToContextCheckpointRecord(existingForRunRow);
261
+ if (agentContextCheckpointMatchesCreateInput(record, input)) {
262
+ assertAgentContextCheckpointReplayState({
263
+ create: input,
264
+ existing: record,
265
+ latest,
266
+ headSeq,
267
+ prefix: "SixbPg"
268
+ });
269
+ return record;
270
+ }
271
+ throw new AgentStorageError("invalid_state", `[SixbPg] Agent run '${input.createdByRunId}' already created a different context checkpoint.`);
272
+ }
273
+ const [duplicateId] = await tx`
274
+ SELECT 1 AS present FROM agent_context_checkpoints
275
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
276
+ `;
277
+ if (duplicateId) {
278
+ throw new AgentStorageError("duplicate_id", `[SixbPg] Agent context checkpoint '${input.id}' already exists for project '${input.projectId}'.`);
279
+ }
280
+ const [firstRetainedRow] = await tx`
281
+ SELECT * FROM agent_messages
282
+ WHERE
283
+ project_id = ${input.projectId}
284
+ AND thread_id = ${input.threadId}
285
+ AND seq > ${input.summarizedThroughSeq}
286
+ ORDER BY seq ASC
287
+ LIMIT 1
288
+ `;
289
+ assertAgentContextCheckpointAnchors({
290
+ create: input,
291
+ latest,
292
+ headSeq,
293
+ firstRetained: firstRetainedRow ? rowToMessageRecord(firstRetainedRow) : null,
294
+ prefix: "SixbPg"
295
+ });
296
+ const [row] = await tx`
297
+ INSERT INTO agent_context_checkpoints (
298
+ project_id,
299
+ id,
300
+ thread_id,
301
+ created_by_run_id,
302
+ previous_checkpoint_id,
303
+ reason,
304
+ summary,
305
+ summary_format_version,
306
+ summarized_through_seq,
307
+ observed_head_seq,
308
+ estimated_input_tokens_before,
309
+ estimated_input_tokens_after,
310
+ summary_model_id,
311
+ created_at
312
+ ) VALUES (
313
+ ${input.projectId},
314
+ ${input.id},
315
+ ${input.threadId},
316
+ ${input.createdByRunId},
317
+ ${input.expectedPreviousCheckpointId},
318
+ ${input.reason},
319
+ ${input.summary},
320
+ ${input.summaryFormatVersion},
321
+ ${input.summarizedThroughSeq},
322
+ ${input.observedHeadSeq},
323
+ ${input.estimatedInputTokensBefore},
324
+ ${input.estimatedInputTokensAfter},
325
+ ${input.summaryModelId},
326
+ ${input.createdAt ?? new Date}
327
+ )
328
+ RETURNING *
329
+ `;
330
+ return rowToContextCheckpointRecord(row);
331
+ });
332
+ } catch (error) {
333
+ if (isUniqueViolation(error)) {
334
+ throw new AgentStorageError("duplicate_id", `[SixbPg] Agent context checkpoint '${input.id}' already exists for project '${input.projectId}'.`);
335
+ }
336
+ throw error;
337
+ }
338
+ }
339
+ async getLatest(input) {
340
+ const [row] = await this.sql`
341
+ SELECT * FROM agent_context_checkpoints
342
+ WHERE project_id = ${input.projectId} AND thread_id = ${input.threadId}
343
+ ORDER BY summarized_through_seq DESC, created_at DESC, id DESC
344
+ LIMIT 1
345
+ `;
346
+ return row ? rowToContextCheckpointRecord(row) : null;
347
+ }
348
+ async getByRunIds(input) {
349
+ const runIds = [...new Set(input.runIds)];
350
+ if (runIds.length === 0)
351
+ return [];
352
+ const rows = [];
353
+ for (let offset = 0;offset < runIds.length; offset += PG_CHECKPOINT_RUN_ID_BATCH_SIZE) {
354
+ const batch = runIds.slice(offset, offset + PG_CHECKPOINT_RUN_ID_BATCH_SIZE);
355
+ const placeholders = batch.map((_, index) => `$${index + 2}`).join(", ");
356
+ rows.push(...await this.sql.unsafe(`SELECT * FROM agent_context_checkpoints WHERE project_id = $1 AND created_by_run_id IN (${placeholders})`, [input.projectId, ...batch]));
357
+ }
358
+ const byRunId = new Map(rows.map((row) => [row.created_by_run_id, rowToContextCheckpointRecord(row)]));
359
+ return runIds.flatMap((runId) => {
360
+ const checkpoint = byRunId.get(runId);
361
+ return checkpoint ? [checkpoint] : [];
362
+ });
363
+ }
364
+ }
365
+
187
366
  // src/agents/messages.ts
367
+ import { AGENT_MESSAGE_CONTENT_VERSION } from "@sixb/core/internal/agents";
368
+ import {
369
+ AgentStorageError as AgentStorageError2
370
+ } from "@sixb/core/storage";
188
371
  class PgAgentMessageStore {
189
372
  sql;
190
373
  constructor(sql) {
@@ -194,21 +377,23 @@ class PgAgentMessageStore {
194
377
  const createdAt = input.createdAt ?? new Date;
195
378
  try {
196
379
  return await runPgTransaction(this.sql, async (tx) => {
380
+ if (input.runId !== null) {
381
+ const [run] = await tx`
382
+ SELECT id FROM agent_runs
383
+ WHERE project_id = ${input.projectId} AND id = ${input.runId}
384
+ FOR UPDATE
385
+ `;
386
+ if (!run) {
387
+ throw new AgentStorageError2("run_not_found", `[SixbPg] Agent run '${input.runId}' not found for project '${input.projectId}'.`);
388
+ }
389
+ }
197
390
  const [thread] = await tx`
198
391
  SELECT id FROM agent_threads
199
392
  WHERE project_id = ${input.projectId} AND id = ${input.threadId}
200
393
  FOR UPDATE
201
394
  `;
202
395
  if (!thread) {
203
- throw new AgentStorageError("thread_not_found", `[SixbPg] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
204
- }
205
- if (input.runId !== null) {
206
- const [run] = await tx`
207
- SELECT id FROM agent_runs WHERE project_id = ${input.projectId} AND id = ${input.runId}
208
- `;
209
- if (!run) {
210
- throw new AgentStorageError("run_not_found", `[SixbPg] Agent run '${input.runId}' not found for project '${input.projectId}'.`);
211
- }
396
+ throw new AgentStorageError2("thread_not_found", `[SixbPg] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
212
397
  }
213
398
  const [seqRow] = await tx`
214
399
  SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM agent_messages
@@ -255,11 +440,51 @@ class PgAgentMessageStore {
255
440
  });
256
441
  } catch (error) {
257
442
  if (isUniqueViolation(error)) {
258
- throw new AgentStorageError("duplicate_id", `[SixbPg] Agent message '${input.id}' already exists for project '${input.projectId}'.`);
443
+ throw new AgentStorageError2("duplicate_id", `[SixbPg] Agent message '${input.id}' already exists for project '${input.projectId}'.`);
259
444
  }
260
445
  throw error;
261
446
  }
262
447
  }
448
+ async deleteByRunId(input) {
449
+ return runPgTransaction(this.sql, async (tx) => {
450
+ const [thread] = await tx`
451
+ SELECT id FROM agent_threads
452
+ WHERE project_id = ${input.projectId} AND id = ${input.threadId}
453
+ FOR UPDATE
454
+ `;
455
+ if (!thread) {
456
+ throw new AgentStorageError2("thread_not_found", `[SixbPg] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
457
+ }
458
+ const deleted = await tx`
459
+ DELETE FROM agent_messages
460
+ WHERE project_id = ${input.projectId}
461
+ AND thread_id = ${input.threadId}
462
+ AND run_id = ${input.runId}
463
+ RETURNING id
464
+ `;
465
+ if (deleted.length === 0) {
466
+ return 0;
467
+ }
468
+ const updatedAt = new Date;
469
+ await tx`
470
+ UPDATE agent_threads
471
+ SET
472
+ message_count = (
473
+ SELECT COUNT(*) FROM agent_messages
474
+ WHERE project_id = ${input.projectId} AND thread_id = ${input.threadId}
475
+ ),
476
+ last_message_at = (
477
+ SELECT created_at FROM agent_messages
478
+ WHERE project_id = ${input.projectId} AND thread_id = ${input.threadId}
479
+ ORDER BY seq DESC
480
+ LIMIT 1
481
+ ),
482
+ updated_at = ${updatedAt}
483
+ WHERE project_id = ${input.projectId} AND id = ${input.threadId}
484
+ `;
485
+ return deleted.length;
486
+ });
487
+ }
263
488
  async getById(params) {
264
489
  const [row] = await this.sql`
265
490
  SELECT * FROM agent_messages WHERE project_id = ${params.projectId} AND id = ${params.id}
@@ -273,6 +498,10 @@ class PgAgentMessageStore {
273
498
  const whereClauses = ["project_id = $1", "thread_id = $2"];
274
499
  const params = [input.projectId, input.threadId];
275
500
  let index = 3;
501
+ if (input.afterSeq !== undefined) {
502
+ whereClauses.push(`seq > $${index++}`);
503
+ params.push(input.afterSeq);
504
+ }
276
505
  if (input.roles) {
277
506
  const placeholders = input.roles.map(() => `$${index++}`);
278
507
  whereClauses.push(`role IN (${placeholders.join(", ")})`);
@@ -299,7 +528,7 @@ import { normalizeRequesterGroupIds } from "@sixb/core/internal/auth";
299
528
  import { serializeSixbFailure } from "@sixb/core/internal/errors";
300
529
  import {
301
530
  AGENT_RUN_FAILURE_CODES as AGENT_RUN_FAILURE_CODES2,
302
- AgentStorageError as AgentStorageError2
531
+ AgentStorageError as AgentStorageError3
303
532
  } from "@sixb/core/storage";
304
533
 
305
534
  // src/run-list-query.ts
@@ -384,10 +613,10 @@ class PgAgentRunStore {
384
613
  FOR UPDATE
385
614
  `;
386
615
  if (!thread) {
387
- throw new AgentStorageError2("thread_not_found", `[SixbPg] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
616
+ throw new AgentStorageError3("thread_not_found", `[SixbPg] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`);
388
617
  }
389
618
  if (thread.active_run_id !== null) {
390
- throw new AgentStorageError2("active_run_exists", `[SixbPg] Agent thread '${input.threadId}' already has an active run '${thread.active_run_id}'.`);
619
+ throw new AgentStorageError3("active_run_exists", `[SixbPg] Agent thread '${input.threadId}' already has an active run '${thread.active_run_id}'.`);
391
620
  }
392
621
  const [row] = await tx`
393
622
  INSERT INTO agent_runs (
@@ -423,7 +652,7 @@ class PgAgentRunStore {
423
652
  });
424
653
  } catch (error) {
425
654
  if (isUniqueViolation(error)) {
426
- throw new AgentStorageError2("duplicate_id", `[SixbPg] Agent run '${input.id}' already exists for project '${input.projectId}'.`);
655
+ throw new AgentStorageError3("duplicate_id", `[SixbPg] Agent run '${input.id}' already exists for project '${input.projectId}'.`);
427
656
  }
428
657
  throw error;
429
658
  }
@@ -483,7 +712,7 @@ class PgAgentRunStore {
483
712
  return runPgTransaction(this.sql, async (tx) => {
484
713
  const run = await this.lockRunning(tx, input.projectId, input.id);
485
714
  if (run.execution_token !== input.executionToken) {
486
- throw new AgentStorageError2("execution_lost", `[SixbPg] Execution token is no longer current on agent run '${input.id}'.`);
715
+ throw new AgentStorageError3("execution_lost", `[SixbPg] Execution token is no longer current on agent run '${input.id}'.`);
487
716
  }
488
717
  const [row] = await tx`
489
718
  UPDATE agent_runs
@@ -503,7 +732,7 @@ class PgAgentRunStore {
503
732
  return runPgTransaction(this.sql, async (tx) => {
504
733
  const run = await this.lockRunning(tx, input.projectId, input.id);
505
734
  if (run.execution_token !== input.executionToken) {
506
- throw new AgentStorageError2("execution_lost", `[SixbPg] Execution token is no longer current on agent run '${input.id}'.`);
735
+ throw new AgentStorageError3("execution_lost", `[SixbPg] Execution token is no longer current on agent run '${input.id}'.`);
507
736
  }
508
737
  const [row] = await tx`
509
738
  UPDATE agent_runs
@@ -584,10 +813,10 @@ class PgAgentRunStore {
584
813
  FOR UPDATE
585
814
  `;
586
815
  if (!row) {
587
- throw new AgentStorageError2("run_not_found", `[SixbPg] Agent run '${id}' not found for project '${projectId}'.`);
816
+ throw new AgentStorageError3("run_not_found", `[SixbPg] Agent run '${id}' not found for project '${projectId}'.`);
588
817
  }
589
818
  if (row.status !== status) {
590
- throw new AgentStorageError2("invalid_state", `[SixbPg] Agent run '${id}' is not ${status} (status '${row.status}').`);
819
+ throw new AgentStorageError3("invalid_state", `[SixbPg] Agent run '${id}' is not ${status} (status '${row.status}').`);
591
820
  }
592
821
  return row;
593
822
  }
@@ -601,7 +830,7 @@ class PgAgentRunStore {
601
830
 
602
831
  // src/agents/threads.ts
603
832
  import {
604
- AgentStorageError as AgentStorageError3
833
+ AgentStorageError as AgentStorageError4
605
834
  } from "@sixb/core/storage";
606
835
  class PgAgentThreadStore {
607
836
  sql;
@@ -639,7 +868,7 @@ class PgAgentThreadStore {
639
868
  return rowToThreadRecord(row);
640
869
  } catch (error) {
641
870
  if (isUniqueViolation(error)) {
642
- throw new AgentStorageError3("duplicate_id", `[SixbPg] Agent thread '${input.id}' already exists for project '${input.projectId}'.`);
871
+ throw new AgentStorageError4("duplicate_id", `[SixbPg] Agent thread '${input.id}' already exists for project '${input.projectId}'.`);
643
872
  }
644
873
  throw error;
645
874
  }
@@ -695,10 +924,12 @@ class PgAgentStorage {
695
924
  threads;
696
925
  runs;
697
926
  messages;
927
+ checkpoints;
698
928
  constructor(options) {
699
929
  this.threads = new PgAgentThreadStore(options.sql);
700
930
  this.runs = new PgAgentRunStore(options.sql, options.executions);
701
931
  this.messages = new PgAgentMessageStore(options.sql);
932
+ this.checkpoints = new PgAgentContextCheckpointStore(options.sql);
702
933
  }
703
934
  }
704
935
  // src/auth-storage/auth-storage.ts
@@ -5915,6 +6146,105 @@ CREATE INDEX idx_connector_connections_status
5915
6146
  ON connector_connections (project_id, connector_id, status, id);
5916
6147
  `;
5917
6148
 
6149
+ // src/migrations/026-ai-cost-accounting.sql
6150
+ var _026_ai_cost_accounting_default = `CREATE TABLE ai_model_call_valuations (
6151
+ project_id TEXT NOT NULL CHECK (length(btrim(project_id)) > 0),
6152
+ usage_record_id TEXT NOT NULL CHECK (length(btrim(usage_record_id)) > 0),
6153
+ status TEXT NOT NULL CHECK (status IN ('rated', 'unpriceable')),
6154
+ provider_id TEXT CHECK (provider_id IS NULL OR length(btrim(provider_id)) > 0),
6155
+ model_id TEXT CHECK (model_id IS NULL OR length(btrim(model_id)) > 0),
6156
+ currency TEXT CHECK (currency IS NULL OR currency ~ '^[A-Z]{3}$'),
6157
+ amount_nanos BIGINT CHECK (amount_nanos IS NULL OR amount_nanos >= 0),
6158
+ reason TEXT CHECK (
6159
+ reason IS NULL OR reason IN (
6160
+ 'missingBillingIdentity',
6161
+ 'missingCatalogEntry',
6162
+ 'missingUsageMeter',
6163
+ 'unsupportedPricingDimension',
6164
+ 'invalidUsageForFormula'
6165
+ )
6166
+ ),
6167
+ details JSONB NOT NULL CHECK (jsonb_typeof(details) = 'object'),
6168
+ rated_at TIMESTAMPTZ NOT NULL,
6169
+ PRIMARY KEY (project_id, usage_record_id),
6170
+ FOREIGN KEY (project_id, usage_record_id)
6171
+ REFERENCES ai_model_call_usage(project_id, id)
6172
+ ON DELETE RESTRICT,
6173
+ CHECK (
6174
+ (
6175
+ status = 'rated'
6176
+ AND provider_id IS NOT NULL
6177
+ AND model_id IS NOT NULL
6178
+ AND currency IS NOT NULL
6179
+ AND amount_nanos IS NOT NULL
6180
+ AND reason IS NULL
6181
+ )
6182
+ OR
6183
+ (
6184
+ status = 'unpriceable'
6185
+ AND currency IS NULL
6186
+ AND amount_nanos IS NULL
6187
+ AND reason IS NOT NULL
6188
+ AND (
6189
+ (reason = 'missingBillingIdentity' AND provider_id IS NULL AND model_id IS NULL)
6190
+ OR
6191
+ (reason <> 'missingBillingIdentity' AND provider_id IS NOT NULL AND model_id IS NOT NULL)
6192
+ )
6193
+ )
6194
+ )
6195
+ );
6196
+
6197
+ CREATE INDEX idx_ai_model_call_usage_provider_model_time
6198
+ ON ai_model_call_usage (project_id, provider_id, requested_model_id, occurred_at, id);
6199
+ CREATE INDEX idx_ai_model_call_usage_model_time
6200
+ ON ai_model_call_usage (project_id, requested_model_id, occurred_at, id);
6201
+ CREATE INDEX idx_ai_model_call_valuations_summary
6202
+ ON ai_model_call_valuations (project_id, status, currency, usage_record_id);
6203
+ `;
6204
+
6205
+ // src/migrations/027-agent-context-checkpoints.sql
6206
+ var _027_agent_context_checkpoints_default = `CREATE TABLE agent_context_checkpoints (
6207
+ project_id TEXT NOT NULL,
6208
+ id TEXT NOT NULL,
6209
+ thread_id TEXT NOT NULL,
6210
+ created_by_run_id TEXT NOT NULL,
6211
+ previous_checkpoint_id TEXT,
6212
+ reason TEXT NOT NULL CHECK (reason IN ('threshold', 'overflow')),
6213
+ summary TEXT NOT NULL CHECK (length(trim(summary)) > 0),
6214
+ summary_format_version INTEGER NOT NULL CHECK (summary_format_version = 1),
6215
+ summarized_through_seq INTEGER NOT NULL CHECK (summarized_through_seq >= 1),
6216
+ observed_head_seq INTEGER NOT NULL,
6217
+ estimated_input_tokens_before BIGINT NOT NULL CHECK (estimated_input_tokens_before >= 0),
6218
+ estimated_input_tokens_after BIGINT NOT NULL CHECK (estimated_input_tokens_after >= 0),
6219
+ summary_model_id TEXT NOT NULL CHECK (length(trim(summary_model_id)) > 0),
6220
+ created_at TIMESTAMPTZ NOT NULL,
6221
+ PRIMARY KEY (project_id, id),
6222
+ UNIQUE (project_id, created_by_run_id),
6223
+ FOREIGN KEY (project_id, thread_id)
6224
+ REFERENCES agent_threads(project_id, id) ON DELETE CASCADE,
6225
+ FOREIGN KEY (project_id, created_by_run_id)
6226
+ REFERENCES agent_runs(project_id, id) ON DELETE RESTRICT,
6227
+ FOREIGN KEY (project_id, previous_checkpoint_id)
6228
+ REFERENCES agent_context_checkpoints(project_id, id) ON DELETE RESTRICT,
6229
+ CHECK (summarized_through_seq < observed_head_seq)
6230
+ );
6231
+
6232
+ CREATE INDEX idx_agent_context_checkpoints_thread_latest
6233
+ ON agent_context_checkpoints (
6234
+ project_id,
6235
+ thread_id,
6236
+ summarized_through_seq DESC,
6237
+ created_at DESC,
6238
+ id DESC
6239
+ );
6240
+ `;
6241
+
6242
+ // src/migrations/028-object-override-edit-times.sql
6243
+ var _028_object_override_edit_times_default = `ALTER TABLE ontology_object_overrides
6244
+ ADD COLUMN edited_at JSONB NOT NULL DEFAULT '{}'::jsonb
6245
+ CHECK (jsonb_typeof(edited_at) = 'object');
6246
+ `;
6247
+
5918
6248
  // src/migrations.ts
5919
6249
  var POSTGRES_STORAGE_ADAPTER_ID = "SixbPostgresStorage";
5920
6250
  function pgSql(id, sqlText) {
@@ -6114,7 +6444,10 @@ var postgresStorageMigrations = defineMigrations({
6114
6444
  pgSql("022-projection-executions", _022_projection_executions_default),
6115
6445
  pgSql("023-webhook-executions", _023_webhook_executions_default),
6116
6446
  pgSql("024-ontology-commit-executions", _024_ontology_commit_executions_default),
6117
- pgSql("025-connector-connections", _025_connector_connections_default)
6447
+ pgSql("025-connector-connections", _025_connector_connections_default),
6448
+ pgSql("026-ai-cost-accounting", _026_ai_cost_accounting_default),
6449
+ pgSql("027-agent-context-checkpoints", _027_agent_context_checkpoints_default),
6450
+ pgSql("028-object-override-edit-times", _028_object_override_edit_times_default)
6118
6451
  ]
6119
6452
  });
6120
6453
 
@@ -7159,6 +7492,7 @@ function storedObjectOverride(row) {
7159
7492
  return {
7160
7493
  ref,
7161
7494
  value: structuredClone(row.value),
7495
+ editedAt: structuredClone(row.edited_at),
7162
7496
  lastCommitId: row.last_commit_id,
7163
7497
  updatedAt: toIsoString2(row.updated_at)
7164
7498
  };
@@ -7592,6 +7926,7 @@ class PgMaterializationWriter {
7592
7926
  objectTypeId: item.ref.objectTypeId,
7593
7927
  primaryId: item.ref.primaryId,
7594
7928
  value: item.value,
7929
+ editedAt: item.editedAt,
7595
7930
  lastCommitId: item.lastCommitId,
7596
7931
  updatedAt: item.updatedAt,
7597
7932
  expectedLastCommitId: item.expectedLastCommitId
@@ -7603,10 +7938,10 @@ class PgMaterializationWriter {
7603
7938
  SELECT value FROM jsonb_array_elements(${jsonParameter2(this.sql, objectInserts)}::jsonb)
7604
7939
  )
7605
7940
  INSERT INTO ontology_object_overrides (
7606
- project_id, object_type_id, primary_id, value, last_commit_id, updated_at
7941
+ project_id, object_type_id, primary_id, value, edited_at, last_commit_id, updated_at
7607
7942
  )
7608
7943
  SELECT ${projectId}, value->>'objectTypeId', value->>'primaryId', value->'value',
7609
- value->>'lastCommitId',
7944
+ value->'editedAt', value->>'lastCommitId',
7610
7945
  (value->>'updatedAt')::timestamptz
7611
7946
  FROM staged
7612
7947
  ON CONFLICT DO NOTHING
@@ -7622,7 +7957,8 @@ class PgMaterializationWriter {
7622
7957
  SELECT value FROM jsonb_array_elements(${jsonParameter2(this.sql, objectUpdates)}::jsonb)
7623
7958
  )
7624
7959
  UPDATE ontology_object_overrides AS overrides
7625
- SET value = staged.value->'value', last_commit_id = staged.value->>'lastCommitId',
7960
+ SET value = staged.value->'value', edited_at = staged.value->'editedAt',
7961
+ last_commit_id = staged.value->>'lastCommitId',
7626
7962
  updated_at = (staged.value->>'updatedAt')::timestamptz
7627
7963
  FROM staged
7628
7964
  WHERE overrides.project_id = ${projectId}
@@ -9811,6 +10147,502 @@ function rowToActionSubject(row) {
9811
10147
  };
9812
10148
  }
9813
10149
 
10150
+ // src/pg-ai-cost-storage.ts
10151
+ import {
10152
+ aiModelCallCostDetails,
10153
+ aiModelCallCostMatchesUsage,
10154
+ buildAiAccountingOverviewFromFragments,
10155
+ normalizeAiAccountingQuery,
10156
+ normalizeAiModelCallAccountingQuery,
10157
+ normalizeAiModelCallCostRecord,
10158
+ parseAiModelCallCostDetails,
10159
+ toAccountingItem
10160
+ } from "@sixb/core/internal/ai-cost-storage-provider";
10161
+ import {
10162
+ AiCostStorageError
10163
+ } from "@sixb/core/storage";
10164
+ class PgAiCostStorage {
10165
+ sql;
10166
+ constructor(sql) {
10167
+ this.sql = sql;
10168
+ }
10169
+ async recordModelCallCost(input) {
10170
+ const record = normalizeAiModelCallCostRecord(input);
10171
+ await runPgTransaction(this.sql, async (tx) => {
10172
+ const usage = await findUsage(tx, record.projectId, record.usageRecordId);
10173
+ if (!usage)
10174
+ throw missingUsage(record);
10175
+ assertCostMatchesUsage(record, usage);
10176
+ if (await findCost(tx, record.projectId, record.usageRecordId))
10177
+ return;
10178
+ const rated = record.status === "rated" ? record : undefined;
10179
+ const inserted = await tx`
10180
+ INSERT INTO ai_model_call_valuations (
10181
+ project_id, usage_record_id, status, provider_id, model_id,
10182
+ currency, amount_nanos, reason, details, rated_at
10183
+ ) VALUES (
10184
+ ${record.projectId}, ${record.usageRecordId}, ${record.status},
10185
+ ${record.billingIdentity?.providerId ?? null},
10186
+ ${record.billingIdentity?.modelId ?? null}, ${rated?.money.currency ?? null},
10187
+ ${rated?.money.amountNanos ?? null},
10188
+ ${record.status === "unpriceable" ? record.reason : null},
10189
+ ${JSON.stringify(aiModelCallCostDetails(record))}::text::jsonb, ${record.ratedAt}
10190
+ )
10191
+ ON CONFLICT DO NOTHING
10192
+ RETURNING usage_record_id
10193
+ `;
10194
+ if (inserted.length === 0 && !await findCost(tx, record.projectId, record.usageRecordId)) {
10195
+ throw new Error("[SixbPg] AI valuation insert conflicted without a visible row.");
10196
+ }
10197
+ });
10198
+ }
10199
+ async summarizeExecutions(input) {
10200
+ assertNonBlank(input.projectId, "cost summary projectId");
10201
+ for (const id of input.executionIds)
10202
+ assertNonBlank(id, "cost summary executionId");
10203
+ if (input.executionIds.length === 0)
10204
+ return [];
10205
+ const ids = [...new Set(input.executionIds)];
10206
+ const rows = await this.sql`
10207
+ WITH requested(execution_id) AS (
10208
+ SELECT DISTINCT * FROM unnest(${this.sql.array(ids)}::text[])
10209
+ )
10210
+ SELECT usage.execution_id,
10211
+ cost.status,
10212
+ cost.currency,
10213
+ cost.amount_nanos
10214
+ FROM requested
10215
+ JOIN ai_model_call_usage AS usage
10216
+ ON usage.project_id = ${input.projectId} AND usage.execution_id = requested.execution_id
10217
+ LEFT JOIN ai_model_call_valuations AS cost
10218
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
10219
+ `;
10220
+ return aggregateSummaryRows(input.executionIds, rows);
10221
+ }
10222
+ async queryProjectOverview(input) {
10223
+ const query = normalizeAiAccountingQuery(input);
10224
+ const providerId = query.providerId ?? null;
10225
+ const modelId = query.modelId ?? null;
10226
+ const rows = await this.sql`
10227
+ WITH filtered AS (
10228
+ SELECT usage.provider_id,
10229
+ usage.requested_model_id,
10230
+ usage.occurred_at,
10231
+ usage.input_tokens,
10232
+ usage.output_tokens,
10233
+ usage.uncached_input_tokens,
10234
+ usage.cache_read_input_tokens,
10235
+ usage.cache_write_input_tokens,
10236
+ usage.text_output_tokens,
10237
+ usage.reasoning_output_tokens,
10238
+ usage.reporting_status,
10239
+ COALESCE(direct_agent.agent_id, workflow_agent.agent_id) AS attribution_agent_id,
10240
+ workflow_node.workflow_id AS attribution_workflow_id,
10241
+ cost.status AS cost_status,
10242
+ cost.currency AS cost_currency,
10243
+ cost.amount_nanos AS cost_amount_nanos
10244
+ FROM ai_model_call_usage AS usage
10245
+ LEFT JOIN agent_runs AS direct_agent
10246
+ ON direct_agent.project_id = usage.project_id
10247
+ AND direct_agent.execution_id = usage.execution_id
10248
+ LEFT JOIN workflow_agent_node_runs AS workflow_agent
10249
+ ON workflow_agent.project_id = usage.project_id
10250
+ AND workflow_agent.execution_id = usage.execution_id
10251
+ LEFT JOIN workflow_node_runs AS workflow_node
10252
+ ON workflow_node.project_id = workflow_agent.project_id
10253
+ AND workflow_node.id = workflow_agent.node_run_id
10254
+ LEFT JOIN ai_model_call_valuations AS cost
10255
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
10256
+ WHERE usage.project_id = ${query.projectId}
10257
+ AND usage.occurred_at >= ${query.from} AND usage.occurred_at < ${query.to}
10258
+ AND (${providerId}::text IS NULL OR usage.provider_id = ${providerId})
10259
+ AND (${modelId}::text IS NULL OR usage.requested_model_id = ${modelId})
10260
+ ), classified AS (
10261
+ SELECT filtered.*,
10262
+ CASE
10263
+ WHEN cost_status = 'rated' THEN 'rated'
10264
+ WHEN cost_status = 'unpriceable' THEN 'unpriceable'
10265
+ ELSE 'unvalued'
10266
+ END AS valuation_status,
10267
+ CASE WHEN cost_status = 'rated' THEN cost_currency ELSE NULL END AS amount_currency,
10268
+ CASE WHEN cost_status = 'rated' THEN cost_amount_nanos ELSE NULL END AS amount_nanos
10269
+ FROM filtered
10270
+ ), expanded AS (
10271
+ SELECT 'totals'::text AS dimension,
10272
+ NULL::text AS key_1,
10273
+ NULL::text AS key_2,
10274
+ NULL::timestamptz AS bucket_start,
10275
+ classified.*
10276
+ FROM classified
10277
+ UNION ALL
10278
+ SELECT 'series', NULL, NULL,
10279
+ CASE ${input.bucket}::text
10280
+ WHEN 'hour' THEN
10281
+ date_trunc('hour', occurred_at AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
10282
+ WHEN 'day' THEN
10283
+ date_trunc('day', occurred_at AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
10284
+ ELSE date_trunc('week', occurred_at AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
10285
+ END,
10286
+ classified.*
10287
+ FROM classified
10288
+ UNION ALL
10289
+ SELECT 'model', provider_id, requested_model_id, NULL, classified.* FROM classified
10290
+ UNION ALL
10291
+ SELECT 'agent', attribution_agent_id, NULL, NULL, classified.*
10292
+ FROM classified WHERE attribution_agent_id IS NOT NULL
10293
+ UNION ALL
10294
+ SELECT 'workflow', attribution_workflow_id, NULL, NULL, classified.*
10295
+ FROM classified WHERE attribution_workflow_id IS NOT NULL
10296
+ )
10297
+ SELECT dimension,
10298
+ key_1,
10299
+ key_2,
10300
+ bucket_start,
10301
+ amount_currency,
10302
+ COUNT(*)::text AS model_call_count,
10303
+ COUNT(*) FILTER (WHERE reporting_status <> 'unavailable')::text
10304
+ AS reported_usage_call_count,
10305
+ COUNT(input_tokens)::text AS input_tokens_count,
10306
+ SUM(input_tokens)::text AS input_tokens_sum,
10307
+ COUNT(output_tokens)::text AS output_tokens_count,
10308
+ SUM(output_tokens)::text AS output_tokens_sum,
10309
+ COUNT(uncached_input_tokens)::text AS uncached_input_tokens_count,
10310
+ SUM(uncached_input_tokens)::text AS uncached_input_tokens_sum,
10311
+ COUNT(cache_read_input_tokens)::text AS cache_read_input_tokens_count,
10312
+ SUM(cache_read_input_tokens)::text AS cache_read_input_tokens_sum,
10313
+ COUNT(cache_write_input_tokens)::text AS cache_write_input_tokens_count,
10314
+ SUM(cache_write_input_tokens)::text AS cache_write_input_tokens_sum,
10315
+ COUNT(text_output_tokens)::text AS text_output_tokens_count,
10316
+ SUM(text_output_tokens)::text AS text_output_tokens_sum,
10317
+ COUNT(reasoning_output_tokens)::text AS reasoning_output_tokens_count,
10318
+ SUM(reasoning_output_tokens)::text AS reasoning_output_tokens_sum,
10319
+ COUNT(*) FILTER (WHERE valuation_status = 'rated')::text AS rated_call_count,
10320
+ COUNT(*) FILTER (WHERE valuation_status = 'unpriceable')::text
10321
+ AS unpriceable_call_count,
10322
+ COUNT(*) FILTER (WHERE valuation_status = 'unvalued')::text
10323
+ AS unvalued_call_count,
10324
+ SUM(amount_nanos)::text AS amount_nanos
10325
+ FROM expanded
10326
+ GROUP BY dimension, key_1, key_2, bucket_start, amount_currency
10327
+ `;
10328
+ return buildAiAccountingOverviewFromFragments(input, rows.map(aggregateFragmentFromRow));
10329
+ }
10330
+ async listModelCalls(input) {
10331
+ const query = normalizeAiModelCallAccountingQuery(input);
10332
+ const providerId = query.providerId ?? null;
10333
+ const modelId = query.modelId ?? null;
10334
+ const executionId = query.executionId ?? null;
10335
+ const valuationStatus = query.valuationStatus ?? null;
10336
+ const [count] = await this.sql`
10337
+ SELECT COUNT(*)::text AS total
10338
+ FROM ai_model_call_usage AS usage
10339
+ LEFT JOIN ai_model_call_valuations AS cost
10340
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
10341
+ WHERE usage.project_id = ${query.projectId}
10342
+ AND usage.occurred_at >= ${query.from} AND usage.occurred_at < ${query.to}
10343
+ AND (${providerId}::text IS NULL OR usage.provider_id = ${providerId})
10344
+ AND (${modelId}::text IS NULL OR usage.requested_model_id = ${modelId})
10345
+ AND (${executionId}::text IS NULL OR usage.execution_id = ${executionId})
10346
+ AND (
10347
+ ${valuationStatus}::text IS NULL OR ${valuationStatus} = CASE
10348
+ WHEN cost.status = 'rated' THEN 'rated'
10349
+ WHEN cost.status = 'unpriceable' THEN 'unpriceable'
10350
+ ELSE 'unvalued'
10351
+ END
10352
+ )
10353
+ `;
10354
+ const rows = await this.sql`
10355
+ SELECT usage.*,
10356
+ CASE
10357
+ WHEN direct_agent.id IS NOT NULL THEN 'agent'
10358
+ WHEN workflow_agent.node_run_id IS NOT NULL THEN 'workflowAgent'
10359
+ ELSE NULL
10360
+ END AS attribution_kind,
10361
+ COALESCE(direct_agent.agent_id, workflow_agent.agent_id) AS attribution_agent_id,
10362
+ direct_agent.id AS attribution_agent_run_id,
10363
+ direct_agent.thread_id AS attribution_thread_id,
10364
+ workflow_agent.node_run_id AS attribution_node_run_id,
10365
+ workflow_node.workflow_id AS attribution_workflow_id,
10366
+ workflow_node.workflow_run_id AS attribution_workflow_run_id,
10367
+ COALESCE((
10368
+ SELECT jsonb_agg(groups.group_id ORDER BY groups.group_id)
10369
+ FROM ai_model_call_usage_groups AS groups
10370
+ WHERE groups.project_id = usage.project_id AND groups.usage_record_id = usage.id
10371
+ ), '[]'::jsonb) AS requester_group_ids,
10372
+ cost.status AS cost_status,
10373
+ cost.provider_id AS cost_provider_id,
10374
+ cost.model_id AS cost_model_id,
10375
+ cost.currency AS cost_currency,
10376
+ cost.amount_nanos AS cost_amount_nanos,
10377
+ cost.reason AS cost_reason,
10378
+ cost.details AS cost_details,
10379
+ cost.rated_at AS cost_rated_at
10380
+ FROM ai_model_call_usage AS usage
10381
+ LEFT JOIN agent_runs AS direct_agent
10382
+ ON direct_agent.project_id = usage.project_id
10383
+ AND direct_agent.execution_id = usage.execution_id
10384
+ LEFT JOIN workflow_agent_node_runs AS workflow_agent
10385
+ ON workflow_agent.project_id = usage.project_id
10386
+ AND workflow_agent.execution_id = usage.execution_id
10387
+ LEFT JOIN workflow_node_runs AS workflow_node
10388
+ ON workflow_node.project_id = workflow_agent.project_id
10389
+ AND workflow_node.id = workflow_agent.node_run_id
10390
+ LEFT JOIN ai_model_call_valuations AS cost
10391
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
10392
+ WHERE usage.project_id = ${query.projectId}
10393
+ AND usage.occurred_at >= ${query.from} AND usage.occurred_at < ${query.to}
10394
+ AND (${providerId}::text IS NULL OR usage.provider_id = ${providerId})
10395
+ AND (${modelId}::text IS NULL OR usage.requested_model_id = ${modelId})
10396
+ AND (${executionId}::text IS NULL OR usage.execution_id = ${executionId})
10397
+ AND (
10398
+ ${valuationStatus}::text IS NULL OR ${valuationStatus} = CASE
10399
+ WHEN cost.status = 'rated' THEN 'rated'
10400
+ WHEN cost.status = 'unpriceable' THEN 'unpriceable'
10401
+ ELSE 'unvalued'
10402
+ END
10403
+ )
10404
+ ORDER BY usage.occurred_at DESC, usage.id ASC
10405
+ LIMIT ${query.limit} OFFSET ${query.offset}
10406
+ `;
10407
+ const items = rows.map((row) => toAccountingItem(accountingItemFromRow(row)));
10408
+ if (!count)
10409
+ throw new Error("[SixbPg] AI accounting count query returned no row.");
10410
+ const total = safeSqlCount(count.total, "model-call list total");
10411
+ return structuredClone({
10412
+ items,
10413
+ total,
10414
+ hasMore: query.offset + items.length < total
10415
+ });
10416
+ }
10417
+ }
10418
+ async function findCost(sql, projectId, usageId) {
10419
+ const [row] = await sql`
10420
+ SELECT * FROM ai_model_call_valuations
10421
+ WHERE project_id = ${projectId} AND usage_record_id = ${usageId}
10422
+ `;
10423
+ return row ? rowToCost(row) : null;
10424
+ }
10425
+ async function findUsage(sql, projectId, id) {
10426
+ const [row] = await sql`
10427
+ SELECT * FROM ai_model_call_usage WHERE project_id = ${projectId} AND id = ${id}
10428
+ `;
10429
+ return row ? usageFromRow(row, []) : null;
10430
+ }
10431
+ function aggregateFragmentFromRow(row) {
10432
+ const dimensionKeys = row.dimension === "model" ? { providerId: required2(row.key_1), modelId: required2(row.key_2) } : row.dimension === "agent" ? { agentId: required2(row.key_1) } : row.dimension === "workflow" ? { workflowId: required2(row.key_1) } : row.dimension === "series" ? { start: new Date(required2(row.bucket_start)) } : {};
10433
+ return {
10434
+ dimension: row.dimension,
10435
+ ...dimensionKeys,
10436
+ modelCallCount: row.model_call_count,
10437
+ usage: {
10438
+ reportedCallCount: row.reported_usage_call_count,
10439
+ inputTokens: meterFragment(row.input_tokens_count, row.input_tokens_sum),
10440
+ outputTokens: meterFragment(row.output_tokens_count, row.output_tokens_sum),
10441
+ uncachedInputTokens: meterFragment(row.uncached_input_tokens_count, row.uncached_input_tokens_sum),
10442
+ cacheReadInputTokens: meterFragment(row.cache_read_input_tokens_count, row.cache_read_input_tokens_sum),
10443
+ cacheWriteInputTokens: meterFragment(row.cache_write_input_tokens_count, row.cache_write_input_tokens_sum),
10444
+ textOutputTokens: meterFragment(row.text_output_tokens_count, row.text_output_tokens_sum),
10445
+ reasoningOutputTokens: meterFragment(row.reasoning_output_tokens_count, row.reasoning_output_tokens_sum)
10446
+ },
10447
+ costs: {
10448
+ ...row.amount_currency === null || row.amount_nanos === null ? {} : {
10449
+ amount: {
10450
+ currency: row.amount_currency,
10451
+ amountNanos: row.amount_nanos
10452
+ }
10453
+ },
10454
+ ratedCallCount: row.rated_call_count,
10455
+ unpriceableCallCount: row.unpriceable_call_count,
10456
+ unvaluedCallCount: row.unvalued_call_count
10457
+ }
10458
+ };
10459
+ }
10460
+ function meterFragment(presentCallCount, total) {
10461
+ return { presentCallCount, total };
10462
+ }
10463
+ function accountingItemFromRow(row) {
10464
+ const usage = usageFromRow(row, parseStringArray(row.requester_group_ids));
10465
+ const cost = row.cost_status === null ? undefined : costFromJoinedRow(row);
10466
+ const attribution = row.attribution_kind === null ? undefined : row.attribution_kind === "agent" ? {
10467
+ kind: "agent",
10468
+ agentId: required2(row.attribution_agent_id),
10469
+ agentRunId: required2(row.attribution_agent_run_id),
10470
+ threadId: required2(row.attribution_thread_id)
10471
+ } : {
10472
+ kind: "workflowAgent",
10473
+ agentId: required2(row.attribution_agent_id),
10474
+ nodeRunId: required2(row.attribution_node_run_id),
10475
+ workflowId: required2(row.attribution_workflow_id),
10476
+ workflowRunId: required2(row.attribution_workflow_run_id)
10477
+ };
10478
+ return {
10479
+ usage,
10480
+ ...attribution ? { attribution } : {},
10481
+ ...cost ? { cost } : {}
10482
+ };
10483
+ }
10484
+ function usageFromRow(row, requesterGroupIds) {
10485
+ const number = (value) => value === null ? undefined : Number(value);
10486
+ return {
10487
+ id: row.id,
10488
+ projectId: row.project_id,
10489
+ executionId: row.execution_id,
10490
+ attempt: Number(row.attempt),
10491
+ callId: row.call_id,
10492
+ requesterGroupIds,
10493
+ providerId: row.provider_id,
10494
+ requestedModelId: row.requested_model_id,
10495
+ ...row.response_model_id === null ? {} : { responseModelId: row.response_model_id },
10496
+ responseId: row.response_id,
10497
+ usage: {
10498
+ ...number(row.input_tokens) === undefined ? {} : { inputTokens: number(row.input_tokens) },
10499
+ ...number(row.output_tokens) === undefined ? {} : { outputTokens: number(row.output_tokens) },
10500
+ ...number(row.total_tokens) === undefined ? {} : { totalTokens: number(row.total_tokens) },
10501
+ ...number(row.uncached_input_tokens) === undefined ? {} : { uncachedInputTokens: number(row.uncached_input_tokens) },
10502
+ ...number(row.cache_read_input_tokens) === undefined ? {} : { cacheReadInputTokens: number(row.cache_read_input_tokens) },
10503
+ ...number(row.cache_write_input_tokens) === undefined ? {} : { cacheWriteInputTokens: number(row.cache_write_input_tokens) },
10504
+ ...number(row.text_output_tokens) === undefined ? {} : { textOutputTokens: number(row.text_output_tokens) },
10505
+ ...number(row.reasoning_output_tokens) === undefined ? {} : { reasoningOutputTokens: number(row.reasoning_output_tokens) },
10506
+ reportingStatus: row.reporting_status
10507
+ },
10508
+ ...row.raw_usage === null ? {} : { rawUsage: parseJsonObject(row.raw_usage) },
10509
+ occurredAt: new Date(row.occurred_at),
10510
+ recordedAt: new Date(row.recorded_at)
10511
+ };
10512
+ }
10513
+ function rowToCost(row) {
10514
+ return costRecord({
10515
+ projectId: row.project_id,
10516
+ usageId: row.usage_record_id,
10517
+ status: row.status,
10518
+ providerId: row.provider_id,
10519
+ modelId: row.model_id,
10520
+ currency: row.currency,
10521
+ amountNanos: row.amount_nanos,
10522
+ reason: row.reason,
10523
+ details: row.details,
10524
+ ratedAt: row.rated_at
10525
+ });
10526
+ }
10527
+ function costFromJoinedRow(row) {
10528
+ return costRecord({
10529
+ projectId: row.project_id,
10530
+ usageId: row.id,
10531
+ status: required2(row.cost_status),
10532
+ providerId: row.cost_provider_id,
10533
+ modelId: row.cost_model_id,
10534
+ currency: row.cost_currency,
10535
+ amountNanos: row.cost_amount_nanos,
10536
+ reason: row.cost_reason,
10537
+ details: row.cost_details,
10538
+ ratedAt: required2(row.cost_rated_at)
10539
+ });
10540
+ }
10541
+ function costRecord(input) {
10542
+ const details = parseAiModelCallCostDetails(input.details);
10543
+ const base = {
10544
+ projectId: input.projectId,
10545
+ usageRecordId: input.usageId,
10546
+ pricingContext: details.pricingContext,
10547
+ priceSource: {
10548
+ ...details.priceSource,
10549
+ observedAt: new Date(details.priceSource.observedAt)
10550
+ },
10551
+ ratedAt: new Date(input.ratedAt)
10552
+ };
10553
+ if (input.status === "rated") {
10554
+ return normalizeAiModelCallCostRecord({
10555
+ ...base,
10556
+ status: "rated",
10557
+ billingIdentity: { providerId: required2(input.providerId), modelId: required2(input.modelId) },
10558
+ money: { currency: required2(input.currency), amountNanos: required2(input.amountNanos) },
10559
+ components: required2(details.components)
10560
+ });
10561
+ }
10562
+ return normalizeAiModelCallCostRecord({
10563
+ ...base,
10564
+ status: "unpriceable",
10565
+ ...input.providerId && input.modelId ? { billingIdentity: { providerId: input.providerId, modelId: input.modelId } } : {},
10566
+ reason: required2(input.reason),
10567
+ ...details.missingMeters ? { missingMeters: details.missingMeters } : {}
10568
+ });
10569
+ }
10570
+ function aggregateSummaryRows(ids, rows) {
10571
+ const map = new Map(ids.map((id) => [id, accumulator()]));
10572
+ for (const row of rows) {
10573
+ const value = map.get(row.execution_id);
10574
+ if (!value)
10575
+ continue;
10576
+ if (row.status === null)
10577
+ value.unvaluedCallCount += 1;
10578
+ else if (row.status === "unpriceable")
10579
+ value.unpriceableCallCount += 1;
10580
+ else {
10581
+ value.ratedCallCount += 1;
10582
+ add(value.amounts, required2(row.currency), required2(row.amount_nanos));
10583
+ }
10584
+ }
10585
+ return ids.map((id) => finish(map.get(id)));
10586
+ }
10587
+ function accumulator() {
10588
+ return {
10589
+ amounts: new Map,
10590
+ ratedCallCount: 0,
10591
+ unpriceableCallCount: 0,
10592
+ unvaluedCallCount: 0
10593
+ };
10594
+ }
10595
+ function finish(value) {
10596
+ return {
10597
+ amounts: [...value.amounts].sort(([left], [right]) => left.localeCompare(right)).map(([currency, amount]) => ({ currency, amountNanos: amount.toString() })),
10598
+ ratedCallCount: value.ratedCallCount,
10599
+ unpriceableCallCount: value.unpriceableCallCount,
10600
+ unvaluedCallCount: value.unvaluedCallCount
10601
+ };
10602
+ }
10603
+ function add(amounts, currency, amount) {
10604
+ amounts.set(currency, (amounts.get(currency) ?? 0n) + BigInt(amount));
10605
+ }
10606
+ function parseStringArray(value) {
10607
+ const parsed = typeof value === "string" ? JSON.parse(value) : value;
10608
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
10609
+ throw new Error("[SixbPg] AI requester group ids are invalid.");
10610
+ }
10611
+ return parsed;
10612
+ }
10613
+ function parseJsonObject(value) {
10614
+ return typeof value === "string" ? JSON.parse(value) : value;
10615
+ }
10616
+ function assertCostMatchesUsage(record, usage) {
10617
+ if (!aiModelCallCostMatchesUsage(record, usage)) {
10618
+ throw new AiCostStorageError("cost_mismatch", `[SixbPg] AI valuation for usage '${record.usageRecordId}' does not match its usage record.`);
10619
+ }
10620
+ }
10621
+ function missingUsage(record) {
10622
+ return new AiCostStorageError("missing_usage", `[SixbPg] AI usage '${record.usageRecordId}' does not exist.`);
10623
+ }
10624
+ function assertNonBlank(value, field) {
10625
+ if (typeof value !== "string" || value.trim().length === 0) {
10626
+ throw new TypeError(`[Sixb] AI ${field} must be nonblank.`);
10627
+ }
10628
+ }
10629
+ function safeSqlCount(value, field) {
10630
+ if (!/^(0|[1-9]\d*)$/.test(value)) {
10631
+ throw new Error(`[SixbPg] AI accounting ${field} is invalid.`);
10632
+ }
10633
+ const count = Number(value);
10634
+ if (!Number.isSafeInteger(count)) {
10635
+ throw new TypeError(`[Sixb] AI accounting ${field} exceeds the safe integer range.`);
10636
+ }
10637
+ return count;
10638
+ }
10639
+ function required2(value) {
10640
+ if (value === null || value === undefined) {
10641
+ throw new Error("[SixbPg] AI valuation column is unexpectedly null.");
10642
+ }
10643
+ return value;
10644
+ }
10645
+
9814
10646
  // src/pg-ai-usage-storage.ts
9815
10647
  import {
9816
10648
  AiUsageStorageError,
@@ -9904,6 +10736,17 @@ class PgAiUsageStorage {
9904
10736
  });
9905
10737
  return summary;
9906
10738
  }
10739
+ async getLatestForExecution(input) {
10740
+ assertNonBlankProjectId(input.projectId);
10741
+ assertAiUsageExecutionId(input.executionId);
10742
+ const [row] = await this.sql`
10743
+ SELECT * FROM ai_model_call_usage
10744
+ WHERE project_id = ${input.projectId} AND execution_id = ${input.executionId}
10745
+ ORDER BY occurred_at DESC, id DESC
10746
+ LIMIT 1
10747
+ `;
10748
+ return row ? this.rowToRecord(this.sql, row) : null;
10749
+ }
9907
10750
  async summarizeExecutions(input) {
9908
10751
  assertNonBlankProjectId(input.projectId);
9909
10752
  for (const executionId of input.executionIds)
@@ -9950,7 +10793,7 @@ class PgAiUsageStorage {
9950
10793
  ...row.response_model_id === null ? {} : { responseModelId: row.response_model_id },
9951
10794
  responseId: row.response_id,
9952
10795
  usage: {
9953
- ...usageFromRow(row),
10796
+ ...usageFromRow2(row),
9954
10797
  ...row.total_tokens === null ? {} : { totalTokens: Number(row.total_tokens) },
9955
10798
  reportingStatus: row.reporting_status
9956
10799
  },
@@ -9987,7 +10830,7 @@ function aggregateExecutionRows(executionIds, rows) {
9987
10830
  for (const executionId of executionIds)
9988
10831
  usageByExecutionId.set(executionId, []);
9989
10832
  for (const row of rows) {
9990
- usageByExecutionId.get(row.execution_id)?.push(usageFromRow(row));
10833
+ usageByExecutionId.get(row.execution_id)?.push(usageFromRow2(row));
9991
10834
  }
9992
10835
  return executionIds.map((executionId) => {
9993
10836
  const usages = usageByExecutionId.get(executionId) ?? [];
@@ -9997,7 +10840,7 @@ function aggregateExecutionRows(executionIds, rows) {
9997
10840
  };
9998
10841
  });
9999
10842
  }
10000
- function usageFromRow(row) {
10843
+ function usageFromRow2(row) {
10001
10844
  return {
10002
10845
  ...row.input_tokens === null ? {} : { inputTokens: Number(row.input_tokens) },
10003
10846
  ...row.output_tokens === null ? {} : { outputTokens: Number(row.output_tokens) },
@@ -10166,6 +11009,9 @@ function toStorageRow(row) {
10166
11009
  };
10167
11010
  }
10168
11011
 
11012
+ // src/pg-object-storage.ts
11013
+ import { linkBatchKey, objectBatchKey } from "@sixb/core/storage";
11014
+
10169
11015
  // src/pg-object-query-compiler.ts
10170
11016
  import {
10171
11017
  ObjectQueryExecutionError
@@ -10231,6 +11077,8 @@ function compileObjectQueryInternal(projectId, query, ctx) {
10231
11077
  switch (query.kind) {
10232
11078
  case "start":
10233
11079
  return compileStart(projectId, query);
11080
+ case "refs":
11081
+ return compileRefs(projectId, query);
10234
11082
  case "filter":
10235
11083
  return compileFilter(projectId, query.input, query.predicate);
10236
11084
  case "sort":
@@ -10277,6 +11125,50 @@ function compileStart(projectId, query) {
10277
11125
  }
10278
11126
  };
10279
11127
  }
11128
+ function compileRefs(projectId, query) {
11129
+ if (query.refs.length === 0) {
11130
+ throw new Error("[SixbPg] PostgreSQL object storage requires at least one ref");
11131
+ }
11132
+ const order = compileOrder(identityOrderFields());
11133
+ const selectedOrder = compileOrder(identityOrderFields(), "selected");
11134
+ const refsJson = JSON.stringify(query.refs);
11135
+ const requested = `
11136
+ SELECT DISTINCT
11137
+ ref.value ->> 'objectTypeId' AS object_type_id,
11138
+ ref.value ->> 'primaryId' AS primary_id
11139
+ FROM jsonb_array_elements(?::text::jsonb) AS ref(value)
11140
+ `;
11141
+ const sql = `
11142
+ WITH requested AS (${requested})
11143
+ SELECT selected.*, selected.properties AS _cursor_properties
11144
+ FROM requested
11145
+ JOIN objects AS selected
11146
+ ON selected.project_id = ?
11147
+ AND selected.object_type_id = requested.object_type_id
11148
+ AND selected.primary_id = requested.primary_id
11149
+ ORDER BY ${selectedOrder.sql}
11150
+ `;
11151
+ return {
11152
+ sql,
11153
+ args: [refsJson, projectId, ...selectedOrder.args],
11154
+ totalSql: `
11155
+ WITH requested AS (${requested})
11156
+ SELECT COUNT(*)::bigint AS total
11157
+ FROM requested
11158
+ JOIN objects AS selected
11159
+ ON selected.project_id = ?
11160
+ AND selected.object_type_id = requested.object_type_id
11161
+ AND selected.primary_id = requested.primary_id
11162
+ `,
11163
+ totalArgs: [refsJson, projectId],
11164
+ order,
11165
+ hasMore: () => false,
11166
+ trimRows: identityRows,
11167
+ nextPageToken: () => {
11168
+ return;
11169
+ }
11170
+ };
11171
+ }
10280
11172
  function compileFilter(projectId, inputQuery, predicateNode) {
10281
11173
  return compileWhere(projectId, inputQuery, compilePredicate(predicateNode));
10282
11174
  }
@@ -10641,6 +11533,8 @@ function compileAggregateSource(projectId, query) {
10641
11533
  switch (query.kind) {
10642
11534
  case "start":
10643
11535
  return compileAggregateStart(projectId, query);
11536
+ case "refs":
11537
+ return compileAggregateRefs(projectId, query);
10644
11538
  case "filter":
10645
11539
  return compileAggregateWhere(projectId, query.input, compilePredicate(query.predicate));
10646
11540
  case "text": {
@@ -10658,7 +11552,7 @@ function compileAggregateSource(projectId, query) {
10658
11552
  case "page":
10659
11553
  return compileRowQueryAggregateSource(projectId, query);
10660
11554
  case "vector":
10661
- throw new Error(`[SixbPg] PostgreSQL object storage does not support query node 'vector'`);
11555
+ throw new Error(`[SixbPg] PostgreSQL object storage does not support query node '${query.kind}'`);
10662
11556
  }
10663
11557
  }
10664
11558
  function compileAggregateStart(projectId, query) {
@@ -10674,6 +11568,27 @@ function compileAggregateStart(projectId, query) {
10674
11568
  args: [projectId, query.objectTypeId]
10675
11569
  };
10676
11570
  }
11571
+ function compileAggregateRefs(projectId, query) {
11572
+ if (query.refs.length === 0) {
11573
+ throw new Error("[SixbPg] PostgreSQL object storage requires at least one ref");
11574
+ }
11575
+ return {
11576
+ sql: `
11577
+ SELECT selected.project_id, selected.object_type_id, selected.primary_id, selected.properties
11578
+ FROM (
11579
+ SELECT DISTINCT
11580
+ ref.value ->> 'objectTypeId' AS object_type_id,
11581
+ ref.value ->> 'primaryId' AS primary_id
11582
+ FROM jsonb_array_elements(?::text::jsonb) AS ref(value)
11583
+ ) AS requested
11584
+ JOIN objects AS selected
11585
+ ON selected.project_id = ?
11586
+ AND selected.object_type_id = requested.object_type_id
11587
+ AND selected.primary_id = requested.primary_id
11588
+ `,
11589
+ args: [JSON.stringify(query.refs), projectId]
11590
+ };
11591
+ }
10677
11592
  function compileAggregateWhere(projectId, inputQuery, predicate) {
10678
11593
  const input = compileAggregateSource(projectId, inputQuery);
10679
11594
  return {
@@ -11219,6 +12134,7 @@ var PG_OBJECT_QUERY_CAPABILITIES = {
11219
12134
  facetObjects: true,
11220
12135
  nodes: {
11221
12136
  start: true,
12137
+ refs: true,
11222
12138
  filter: true,
11223
12139
  text: true,
11224
12140
  sort: true,
@@ -11270,7 +12186,7 @@ var PG_OBJECT_QUERY_CAPABILITIES = {
11270
12186
  stablePageTokens: true
11271
12187
  },
11272
12188
  notes: [
11273
- "PostgreSQL object query pushdown supports start/filter/text/sort/limit/page/traverse/set/project/expand over JSONB properties and object links.",
12189
+ "PostgreSQL object query pushdown supports start/refs/filter/text/sort/limit/page/traverse/set/project/expand over JSONB properties and object links.",
11274
12190
  "expand hydrates linked objects in-database (top-N per parent via LATERAL + jsonb_agg); core resolves each expansion's cardinality before pushdown, and a mixed/unresolved one stays on the fallback.",
11275
12191
  "Exact decimal predicates, ordering, and keyset pagination use PostgreSQL numeric casts.",
11276
12192
  "Relevance sorting, vector search, and unresolved start.includeSubtypes remain planner fallback or rejection cases."
@@ -11357,7 +12273,7 @@ class PgObjectStorage {
11357
12273
  return result;
11358
12274
  const rows = await valuesJoin(this.sql, "SELECT o.* FROM objects o", ["object_type_id", "primary_id"], params.items.map((i) => [i.objectTypeId, i.primaryId]), `WHERE o.project_id = $1`, [params.projectId]);
11359
12275
  for (const row of rows) {
11360
- result.set(`${row.object_type_id}:${row.primary_id}`, rowToObject(row));
12276
+ result.set(objectBatchKey(row.object_type_id, row.primary_id), rowToObject(row));
11361
12277
  }
11362
12278
  return result;
11363
12279
  }
@@ -11367,13 +12283,73 @@ class PgObjectStorage {
11367
12283
  return result;
11368
12284
  const rows = await valuesJoin(this.sql, "SELECT l.* FROM links l", ["source_type_id", "source_id", "link_id"], params.items.map((i) => [i.objectTypeId, i.objectId, i.linkId]), `WHERE l.project_id = $1`, [params.projectId]);
11369
12285
  for (const row of rows) {
11370
- const key = `${row.source_type_id}:${row.source_id}:${row.link_id}`;
12286
+ const key = linkBatchKey(row.source_type_id, row.source_id, row.link_id);
11371
12287
  const existing = result.get(key) ?? [];
11372
12288
  existing.push(rowToLink(row));
11373
12289
  result.set(key, existing);
11374
12290
  }
11375
12291
  return result;
11376
12292
  }
12293
+ async queryLinks(params) {
12294
+ assertLinkQueryLimit(params.limit);
12295
+ if (params.objectRefs.length === 0 || params.endpointObjectTypeIds?.length === 0) {
12296
+ return { links: [], hasMore: false };
12297
+ }
12298
+ const args = [JSON.stringify(params.objectRefs), params.projectId];
12299
+ const addArg = (value) => {
12300
+ args.push(value);
12301
+ return `$${args.length}`;
12302
+ };
12303
+ const sourceJoin = `
12304
+ SELECT link.*
12305
+ FROM links AS link
12306
+ JOIN requested
12307
+ ON requested.object_type_id = link.source_type_id
12308
+ AND requested.object_id = link.source_id
12309
+ WHERE link.project_id = $2
12310
+ `;
12311
+ const targetJoin = `
12312
+ SELECT link.*
12313
+ FROM links AS link
12314
+ JOIN requested
12315
+ ON requested.object_type_id = link.target_type_id
12316
+ AND requested.object_id = link.target_id
12317
+ WHERE link.project_id = $2
12318
+ `;
12319
+ const incidentSql = params.direction === "outgoing" ? sourceJoin : params.direction === "incoming" ? targetJoin : `${sourceJoin} UNION ${targetJoin}`;
12320
+ const predicates = [];
12321
+ if (params.linkId !== undefined) {
12322
+ predicates.push(`link_id = ${addArg(params.linkId)}::text`);
12323
+ }
12324
+ if (params.endpointObjectTypeIds !== undefined) {
12325
+ const allowedTypes = addArg(JSON.stringify([...new Set(params.endpointObjectTypeIds)]));
12326
+ predicates.push(`source_type_id IN (SELECT jsonb_array_elements_text(${allowedTypes}::text::jsonb))`, `target_type_id IN (SELECT jsonb_array_elements_text(${allowedTypes}::text::jsonb))`);
12327
+ }
12328
+ if (params.after) {
12329
+ const cursor = params.after.map((value) => `${addArg(value)}::text`);
12330
+ predicates.push(`(source_type_id, source_id, link_id, target_type_id, target_id) > (${cursor.join(", ")})`);
12331
+ }
12332
+ const limit = addArg(params.limit + 1);
12333
+ const whereSql = predicates.length > 0 ? `WHERE ${predicates.join(" AND ")}` : "";
12334
+ const rows = await this.sql.unsafe(`
12335
+ WITH requested AS (
12336
+ SELECT DISTINCT
12337
+ requested."objectTypeId" AS object_type_id,
12338
+ requested."primaryId" AS object_id
12339
+ FROM jsonb_to_recordset($1::text::jsonb)
12340
+ AS requested("objectTypeId" text, "primaryId" text)
12341
+ ), incident AS (${incidentSql})
12342
+ SELECT *
12343
+ FROM incident
12344
+ ${whereSql}
12345
+ ORDER BY source_type_id, source_id, link_id, target_type_id, target_id
12346
+ LIMIT ${limit}
12347
+ `, args);
12348
+ return {
12349
+ links: rows.slice(0, params.limit).map((row) => rowToLink(row)),
12350
+ hasMore: rows.length > params.limit
12351
+ };
12352
+ }
11377
12353
  async listIncidentLinksBatch(params) {
11378
12354
  if (params.items.length === 0)
11379
12355
  return [];
@@ -11383,7 +12359,7 @@ class PgObjectStorage {
11383
12359
  const deduped = new Map;
11384
12360
  for (const row of [...sourceRows, ...targetRows]) {
11385
12361
  const link = rowToLink(row);
11386
- deduped.set(`${link.sourceTypeId}:${link.sourceId}:${link.linkId}:${link.targetTypeId}:${link.targetId}`, link);
12362
+ deduped.set(linkIdentity(link), link);
11387
12363
  }
11388
12364
  return [...deduped.values()];
11389
12365
  }
@@ -11565,6 +12541,20 @@ function rowToLink(row) {
11565
12541
  lastCommitId: row.last_commit_id
11566
12542
  };
11567
12543
  }
12544
+ function linkIdentity(link) {
12545
+ return JSON.stringify([
12546
+ link.sourceTypeId,
12547
+ link.sourceId,
12548
+ link.linkId,
12549
+ link.targetTypeId,
12550
+ link.targetId
12551
+ ]);
12552
+ }
12553
+ function assertLinkQueryLimit(limit) {
12554
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
12555
+ throw new Error("Object link query limit must be a positive safe integer.");
12556
+ }
12557
+ }
11568
12558
 
11569
12559
  // src/pg-pipeline-run-storage.ts
11570
12560
  import { parseSixbFailure as parseSixbFailure3, serializeSixbFailure as serializeSixbFailure3 } from "@sixb/core/internal/errors";
@@ -14190,6 +15180,7 @@ class PostgresStorage {
14190
15180
  executions;
14191
15181
  agents;
14192
15182
  aiUsage;
15183
+ aiCosts;
14193
15184
  actionRuns;
14194
15185
  pipelineRuns;
14195
15186
  workflowRuns;
@@ -14239,6 +15230,7 @@ class PostgresStorage {
14239
15230
  this.executions = createOperationScopedFacade(stores.executions, scope);
14240
15231
  this.agents = createAgentOperationScope(stores.agents, scope);
14241
15232
  this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope);
15233
+ this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope);
14242
15234
  this.actionRuns = createOperationScopedFacade(stores.actionRuns, scope);
14243
15235
  this.pipelineRuns = createOperationScopedFacade(stores.pipelineRuns, scope);
14244
15236
  this.workflowRuns = createWorkflowRunOperationScope(stores.workflowRuns, scope);
@@ -14327,6 +15319,7 @@ function createPostgresStores(sql, options) {
14327
15319
  executions,
14328
15320
  agents: new PgAgentStorage({ sql, executions }),
14329
15321
  aiUsage: new PgAiUsageStorage(sql),
15322
+ aiCosts: new PgAiCostStorage(sql),
14330
15323
  actionRuns: new PgActionRunStorage(sql, executions),
14331
15324
  pipelineRuns: new PgPipelineRunStorage(sql, executions),
14332
15325
  workflowRuns: new PgWorkflowRunStorage(sql, executions),
@@ -14353,4 +15346,4 @@ export {
14353
15346
  PostgresStorage
14354
15347
  };
14355
15348
 
14356
- //# debugId=C543A4DF0CE709F964756E2164756E21
15349
+ //# debugId=0531F72AD7BC178764756E2164756E21