@sixb/pg 0.1.6 → 0.1.8
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/rows.d.ts +8 -5
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +3 -2
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts +5 -0
- package/dist/ai-cost-groups.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +4 -1
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/device-authorizations.d.ts +29 -0
- package/dist/auth-storage/device-authorizations.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +19 -1
- package/dist/auth-storage/rows.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +1 -1
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8327 -5838
- package/dist/index.js.map +28 -19
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/index.d.ts +2 -0
- package/dist/objects/index.d.ts.map +1 -0
- package/dist/{pg-object-query-compiler.d.ts → objects/query-compiler.d.ts} +25 -4
- package/dist/objects/query-compiler.d.ts.map +1 -0
- package/dist/objects/read-scope.d.ts +17 -0
- package/dist/objects/read-scope.d.ts.map +1 -0
- package/dist/objects/reader.d.ts +50 -0
- package/dist/objects/reader.d.ts.map +1 -0
- package/dist/objects/rows.d.ts +47 -0
- package/dist/objects/rows.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts +8 -0
- package/dist/objects/selected-reader.d.ts.map +1 -0
- package/dist/{pg-object-storage.d.ts → objects/storage.d.ts} +12 -20
- package/dist/objects/storage.d.ts.map +1 -0
- package/dist/pg-ai-cost-storage.d.ts +2 -1
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-limit-storage.d.ts +34 -0
- package/dist/pg-ai-limit-storage.d.ts.map +1 -0
- package/dist/pg-ai-usage-storage.d.ts +28 -0
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-execution-storage.d.ts +3 -2
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/share-grant-storage.d.ts +11 -0
- package/dist/share-grant-storage.d.ts.map +1 -0
- package/dist/share-session-storage.d.ts +11 -0
- package/dist/share-session-storage.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -1
- package/dist/transactions.d.ts +9 -2
- package/dist/transactions.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/agents/rows.ts +38 -9
- package/src/agents/runs.ts +148 -11
- package/src/agents/threads.ts +1 -17
- package/src/ai-cost-groups.ts +102 -0
- package/src/auth-storage/auth-storage.ts +84 -0
- package/src/auth-storage/device-authorizations.ts +131 -0
- package/src/auth-storage/rows.ts +41 -0
- package/src/auth-storage/shared.ts +1 -0
- package/src/index.ts +24 -4
- package/src/migrations/029-model-accounting.sql +24 -0
- package/src/migrations/030-ai-usage-limits.sql +94 -0
- package/src/migrations/031-subagent-runs.sql +97 -0
- package/src/migrations/032-conversation-run-spec.sql +23 -0
- package/src/migrations/033-retire-agent-definitions.sql +28 -0
- package/src/migrations/034-device-authorizations.sql +24 -0
- package/src/migrations/035-share-grants.sql +57 -0
- package/src/migrations/036-share-sessions.sql +173 -0
- package/src/migrations.ts +22 -0
- package/src/objects/index.ts +1 -0
- package/src/{pg-object-query-compiler.ts → objects/query-compiler.ts} +185 -100
- package/src/objects/read-scope.ts +324 -0
- package/src/objects/reader.ts +604 -0
- package/src/objects/rows.ts +146 -0
- package/src/objects/selected-reader.ts +118 -0
- package/src/objects/storage.ts +305 -0
- package/src/pg-ai-cost-storage.ts +71 -24
- package/src/pg-ai-limit-storage.ts +901 -0
- package/src/pg-ai-usage-storage.ts +34 -0
- package/src/pg-execution-storage.ts +21 -2
- package/src/pg-workflow-run-storage.ts +8 -8
- package/src/share-grant-storage.ts +236 -0
- package/src/share-session-storage.ts +127 -0
- package/src/storage-errors.ts +6 -0
- package/src/transactions.ts +46 -5
- package/dist/pg-object-query-compiler.d.ts.map +0 -1
- package/dist/pg-object-storage.d.ts.map +0 -1
- package/src/pg-object-storage.ts +0 -734
package/src/agents/runs.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
assertAgentRunExecution,
|
|
3
|
+
assertConversationAgentRunSpec,
|
|
4
|
+
assertCreateSubagentRunInput,
|
|
5
|
+
assertSubagentRunResult,
|
|
6
|
+
subagentRunMatchesCreateInput,
|
|
7
|
+
} from "@sixb/core/internal/agent-run-storage-provider"
|
|
2
8
|
import { normalizeRequesterGroupIds } from "@sixb/core/internal/auth"
|
|
3
9
|
import { serializeSixbFailure } from "@sixb/core/internal/errors"
|
|
4
10
|
import {
|
|
@@ -7,7 +13,9 @@ import {
|
|
|
7
13
|
type AgentRunStore,
|
|
8
14
|
AgentStorageError,
|
|
9
15
|
type ConfirmAgentRunExecutionOwnershipInput,
|
|
16
|
+
type ConversationAgentRunRecord,
|
|
10
17
|
type CreateAgentRunInput,
|
|
18
|
+
type CreateSubagentRunInput,
|
|
11
19
|
type ExecutionStorage,
|
|
12
20
|
type FinishAgentRunInput,
|
|
13
21
|
type FinishQueuedAgentRunInput,
|
|
@@ -15,6 +23,7 @@ import {
|
|
|
15
23
|
type ListAgentRunsResult,
|
|
16
24
|
type ReclaimAgentRunInput,
|
|
17
25
|
type StartAgentRunInput,
|
|
26
|
+
type SubagentRunRecord,
|
|
18
27
|
} from "@sixb/core/storage"
|
|
19
28
|
import type { SQLClient, SqlParameter } from "../pg-client"
|
|
20
29
|
import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "../run-list-query"
|
|
@@ -30,14 +39,14 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
30
39
|
private readonly executions: ExecutionStorage
|
|
31
40
|
) {}
|
|
32
41
|
|
|
33
|
-
async create(input: CreateAgentRunInput): Promise<
|
|
42
|
+
async create(input: CreateAgentRunInput): Promise<ConversationAgentRunRecord> {
|
|
43
|
+
assertConversationAgentRunSpec(input.spec, "SixbPg")
|
|
34
44
|
const createdAt = input.createdAt ?? new Date()
|
|
35
45
|
await assertAgentRunExecution({
|
|
36
46
|
executions: this.executions,
|
|
37
47
|
projectId: input.projectId,
|
|
38
48
|
executionId: input.executionId,
|
|
39
49
|
runId: input.id,
|
|
40
|
-
agentId: input.agentId,
|
|
41
50
|
})
|
|
42
51
|
|
|
43
52
|
try {
|
|
@@ -54,7 +63,6 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
54
63
|
`[SixbPg] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`
|
|
55
64
|
)
|
|
56
65
|
}
|
|
57
|
-
|
|
58
66
|
if (thread.active_run_id !== null) {
|
|
59
67
|
throw new AgentStorageError(
|
|
60
68
|
"active_run_exists",
|
|
@@ -67,9 +75,10 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
67
75
|
project_id,
|
|
68
76
|
id,
|
|
69
77
|
execution_id,
|
|
78
|
+
kind,
|
|
70
79
|
thread_id,
|
|
71
|
-
agent_id,
|
|
72
80
|
trigger_message_id,
|
|
81
|
+
spec,
|
|
73
82
|
requester_group_ids,
|
|
74
83
|
status,
|
|
75
84
|
attempt,
|
|
@@ -78,9 +87,10 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
78
87
|
${input.projectId},
|
|
79
88
|
${input.id},
|
|
80
89
|
${input.executionId},
|
|
90
|
+
${"conversation"},
|
|
81
91
|
${input.threadId},
|
|
82
|
-
${input.agentId},
|
|
83
92
|
${input.triggerMessageId},
|
|
93
|
+
${JSON.stringify(input.spec)}::text::jsonb,
|
|
84
94
|
${JSON.stringify(normalizeRequesterGroupIds(input.requesterGroupIds))}::text::jsonb,
|
|
85
95
|
${"queued"},
|
|
86
96
|
${0},
|
|
@@ -94,7 +104,11 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
94
104
|
WHERE project_id = ${input.projectId} AND id = ${input.threadId}
|
|
95
105
|
`
|
|
96
106
|
|
|
97
|
-
|
|
107
|
+
const created = rowToRunRecord(row)
|
|
108
|
+
if (created.kind !== "conversation") {
|
|
109
|
+
throw new Error(`[SixbPg] Agent run insert '${input.id}' returned the wrong run kind.`)
|
|
110
|
+
}
|
|
111
|
+
return created
|
|
98
112
|
})
|
|
99
113
|
} catch (error) {
|
|
100
114
|
if (isUniqueViolation(error)) {
|
|
@@ -108,6 +122,108 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
124
|
|
|
125
|
+
async createSubagent(input: CreateSubagentRunInput): Promise<SubagentRunRecord> {
|
|
126
|
+
assertCreateSubagentRunInput(input, "SixbPg")
|
|
127
|
+
const execution = await assertAgentRunExecution({
|
|
128
|
+
executions: this.executions,
|
|
129
|
+
projectId: input.projectId,
|
|
130
|
+
executionId: input.executionId,
|
|
131
|
+
runId: input.id,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
return await runPgTransaction(this.sql, async (tx) => {
|
|
136
|
+
const [parent] = await tx<AgentRunRow[]>`
|
|
137
|
+
SELECT * FROM agent_runs
|
|
138
|
+
WHERE project_id = ${input.projectId} AND id = ${input.parentRunId}
|
|
139
|
+
FOR UPDATE
|
|
140
|
+
`
|
|
141
|
+
if (!parent || parent.kind !== "conversation") {
|
|
142
|
+
throw new AgentStorageError(
|
|
143
|
+
"run_not_found",
|
|
144
|
+
`[SixbPg] Parent Agent run '${input.parentRunId}' was not found.`
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const [existingRow] = await tx<AgentRunRow[]>`
|
|
149
|
+
SELECT * FROM agent_runs
|
|
150
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
151
|
+
`
|
|
152
|
+
if (existingRow) {
|
|
153
|
+
const existing = rowToRunRecord(existingRow)
|
|
154
|
+
if (subagentRunMatchesCreateInput(existing, input)) return existing
|
|
155
|
+
throw new AgentStorageError(
|
|
156
|
+
"duplicate_id",
|
|
157
|
+
`[SixbPg] Subagent run '${input.id}' already exists with different immutable inputs.`
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
if (parent.status !== "running") {
|
|
161
|
+
throw new AgentStorageError(
|
|
162
|
+
"invalid_state",
|
|
163
|
+
`[SixbPg] Parent Agent run '${parent.id}' is not running (status '${parent.status}').`
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
if (
|
|
167
|
+
execution.source.type !== "execution" ||
|
|
168
|
+
execution.source.executionId !== parent.execution_id
|
|
169
|
+
) {
|
|
170
|
+
throw new AgentStorageError(
|
|
171
|
+
"invalid_input",
|
|
172
|
+
`[SixbPg] Subagent execution '${execution.id}' is not a child of parent execution '${parent.execution_id}'.`
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
if (parent.execution_token !== input.parentExecutionToken) {
|
|
176
|
+
throw new AgentStorageError(
|
|
177
|
+
"execution_lost",
|
|
178
|
+
`[SixbPg] Parent Agent run '${parent.id}' is no longer owned by this execution.`
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
const parentRecord = rowToRunRecord(parent)
|
|
182
|
+
|
|
183
|
+
const [countRow] = await tx<{ count: string | number }[]>`
|
|
184
|
+
SELECT COUNT(*)::bigint AS count FROM agent_runs
|
|
185
|
+
WHERE project_id = ${input.projectId}
|
|
186
|
+
AND parent_run_id = ${input.parentRunId}
|
|
187
|
+
AND status IN (${"queued"}, ${"running"})
|
|
188
|
+
`
|
|
189
|
+
const activeChildren = Number(countRow?.count ?? 0)
|
|
190
|
+
if (activeChildren >= input.maxActiveChildren) {
|
|
191
|
+
throw new AgentStorageError(
|
|
192
|
+
"active_child_limit",
|
|
193
|
+
`[SixbPg] Agent run '${parent.id}' already has ${activeChildren} active subagents.`
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const [row] = await tx<AgentRunRow[]>`
|
|
198
|
+
INSERT INTO agent_runs (
|
|
199
|
+
project_id, id, execution_id, kind, parent_run_id, spawn_key, spec,
|
|
200
|
+
requester_group_ids, status, attempt, created_at
|
|
201
|
+
) VALUES (
|
|
202
|
+
${input.projectId}, ${input.id}, ${input.executionId}, ${"subagent"},
|
|
203
|
+
${input.parentRunId}, ${input.spawnKey},
|
|
204
|
+
${JSON.stringify(input.spec)}::text::jsonb,
|
|
205
|
+
${JSON.stringify(parentRecord.requesterGroupIds)}::text::jsonb,
|
|
206
|
+
${"queued"}, ${0}, ${input.createdAt ?? new Date()}
|
|
207
|
+
)
|
|
208
|
+
RETURNING *
|
|
209
|
+
`
|
|
210
|
+
const created = rowToRunRecord(row)
|
|
211
|
+
if (created.kind !== "subagent") {
|
|
212
|
+
throw new Error(`[SixbPg] Subagent insert '${input.id}' returned the wrong run kind.`)
|
|
213
|
+
}
|
|
214
|
+
return created
|
|
215
|
+
})
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (isUniqueViolation(error)) {
|
|
218
|
+
throw new AgentStorageError(
|
|
219
|
+
"duplicate_id",
|
|
220
|
+
`[SixbPg] Subagent run '${input.id}' conflicts with an existing durable spawn.`
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
throw error
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
111
227
|
async start(input: StartAgentRunInput): Promise<AgentRunRecord> {
|
|
112
228
|
const startedAt = input.startedAt ?? new Date()
|
|
113
229
|
return runPgTransaction(this.sql, async (tx) => {
|
|
@@ -198,6 +314,20 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
198
314
|
return runPgTransaction(this.sql, async (tx) => {
|
|
199
315
|
const run = await this.lockRunning(tx, input.projectId, input.id)
|
|
200
316
|
|
|
317
|
+
if (input.status === "succeeded" && run.kind === "subagent") {
|
|
318
|
+
assertSubagentRunResult(input.result, run.id, "SixbPg")
|
|
319
|
+
}
|
|
320
|
+
if (
|
|
321
|
+
input.status === "succeeded" &&
|
|
322
|
+
run.kind === "conversation" &&
|
|
323
|
+
input.result !== undefined
|
|
324
|
+
) {
|
|
325
|
+
throw new AgentStorageError(
|
|
326
|
+
"invalid_input",
|
|
327
|
+
`[SixbPg] Conversational Agent run '${run.id}' cannot persist a subagent result.`
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
201
331
|
if (run.execution_token !== input.executionToken) {
|
|
202
332
|
throw new AgentStorageError(
|
|
203
333
|
"execution_lost",
|
|
@@ -213,6 +343,7 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
213
343
|
finish_reason = ${input.finishReason ?? null},
|
|
214
344
|
error = ${errorValue}::text::jsonb,
|
|
215
345
|
diagnostics = ${input.diagnostics === undefined ? null : JSON.stringify(input.diagnostics)}::text::jsonb,
|
|
346
|
+
result = ${input.status === "succeeded" && input.result !== undefined ? JSON.stringify(input.result) : null}::text::jsonb,
|
|
216
347
|
execution_token = ${null},
|
|
217
348
|
execution_queue_lease_expires_at = ${null},
|
|
218
349
|
completed_at = ${completedAt}
|
|
@@ -262,7 +393,7 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
262
393
|
}
|
|
263
394
|
|
|
264
395
|
async list(input: ListAgentRunsInput): Promise<ListAgentRunsResult> {
|
|
265
|
-
if (hasEmptyStatuses(input)) {
|
|
396
|
+
if (hasEmptyStatuses(input) || input.kinds?.length === 0) {
|
|
266
397
|
return { runs: [], hasMore: false, total: 0 }
|
|
267
398
|
}
|
|
268
399
|
|
|
@@ -270,14 +401,19 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
270
401
|
const params: SqlParameter[] = [input.projectId]
|
|
271
402
|
let index = 2
|
|
272
403
|
|
|
404
|
+
if (input.kinds) {
|
|
405
|
+
whereClauses.push(`kind = ANY($${index++}::text[])`)
|
|
406
|
+
params.push(input.kinds as string[])
|
|
407
|
+
}
|
|
408
|
+
|
|
273
409
|
if (input.threadId) {
|
|
274
410
|
whereClauses.push(`thread_id = $${index++}`)
|
|
275
411
|
params.push(input.threadId)
|
|
276
412
|
}
|
|
277
413
|
|
|
278
|
-
if (input.
|
|
279
|
-
whereClauses.push(`
|
|
280
|
-
params.push(input.
|
|
414
|
+
if (input.parentRunId) {
|
|
415
|
+
whereClauses.push(`parent_run_id = $${index++}`)
|
|
416
|
+
params.push(input.parentRunId)
|
|
281
417
|
}
|
|
282
418
|
|
|
283
419
|
index = appendRunListFilters(
|
|
@@ -336,6 +472,7 @@ export class PgAgentRunStore implements AgentRunStore {
|
|
|
336
472
|
}
|
|
337
473
|
|
|
338
474
|
private async releaseThread(sql: SQLClient, run: AgentRunRow, completedAt: Date): Promise<void> {
|
|
475
|
+
if (run.kind !== "conversation") return
|
|
339
476
|
await sql`
|
|
340
477
|
UPDATE agent_threads SET active_run_id = ${null}, updated_at = ${completedAt}
|
|
341
478
|
WHERE project_id = ${run.project_id} AND id = ${run.thread_id} AND active_run_id = ${run.id}
|
package/src/agents/threads.ts
CHANGED
|
@@ -23,7 +23,6 @@ export class PgAgentThreadStore implements AgentThreadStore {
|
|
|
23
23
|
INSERT INTO agent_threads (
|
|
24
24
|
project_id,
|
|
25
25
|
id,
|
|
26
|
-
agent_id,
|
|
27
26
|
owner_principal_type,
|
|
28
27
|
owner_principal_id,
|
|
29
28
|
title,
|
|
@@ -33,7 +32,6 @@ export class PgAgentThreadStore implements AgentThreadStore {
|
|
|
33
32
|
) VALUES (
|
|
34
33
|
${input.projectId},
|
|
35
34
|
${input.id},
|
|
36
|
-
${input.agentId},
|
|
37
35
|
${input.ownerPrincipal.type},
|
|
38
36
|
${input.ownerPrincipal.id},
|
|
39
37
|
${input.title ?? null},
|
|
@@ -66,10 +64,7 @@ export class PgAgentThreadStore implements AgentThreadStore {
|
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
async list(input: ListAgentThreadsInput): Promise<ListAgentThreadsResult> {
|
|
69
|
-
if (
|
|
70
|
-
(input.statuses !== undefined && input.statuses.length === 0) ||
|
|
71
|
-
(input.agentIds !== undefined && input.agentIds.length === 0)
|
|
72
|
-
) {
|
|
67
|
+
if (input.statuses !== undefined && input.statuses.length === 0) {
|
|
73
68
|
return { threads: [], hasMore: false, total: 0 }
|
|
74
69
|
}
|
|
75
70
|
|
|
@@ -77,17 +72,6 @@ export class PgAgentThreadStore implements AgentThreadStore {
|
|
|
77
72
|
const params: SqlParameter[] = [input.projectId]
|
|
78
73
|
let index = 2
|
|
79
74
|
|
|
80
|
-
if (input.agentId) {
|
|
81
|
-
whereClauses.push(`agent_id = $${index++}`)
|
|
82
|
-
params.push(input.agentId)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (input.agentIds) {
|
|
86
|
-
const placeholders = input.agentIds.map(() => `$${index++}`)
|
|
87
|
-
whereClauses.push(`agent_id IN (${placeholders.join(", ")})`)
|
|
88
|
-
params.push(...input.agentIds)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
75
|
if (input.statuses) {
|
|
92
76
|
const placeholders = input.statuses.map(() => `$${index++}`)
|
|
93
77
|
whereClauses.push(`status IN (${placeholders.join(", ")})`)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AiModelCallGroupRow,
|
|
3
|
+
aiModelCallGroupFragmentFromRow,
|
|
4
|
+
buildAiModelCallGroupsFromFragments,
|
|
5
|
+
normalizeAiModelCallAccountingQuery,
|
|
6
|
+
} from "@sixb/core/internal/ai-cost-storage-provider"
|
|
7
|
+
import type { ListAiModelCallGroupsInput, ListAiModelCallGroupsResult } from "@sixb/core/storage"
|
|
8
|
+
import type { PgStoreClient } from "./transactions"
|
|
9
|
+
|
|
10
|
+
/** Paginate initiating executions, then aggregate their matching calls without loading usage payloads. */
|
|
11
|
+
export async function listAiModelCallGroups(
|
|
12
|
+
sql: PgStoreClient,
|
|
13
|
+
input: ListAiModelCallGroupsInput
|
|
14
|
+
): Promise<ListAiModelCallGroupsResult> {
|
|
15
|
+
const query = normalizeAiModelCallAccountingQuery(input)
|
|
16
|
+
const parameters = [
|
|
17
|
+
query.projectId,
|
|
18
|
+
query.from.toISOString(),
|
|
19
|
+
query.to.toISOString(),
|
|
20
|
+
query.providerId ?? null,
|
|
21
|
+
query.modelId ?? null,
|
|
22
|
+
query.valuationStatus ?? null,
|
|
23
|
+
]
|
|
24
|
+
const [count] = await sql.unsafe<{ total: string }[]>(COUNT_SQL, parameters)
|
|
25
|
+
const rows = await sql.unsafe<AiModelCallGroupRow[]>(PAGE_SQL, [
|
|
26
|
+
...parameters,
|
|
27
|
+
query.limit,
|
|
28
|
+
query.offset,
|
|
29
|
+
])
|
|
30
|
+
const total = Number(count?.total)
|
|
31
|
+
if (!Number.isSafeInteger(total) || total < 0)
|
|
32
|
+
throw new RangeError("[Sixb] Invalid model-call group count.")
|
|
33
|
+
const items = buildAiModelCallGroupsFromFragments(rows.map(aiModelCallGroupFragmentFromRow))
|
|
34
|
+
return { items, total, hasMore: query.offset + items.length < total }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const FILTERED_SQL = `
|
|
38
|
+
WITH filtered AS (
|
|
39
|
+
SELECT COALESCE(root_agent.execution_id, usage.execution_id) AS root_execution_id,
|
|
40
|
+
usage.execution_id, usage.provider_id, usage.requested_model_id, usage.occurred_at,
|
|
41
|
+
usage.total_tokens,
|
|
42
|
+
COALESCE(cost.status, 'unvalued') AS valuation_status,
|
|
43
|
+
cost.currency AS amount_currency, cost.amount_nanos,
|
|
44
|
+
root_agent.id AS root_agent_run_id,
|
|
45
|
+
root_agent.thread_id AS root_thread_id,
|
|
46
|
+
direct_agent.kind AS direct_kind,
|
|
47
|
+
direct_agent.id AS direct_run_id,
|
|
48
|
+
direct_agent.parent_run_id AS parent_run_id,
|
|
49
|
+
workflow_node.node_id AS workflow_agent_step_id,
|
|
50
|
+
workflow_agent.node_run_id AS workflow_node_run_id,
|
|
51
|
+
workflow_node.workflow_id AS workflow_id,
|
|
52
|
+
workflow_node.workflow_run_id AS workflow_run_id
|
|
53
|
+
FROM ai_model_call_usage AS usage
|
|
54
|
+
LEFT JOIN agent_runs AS direct_agent
|
|
55
|
+
ON direct_agent.project_id = usage.project_id AND direct_agent.execution_id = usage.execution_id
|
|
56
|
+
LEFT JOIN agent_runs AS root_agent
|
|
57
|
+
ON root_agent.project_id = usage.project_id
|
|
58
|
+
AND root_agent.id = COALESCE(direct_agent.parent_run_id, direct_agent.id)
|
|
59
|
+
LEFT JOIN workflow_agent_node_runs AS workflow_agent
|
|
60
|
+
ON workflow_agent.project_id = usage.project_id AND workflow_agent.execution_id = usage.execution_id
|
|
61
|
+
LEFT JOIN workflow_node_runs AS workflow_node
|
|
62
|
+
ON workflow_node.project_id = workflow_agent.project_id AND workflow_node.id = workflow_agent.node_run_id
|
|
63
|
+
LEFT JOIN ai_model_call_valuations AS cost
|
|
64
|
+
ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
|
|
65
|
+
WHERE usage.project_id = $1
|
|
66
|
+
AND usage.occurred_at >= $2::timestamptz
|
|
67
|
+
AND usage.occurred_at < $3::timestamptz
|
|
68
|
+
AND ($4::text IS NULL OR usage.provider_id = $4)
|
|
69
|
+
AND ($5::text IS NULL OR usage.requested_model_id = $5)
|
|
70
|
+
AND ($6::text IS NULL OR COALESCE(cost.status, 'unvalued') = $6)
|
|
71
|
+
), roots AS (
|
|
72
|
+
SELECT root_execution_id, MAX(occurred_at) AS last_call_at
|
|
73
|
+
FROM filtered GROUP BY root_execution_id
|
|
74
|
+
)
|
|
75
|
+
`
|
|
76
|
+
const COUNT_SQL = `${FILTERED_SQL} SELECT CAST(COUNT(*) AS TEXT) AS total FROM roots`
|
|
77
|
+
const PAGE_SQL = `${FILTERED_SQL}, page AS (
|
|
78
|
+
SELECT root_execution_id FROM roots
|
|
79
|
+
ORDER BY last_call_at DESC, root_execution_id COLLATE "C" ASC LIMIT $7 OFFSET $8
|
|
80
|
+
)
|
|
81
|
+
SELECT filtered.root_execution_id, execution_id, provider_id, requested_model_id,
|
|
82
|
+
MIN(occurred_at) AS first_call_at, MAX(occurred_at) AS last_call_at,
|
|
83
|
+
CAST(COUNT(*) AS TEXT) AS call_count,
|
|
84
|
+
CAST(COUNT(total_tokens) AS TEXT) AS token_count,
|
|
85
|
+
CAST(SUM(total_tokens) AS TEXT) AS total_tokens,
|
|
86
|
+
valuation_status, amount_currency,
|
|
87
|
+
CAST(SUM(amount_nanos / 1000000000000) AS TEXT) AS amount_high,
|
|
88
|
+
CAST(SUM((amount_nanos / 1000000) % 1000000) AS TEXT) AS amount_middle,
|
|
89
|
+
CAST(SUM(amount_nanos % 1000000) AS TEXT) AS amount_low,
|
|
90
|
+
MAX(root_agent_run_id) AS root_agent_run_id,
|
|
91
|
+
MAX(root_thread_id) AS root_thread_id,
|
|
92
|
+
MAX(direct_kind) AS direct_kind,
|
|
93
|
+
MAX(direct_run_id) AS direct_run_id,
|
|
94
|
+
MAX(parent_run_id) AS parent_run_id,
|
|
95
|
+
MAX(workflow_agent_step_id) AS workflow_agent_step_id,
|
|
96
|
+
MAX(workflow_node_run_id) AS workflow_node_run_id,
|
|
97
|
+
MAX(workflow_id) AS workflow_id,
|
|
98
|
+
MAX(workflow_run_id) AS workflow_run_id
|
|
99
|
+
FROM filtered JOIN page ON page.root_execution_id = filtered.root_execution_id
|
|
100
|
+
GROUP BY filtered.root_execution_id, execution_id, provider_id, requested_model_id,
|
|
101
|
+
valuation_status, amount_currency
|
|
102
|
+
`
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AuthStorage,
|
|
3
|
+
CompleteDeviceAuthorizationInput,
|
|
4
|
+
CompleteDeviceAuthorizationResult,
|
|
3
5
|
CompleteMagicLinkSignInInput,
|
|
4
6
|
CompleteOidcSignInInput,
|
|
5
7
|
CompleteSignInResult,
|
|
8
|
+
DeviceAuthorizationRecord,
|
|
6
9
|
GroupMembershipRecord,
|
|
7
10
|
InvitationRecord,
|
|
8
11
|
SessionRecord,
|
|
@@ -19,6 +22,10 @@ import {
|
|
|
19
22
|
runPgTransaction,
|
|
20
23
|
} from "../transactions"
|
|
21
24
|
import { PgAuthAccessTokenStore } from "./access-tokens"
|
|
25
|
+
import {
|
|
26
|
+
getPgDeviceAuthorizationById,
|
|
27
|
+
PgAuthDeviceAuthorizationStore,
|
|
28
|
+
} from "./device-authorizations"
|
|
22
29
|
import { PgAuthGroupMembershipStore } from "./group-memberships"
|
|
23
30
|
import { PgAuthUserIdentityStore } from "./identities"
|
|
24
31
|
import { PgAuthInvitationStore } from "./invitations"
|
|
@@ -77,6 +84,7 @@ export class PgAuthStorage implements AuthStorage {
|
|
|
77
84
|
readonly groupMemberships: PgAuthGroupMembershipStore
|
|
78
85
|
readonly magicLinks: PgAuthMagicLinkStore
|
|
79
86
|
readonly oidcAuthorizationAttempts: PgAuthOidcAuthorizationAttemptStore
|
|
87
|
+
readonly deviceAuthorizations: PgAuthDeviceAuthorizationStore
|
|
80
88
|
|
|
81
89
|
constructor(options: PgAuthStorageOptions) {
|
|
82
90
|
this.sql = options.sql
|
|
@@ -90,6 +98,43 @@ export class PgAuthStorage implements AuthStorage {
|
|
|
90
98
|
this.groupMemberships = new PgAuthGroupMembershipStore(this.sql)
|
|
91
99
|
this.magicLinks = new PgAuthMagicLinkStore(this.sql)
|
|
92
100
|
this.oidcAuthorizationAttempts = new PgAuthOidcAuthorizationAttemptStore(this.sql)
|
|
101
|
+
this.deviceAuthorizations = new PgAuthDeviceAuthorizationStore(this.sql)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async completeDeviceAuthorization(
|
|
105
|
+
input: CompleteDeviceAuthorizationInput
|
|
106
|
+
): Promise<CompleteDeviceAuthorizationResult> {
|
|
107
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
108
|
+
const authorization = await getPgDeviceAuthorizationById(tx, input.projectId, input.id)
|
|
109
|
+
if (!authorization) {
|
|
110
|
+
throw new AuthStorageError(
|
|
111
|
+
"missing_device_authorization",
|
|
112
|
+
`[Sixb] Device authorization '${input.id}' not found for project '${input.projectId}'.`
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
assertCompletableDeviceAuthorization(authorization, input)
|
|
116
|
+
const accessToken = await new PgAuthAccessTokenStore(tx).create(input.accessToken)
|
|
117
|
+
const [consumed] = await tx`
|
|
118
|
+
UPDATE auth_device_authorizations
|
|
119
|
+
SET status = 'consumed', consumed_at = ${input.completedAt}
|
|
120
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id} AND status = 'approved'
|
|
121
|
+
RETURNING *
|
|
122
|
+
`
|
|
123
|
+
if (!consumed) {
|
|
124
|
+
throw new AuthStorageError(
|
|
125
|
+
"invalid_device_authorization",
|
|
126
|
+
"[Sixb] Device authorization was already consumed."
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
authorization: {
|
|
131
|
+
...authorization,
|
|
132
|
+
status: "consumed" as const,
|
|
133
|
+
consumedAt: new Date(input.completedAt),
|
|
134
|
+
},
|
|
135
|
+
accessToken,
|
|
136
|
+
}
|
|
137
|
+
})
|
|
93
138
|
}
|
|
94
139
|
|
|
95
140
|
async completeMagicLinkSignIn(
|
|
@@ -796,3 +841,42 @@ function assertSignInSessionAudience(
|
|
|
796
841
|
`[Sixb] Sign-in session audience '${sessionAudience}' does not match stored auth audience '${storedAudience}' for project '${projectId}'.`
|
|
797
842
|
)
|
|
798
843
|
}
|
|
844
|
+
|
|
845
|
+
function assertCompletableDeviceAuthorization(
|
|
846
|
+
authorization: DeviceAuthorizationRecord,
|
|
847
|
+
input: CompleteDeviceAuthorizationInput
|
|
848
|
+
): void {
|
|
849
|
+
if (authorization.deviceCodeHash !== input.deviceCodeHash) {
|
|
850
|
+
throw new AuthStorageError(
|
|
851
|
+
"invalid_device_authorization",
|
|
852
|
+
"[Sixb] Invalid device authorization."
|
|
853
|
+
)
|
|
854
|
+
}
|
|
855
|
+
if (authorization.status === "pending") {
|
|
856
|
+
throw new AuthStorageError(
|
|
857
|
+
"pending_device_authorization",
|
|
858
|
+
"[Sixb] Device authorization is pending."
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
if (authorization.status === "denied") {
|
|
862
|
+
throw new AuthStorageError(
|
|
863
|
+
"device_authorization_denied",
|
|
864
|
+
"[Sixb] Device authorization was denied."
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
if (
|
|
868
|
+
authorization.status !== "approved" ||
|
|
869
|
+
authorization.expiresAt <= input.completedAt ||
|
|
870
|
+
!authorization.approvedUserId ||
|
|
871
|
+
!authorization.approvedSessionId ||
|
|
872
|
+
input.accessToken.projectId !== input.projectId ||
|
|
873
|
+
input.accessToken.subjectType !== "user" ||
|
|
874
|
+
input.accessToken.subjectId !== authorization.approvedUserId ||
|
|
875
|
+
input.accessToken.createdBySessionId !== authorization.approvedSessionId
|
|
876
|
+
) {
|
|
877
|
+
throw new AuthStorageError(
|
|
878
|
+
"invalid_device_authorization",
|
|
879
|
+
"[Sixb] Device authorization is invalid."
|
|
880
|
+
)
|
|
881
|
+
}
|
|
882
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthDeviceAuthorizationStore,
|
|
3
|
+
CreateDeviceAuthorizationInput,
|
|
4
|
+
DeviceAuthorizationRecord,
|
|
5
|
+
} from "@sixb/core/storage"
|
|
6
|
+
import { AuthStorageError, MAX_PENDING_DEVICE_AUTHORIZATIONS } from "@sixb/core/storage"
|
|
7
|
+
import {
|
|
8
|
+
authLockKey,
|
|
9
|
+
lockAdvisoryKeys,
|
|
10
|
+
type PgStoreClient,
|
|
11
|
+
runPgTransaction,
|
|
12
|
+
} from "../transactions"
|
|
13
|
+
import type { PgAuthDeviceAuthorizationRow } from "./rows"
|
|
14
|
+
import { rowToDeviceAuthorizationRecord } from "./rows"
|
|
15
|
+
import { assertNonEmpty, mapUniqueConstraintError } from "./shared"
|
|
16
|
+
|
|
17
|
+
export class PgAuthDeviceAuthorizationStore implements AuthDeviceAuthorizationStore {
|
|
18
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
19
|
+
|
|
20
|
+
async create(input: CreateDeviceAuthorizationInput): Promise<DeviceAuthorizationRecord> {
|
|
21
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
22
|
+
await lockAdvisoryKeys(tx, [authLockKey("device-authorizations", input.projectId)])
|
|
23
|
+
await tx`
|
|
24
|
+
DELETE FROM auth_device_authorizations
|
|
25
|
+
WHERE project_id = ${input.projectId} AND expires_at <= ${input.createdAt}
|
|
26
|
+
`
|
|
27
|
+
const [pending] = await tx<{ readonly count: string | number }[]>`
|
|
28
|
+
SELECT COUNT(*)::bigint AS count FROM auth_device_authorizations
|
|
29
|
+
WHERE project_id = ${input.projectId} AND status = 'pending'
|
|
30
|
+
`
|
|
31
|
+
if (Number(pending?.count ?? 0) >= MAX_PENDING_DEVICE_AUTHORIZATIONS) {
|
|
32
|
+
throw new AuthStorageError(
|
|
33
|
+
"device_authorization_limit_exceeded",
|
|
34
|
+
`[Sixb] Project '${input.projectId}' has too many pending device authorizations.`
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const [row] = await tx<PgAuthDeviceAuthorizationRow[]>`
|
|
39
|
+
INSERT INTO auth_device_authorizations (
|
|
40
|
+
project_id, id, device_code_hash, user_code, client_name, token_name,
|
|
41
|
+
token_expires_at, status, created_at, expires_at
|
|
42
|
+
) VALUES (
|
|
43
|
+
${assertNonEmpty(input.projectId, "Project id")},
|
|
44
|
+
${assertNonEmpty(input.id, "Device authorization id")},
|
|
45
|
+
${assertNonEmpty(input.deviceCodeHash, "Device code hash")},
|
|
46
|
+
${assertNonEmpty(input.userCode, "User code")},
|
|
47
|
+
${assertNonEmpty(input.clientName, "Client name")},
|
|
48
|
+
${assertNonEmpty(input.tokenName, "Token name")},
|
|
49
|
+
${input.tokenExpiresAt}, 'pending', ${input.createdAt}, ${input.expiresAt}
|
|
50
|
+
) RETURNING *
|
|
51
|
+
`
|
|
52
|
+
return rowToDeviceAuthorizationRecord(row)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
mapUniqueConstraintError(
|
|
55
|
+
error,
|
|
56
|
+
"duplicate_device_authorization",
|
|
57
|
+
`[Sixb] Device authorization '${input.id}' already exists for project '${input.projectId}'.`
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async getById(params: { readonly projectId: string; readonly id: string }) {
|
|
64
|
+
return getPgDeviceAuthorizationById(this.sql, params.projectId, params.id)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getByUserCode(params: { readonly projectId: string; readonly userCode: string }) {
|
|
68
|
+
const [row] = await this.sql<PgAuthDeviceAuthorizationRow[]>`
|
|
69
|
+
SELECT * FROM auth_device_authorizations
|
|
70
|
+
WHERE project_id = ${params.projectId} AND user_code = ${params.userCode}
|
|
71
|
+
`
|
|
72
|
+
return row ? rowToDeviceAuthorizationRecord(row) : null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async approve(params: {
|
|
76
|
+
readonly projectId: string
|
|
77
|
+
readonly id: string
|
|
78
|
+
readonly userId: string
|
|
79
|
+
readonly sessionId: string
|
|
80
|
+
readonly approvedAt: Date
|
|
81
|
+
}) {
|
|
82
|
+
const [row] = await this.sql<PgAuthDeviceAuthorizationRow[]>`
|
|
83
|
+
UPDATE auth_device_authorizations
|
|
84
|
+
SET status = 'approved', approved_user_id = ${params.userId},
|
|
85
|
+
approved_session_id = ${params.sessionId}, approved_at = ${params.approvedAt}
|
|
86
|
+
WHERE project_id = ${params.projectId} AND id = ${params.id}
|
|
87
|
+
AND status = 'pending' AND expires_at > ${params.approvedAt}
|
|
88
|
+
AND EXISTS (
|
|
89
|
+
SELECT 1 FROM auth_sessions
|
|
90
|
+
WHERE project_id = ${params.projectId} AND id = ${params.sessionId}
|
|
91
|
+
AND user_id = ${params.userId} AND revoked_at IS NULL
|
|
92
|
+
AND expires_at > ${params.approvedAt}
|
|
93
|
+
AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.approvedAt})
|
|
94
|
+
)
|
|
95
|
+
RETURNING *
|
|
96
|
+
`
|
|
97
|
+
if (!row) throw invalidTransition(params.id, params.projectId)
|
|
98
|
+
return rowToDeviceAuthorizationRecord(row)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async deny(params: { readonly projectId: string; readonly id: string; readonly deniedAt: Date }) {
|
|
102
|
+
const [row] = await this.sql<PgAuthDeviceAuthorizationRow[]>`
|
|
103
|
+
UPDATE auth_device_authorizations
|
|
104
|
+
SET status = 'denied', denied_at = ${params.deniedAt}
|
|
105
|
+
WHERE project_id = ${params.projectId} AND id = ${params.id}
|
|
106
|
+
AND status = 'pending' AND expires_at > ${params.deniedAt}
|
|
107
|
+
RETURNING *
|
|
108
|
+
`
|
|
109
|
+
if (!row) throw invalidTransition(params.id, params.projectId)
|
|
110
|
+
return rowToDeviceAuthorizationRecord(row)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function getPgDeviceAuthorizationById(
|
|
115
|
+
sql: PgStoreClient,
|
|
116
|
+
projectId: string,
|
|
117
|
+
id: string
|
|
118
|
+
) {
|
|
119
|
+
const [row] = await sql<PgAuthDeviceAuthorizationRow[]>`
|
|
120
|
+
SELECT * FROM auth_device_authorizations
|
|
121
|
+
WHERE project_id = ${projectId} AND id = ${id}
|
|
122
|
+
`
|
|
123
|
+
return row ? rowToDeviceAuthorizationRecord(row) : null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function invalidTransition(id: string, projectId: string): AuthStorageError {
|
|
127
|
+
return new AuthStorageError(
|
|
128
|
+
"invalid_device_authorization",
|
|
129
|
+
`[Sixb] Device authorization '${id}' is no longer pending for project '${projectId}'.`
|
|
130
|
+
)
|
|
131
|
+
}
|