@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.
- package/dist/agents/agent-storage.d.ts +5 -3
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/checkpoints.d.ts +16 -0
- package/dist/agents/checkpoints.d.ts.map +1 -0
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/messages.d.ts +2 -1
- package/dist/agents/messages.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +18 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1028 -35
- package/dist/index.js.map +15 -13
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/dist/pg-ai-cost-storage.d.ts +12 -0
- package/dist/pg-ai-cost-storage.d.ts.map +1 -0
- package/dist/pg-ai-usage-storage.d.ts +2 -1
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-object-storage.d.ts +4 -3
- package/dist/pg-object-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +6 -3
- package/src/agents/checkpoints.ts +204 -0
- package/src/agents/index.ts +1 -0
- package/src/agents/messages.ts +71 -13
- package/src/agents/rows.ts +45 -0
- package/src/index.ts +7 -1
- package/src/migrations/026-ai-cost-accounting.sql +53 -0
- package/src/migrations/027-agent-context-checkpoints.sql +34 -0
- package/src/migrations/028-object-override-edit-times.sql +3 -0
- package/src/migrations.ts +10 -0
- package/src/ontology-storage/materialization-state.ts +2 -0
- package/src/ontology-storage/materialization-writer.ts +5 -3
- package/src/pg-ai-cost-storage.ts +707 -0
- package/src/pg-ai-usage-storage.ts +15 -0
- package/src/pg-object-query-compiler.ts +81 -1
- 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/
|
|
20
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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',
|
|
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
|
-
...
|
|
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(
|
|
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
|
|
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 '
|
|
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(
|
|
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 =
|
|
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(
|
|
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=
|
|
15349
|
+
//# debugId=0531F72AD7BC178764756E2164756E21
|