@sixb/pg 0.1.20 → 0.1.22
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/README.md +1 -0
- package/dist/agents/agent-storage.d.ts +8 -2
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/approvals.d.ts +48 -0
- package/dist/agents/approvals.d.ts.map +1 -0
- package/dist/agents/checkpoints.d.ts +1 -0
- package/dist/agents/checkpoints.d.ts.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/memory.d.ts +19 -0
- package/dist/agents/memory.d.ts.map +1 -0
- package/dist/agents/messages.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +15 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +3 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/settings.d.ts +29 -0
- package/dist/agents/settings.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +10 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts.map +1 -1
- package/dist/auth-storage/auth-storage.d.ts +3 -2
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/sessions.d.ts +2 -1
- package/dist/auth-storage/sessions.d.ts.map +1 -1
- package/dist/auth-storage/users.d.ts +8 -4
- package/dist/auth-storage/users.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6001 -2923
- package/dist/index.js.map +45 -37
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/conditional-reader.d.ts +17 -0
- package/dist/objects/conditional-reader.d.ts.map +1 -0
- package/dist/objects/query-compiler.d.ts +27 -1
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/objects/query-indexes.d.ts.map +1 -1
- package/dist/objects/read-scope.d.ts +10 -1
- package/dist/objects/read-scope.d.ts.map +1 -1
- package/dist/objects/reader.d.ts +2 -0
- package/dist/objects/reader.d.ts.map +1 -1
- package/dist/objects/scoped-reader.d.ts +12 -0
- package/dist/objects/scoped-reader.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts +1 -0
- package/dist/objects/selected-reader.d.ts.map +1 -1
- package/dist/objects/storage.d.ts +7 -1
- package/dist/objects/storage.d.ts.map +1 -1
- package/dist/ontology-storage/index.d.ts +2 -0
- package/dist/ontology-storage/index.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-session.d.ts +6 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/replacement-plans.d.ts.map +1 -1
- package/dist/ontology-storage/sources.d.ts +2 -1
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/ontology-storage/unique-values.d.ts +60 -0
- package/dist/ontology-storage/unique-values.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +4 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/dist/pg-activity-storage.d.ts +22 -0
- package/dist/pg-activity-storage.d.ts.map +1 -0
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-usage-storage.d.ts +8 -1
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-artifact-storage.d.ts +26 -0
- package/dist/pg-artifact-storage.d.ts.map +1 -0
- package/dist/pg-execution-storage.d.ts +3 -1
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/pg-workflow-run-storage.d.ts +4 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -1
- package/dist/share-grant-storage.d.ts +2 -1
- package/dist/share-grant-storage.d.ts.map +1 -1
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +11 -2
- package/src/agents/approvals.ts +400 -0
- package/src/agents/checkpoints.ts +8 -4
- package/src/agents/index.ts +1 -0
- package/src/agents/memory.ts +175 -0
- package/src/agents/messages.ts +14 -7
- package/src/agents/rows.ts +46 -1
- package/src/agents/runs.ts +154 -22
- package/src/agents/settings.ts +105 -0
- package/src/agents/threads.ts +379 -3
- package/src/ai-cost-groups.ts +11 -1
- package/src/auth-storage/auth-storage.ts +45 -13
- package/src/auth-storage/sessions.ts +20 -0
- package/src/auth-storage/users.ts +105 -43
- package/src/index.ts +10 -0
- package/src/migrations/063-artifacts.sql +67 -0
- package/src/migrations/064-drop-output-diagnostics.sql +14 -0
- package/src/migrations/065-plan-work-payload-statistics.sql +5 -0
- package/src/migrations/066-activity.sql +61 -0
- package/src/migrations/067-activity-timeline.sql +15 -0
- package/src/migrations/068-activity-interactions.sql +20 -0
- package/src/migrations/069-artifact-kinds.sql +4 -0
- package/src/migrations/070-artifact-state.sql +16 -0
- package/src/migrations/071-agent-thread-participants.sql +16 -0
- package/src/migrations/072-forked-context-checkpoints.sql +23 -0
- package/src/migrations/073-unique-values.sql +25 -0
- package/src/migrations/074-generated-media-usage.sql +21 -0
- package/src/migrations/075-model-call-files.sql +11 -0
- package/src/migrations/076-agent-memory.sql +33 -0
- package/src/migrations/077-artifact-threads.sql +21 -0
- package/src/migrations/078-agent-approvals.sql +58 -0
- package/src/migrations/079-agent-approval-resume.sql +20 -0
- package/src/migrations/080-subagent-threads.sql +145 -0
- package/src/migrations/081-subagent-questions.sql +6 -0
- package/src/migrations.ts +52 -0
- package/src/objects/conditional-reader.ts +44 -0
- package/src/objects/prepare-queries.ts +1 -1
- package/src/objects/query-compiler.ts +96 -33
- package/src/objects/query-indexes.ts +29 -10
- package/src/objects/read-scope.ts +140 -25
- package/src/objects/reader.ts +50 -6
- package/src/objects/scoped-reader.ts +101 -0
- package/src/objects/selected-reader.ts +16 -76
- package/src/objects/storage.ts +24 -5
- package/src/objects/text-count-index.ts +1 -0
- package/src/ontology-storage/index.ts +3 -0
- package/src/ontology-storage/materialization-session.ts +8 -0
- package/src/ontology-storage/materialization-writer.ts +76 -1
- package/src/ontology-storage/materializations.ts +20 -1
- package/src/ontology-storage/replacement-plans.ts +25 -7
- package/src/ontology-storage/sources.ts +37 -0
- package/src/ontology-storage/unique-values.ts +274 -0
- package/src/pg-action-run-storage.ts +23 -0
- package/src/pg-activity-storage.ts +486 -0
- package/src/pg-ai-cost-storage.ts +62 -12
- package/src/pg-ai-usage-storage.ts +90 -1
- package/src/pg-artifact-storage.ts +521 -0
- package/src/pg-execution-storage.ts +83 -0
- package/src/pg-workflow-run-storage.ts +23 -0
- package/src/share-grant-storage.ts +21 -0
- package/src/storage-errors.ts +7 -0
- package/src/transactions.ts +1 -1
package/src/agents/threads.ts
CHANGED
|
@@ -1,24 +1,40 @@
|
|
|
1
1
|
import {
|
|
2
|
+
assertConversationThreadRow,
|
|
3
|
+
participantsToAdd,
|
|
4
|
+
planAgentThreadFork,
|
|
5
|
+
remapArtifactIds,
|
|
2
6
|
snapshotAgentThreadSandboxParams,
|
|
3
7
|
transitionAgentThreadSandbox,
|
|
4
8
|
} from "@sixb/core/internal/agent-run-storage-provider"
|
|
5
9
|
import {
|
|
10
|
+
type AddAgentThreadParticipantsInput,
|
|
11
|
+
type AgentContextCheckpointRecord,
|
|
6
12
|
AgentStorageError,
|
|
13
|
+
type AgentThreadParticipantRecord,
|
|
7
14
|
type AgentThreadRecord,
|
|
8
15
|
type AgentThreadSandboxState,
|
|
9
16
|
type AgentThreadStore,
|
|
10
17
|
type CreateAgentThreadInput,
|
|
18
|
+
type ForkAgentThreadInput,
|
|
19
|
+
type ListAgentThreadChildrenInput,
|
|
20
|
+
type ListAgentThreadParticipantsInput,
|
|
11
21
|
type ListAgentThreadsInput,
|
|
12
22
|
type ListAgentThreadsResult,
|
|
23
|
+
type RemoveAgentThreadParticipantInput,
|
|
13
24
|
type TransitionAgentThreadSandboxInput,
|
|
14
25
|
} from "@sixb/core/storage"
|
|
15
|
-
import type { SqlParameter } from "../pg-client"
|
|
16
|
-
import { isUniqueViolation } from "../storage-errors"
|
|
26
|
+
import type { SQLClient, SqlParameter } from "../pg-client"
|
|
27
|
+
import { isUniqueViolation, isUniqueViolationOf } from "../storage-errors"
|
|
17
28
|
import { type PgStoreClient, runPgTransaction } from "../transactions"
|
|
18
29
|
import {
|
|
30
|
+
type AgentContextCheckpointRow,
|
|
31
|
+
type AgentMessageRow,
|
|
19
32
|
type AgentRunRow,
|
|
33
|
+
type AgentThreadParticipantRow,
|
|
20
34
|
type AgentThreadRow,
|
|
21
35
|
queryAgentList,
|
|
36
|
+
rowToContextCheckpointRecord,
|
|
37
|
+
rowToParticipantRecord,
|
|
22
38
|
rowToRunRecord,
|
|
23
39
|
rowToThreadRecord,
|
|
24
40
|
} from "./rows"
|
|
@@ -100,6 +116,165 @@ export class PgAgentThreadStore implements AgentThreadStore {
|
|
|
100
116
|
}
|
|
101
117
|
}
|
|
102
118
|
|
|
119
|
+
async fork(input: ForkAgentThreadInput): Promise<AgentThreadRecord> {
|
|
120
|
+
try {
|
|
121
|
+
return await runPgTransaction(this.sql, async (tx) => {
|
|
122
|
+
// A share lock holds back the writes that take the thread for update: an admitted request
|
|
123
|
+
// or a finalized answer lands before the copy or after it, never inside it.
|
|
124
|
+
const [source] = await tx<Pick<AgentThreadRow, "id" | "kind" | "active_run_id">[]>`
|
|
125
|
+
SELECT id, kind, active_run_id FROM agent_threads
|
|
126
|
+
WHERE project_id = ${input.projectId} AND id = ${input.sourceThreadId}
|
|
127
|
+
FOR SHARE
|
|
128
|
+
`
|
|
129
|
+
|
|
130
|
+
if (!source) {
|
|
131
|
+
throw new AgentStorageError(
|
|
132
|
+
"thread_not_found",
|
|
133
|
+
`[SixbPg] Agent thread '${input.sourceThreadId}' not found for project '${input.projectId}'.`
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
assertConversationThreadRow(source, "can be forked", "SixbPg")
|
|
137
|
+
|
|
138
|
+
const [activeRun] =
|
|
139
|
+
source.active_run_id === null
|
|
140
|
+
? []
|
|
141
|
+
: await tx<{ trigger_message_id: string | null }[]>`
|
|
142
|
+
SELECT trigger_message_id FROM agent_runs
|
|
143
|
+
WHERE project_id = ${input.projectId} AND id = ${source.active_run_id}
|
|
144
|
+
`
|
|
145
|
+
|
|
146
|
+
// Only what the plan needs: the parts are copied by the database, not through here. A
|
|
147
|
+
// message of approval outcomes only is not counted (`countsAsThreadMessage`).
|
|
148
|
+
const messages = await tx<(Pick<AgentMessageRow, "id" | "seq"> & { counted: boolean })[]>`
|
|
149
|
+
SELECT id, seq,
|
|
150
|
+
NOT (
|
|
151
|
+
jsonb_array_length(parts) > 0 AND NOT EXISTS (
|
|
152
|
+
SELECT 1 FROM jsonb_array_elements(parts) AS part
|
|
153
|
+
WHERE part->>'type' <> 'approval-outcome'
|
|
154
|
+
)
|
|
155
|
+
) AS counted
|
|
156
|
+
FROM agent_messages
|
|
157
|
+
WHERE project_id = ${input.projectId} AND thread_id = ${source.id}
|
|
158
|
+
ORDER BY seq ASC
|
|
159
|
+
`
|
|
160
|
+
const checkpoints = await tx<AgentContextCheckpointRow[]>`
|
|
161
|
+
SELECT * FROM agent_context_checkpoints
|
|
162
|
+
WHERE project_id = ${input.projectId} AND thread_id = ${source.id}
|
|
163
|
+
ORDER BY summarized_through_seq DESC, created_at DESC, id DESC
|
|
164
|
+
`
|
|
165
|
+
|
|
166
|
+
const plan = planAgentThreadFork(
|
|
167
|
+
input,
|
|
168
|
+
{
|
|
169
|
+
...(activeRun?.trigger_message_id
|
|
170
|
+
? { triggerMessageId: activeRun.trigger_message_id }
|
|
171
|
+
: {}),
|
|
172
|
+
messages: messages.map((message) => ({
|
|
173
|
+
id: message.id,
|
|
174
|
+
seq: Number(message.seq),
|
|
175
|
+
counted: message.counted,
|
|
176
|
+
})),
|
|
177
|
+
checkpoints: checkpoints.map(rowToContextCheckpointRecord),
|
|
178
|
+
},
|
|
179
|
+
"SixbPg"
|
|
180
|
+
)
|
|
181
|
+
const { thread } = plan
|
|
182
|
+
|
|
183
|
+
const [row] = await tx<AgentThreadRow[]>`
|
|
184
|
+
INSERT INTO agent_threads (
|
|
185
|
+
project_id,
|
|
186
|
+
id,
|
|
187
|
+
owner_principal_type,
|
|
188
|
+
owner_principal_id,
|
|
189
|
+
title,
|
|
190
|
+
sandbox_params,
|
|
191
|
+
sandbox_state,
|
|
192
|
+
status,
|
|
193
|
+
last_message_at,
|
|
194
|
+
message_count,
|
|
195
|
+
created_at,
|
|
196
|
+
updated_at
|
|
197
|
+
) VALUES (
|
|
198
|
+
${thread.projectId},
|
|
199
|
+
${thread.id},
|
|
200
|
+
${thread.ownerPrincipal.type},
|
|
201
|
+
${thread.ownerPrincipal.id},
|
|
202
|
+
${thread.title ?? null},
|
|
203
|
+
${thread.sandboxParams === undefined ? null : JSON.stringify(thread.sandboxParams)}::text::jsonb,
|
|
204
|
+
${thread.sandboxState === undefined ? null : JSON.stringify(thread.sandboxState)}::text::jsonb,
|
|
205
|
+
${thread.status},
|
|
206
|
+
${thread.lastMessageAt ?? null},
|
|
207
|
+
${thread.messageCount},
|
|
208
|
+
${thread.createdAt},
|
|
209
|
+
${thread.updatedAt}
|
|
210
|
+
)
|
|
211
|
+
RETURNING *
|
|
212
|
+
`
|
|
213
|
+
|
|
214
|
+
if (plan.messages.length > 0) {
|
|
215
|
+
await tx`
|
|
216
|
+
INSERT INTO agent_messages (
|
|
217
|
+
project_id,
|
|
218
|
+
id,
|
|
219
|
+
thread_id,
|
|
220
|
+
run_id,
|
|
221
|
+
role,
|
|
222
|
+
author_principal_type,
|
|
223
|
+
author_principal_id,
|
|
224
|
+
seq,
|
|
225
|
+
parts,
|
|
226
|
+
metadata,
|
|
227
|
+
content_version,
|
|
228
|
+
created_at,
|
|
229
|
+
completed_at
|
|
230
|
+
)
|
|
231
|
+
SELECT
|
|
232
|
+
message.project_id,
|
|
233
|
+
copy.id,
|
|
234
|
+
${thread.id},
|
|
235
|
+
message.run_id,
|
|
236
|
+
message.role,
|
|
237
|
+
message.author_principal_type,
|
|
238
|
+
message.author_principal_id,
|
|
239
|
+
message.seq,
|
|
240
|
+
message.parts,
|
|
241
|
+
message.metadata,
|
|
242
|
+
message.content_version,
|
|
243
|
+
message.created_at,
|
|
244
|
+
message.completed_at
|
|
245
|
+
FROM unnest(
|
|
246
|
+
${tx.array(plan.messages.map((copy) => copy.sourceId))}::text[],
|
|
247
|
+
${tx.array(plan.messages.map((copy) => copy.id))}::text[]
|
|
248
|
+
) AS copy(source_id, id)
|
|
249
|
+
JOIN agent_messages AS message
|
|
250
|
+
ON message.project_id = ${input.projectId} AND message.id = copy.source_id
|
|
251
|
+
`
|
|
252
|
+
await remapCopiedParts(
|
|
253
|
+
tx,
|
|
254
|
+
input.projectId,
|
|
255
|
+
plan.messages.map((copy) => copy.id),
|
|
256
|
+
input.artifactCopies
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (plan.checkpoint) {
|
|
261
|
+
await insertCopiedCheckpoint(tx, plan.checkpoint)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return rowToThreadRecord(row)
|
|
265
|
+
})
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (isUniqueViolationOf(error, "agent_threads_pkey")) {
|
|
268
|
+
throw new AgentStorageError(
|
|
269
|
+
"duplicate_id",
|
|
270
|
+
`[SixbPg] Agent thread '${input.id}' already exists for project '${input.projectId}'.`
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
throw error
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
103
278
|
async getById(params: { projectId: string; id: string }): Promise<AgentThreadRecord | null> {
|
|
104
279
|
const [row] = await this.sql<AgentThreadRow[]>`
|
|
105
280
|
SELECT * FROM agent_threads WHERE project_id = ${params.projectId} AND id = ${params.id}
|
|
@@ -108,12 +283,52 @@ export class PgAgentThreadStore implements AgentThreadStore {
|
|
|
108
283
|
return row ? rowToThreadRecord(row) : null
|
|
109
284
|
}
|
|
110
285
|
|
|
286
|
+
async getByIds(params: {
|
|
287
|
+
readonly projectId: string
|
|
288
|
+
readonly ids: readonly string[]
|
|
289
|
+
}): Promise<readonly AgentThreadRecord[]> {
|
|
290
|
+
const ids = [...new Set(params.ids)]
|
|
291
|
+
if (ids.length === 0) {
|
|
292
|
+
return []
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const rows = await this.sql<AgentThreadRow[]>`
|
|
296
|
+
SELECT *
|
|
297
|
+
FROM agent_threads
|
|
298
|
+
WHERE project_id = ${params.projectId}
|
|
299
|
+
AND id = ANY(${this.sql.array(ids)}::text[])
|
|
300
|
+
`
|
|
301
|
+
|
|
302
|
+
const byId = new Map(rows.map((row) => [row.id, row]))
|
|
303
|
+
return ids.flatMap((id) => {
|
|
304
|
+
const row = byId.get(id)
|
|
305
|
+
return row ? [rowToThreadRecord(row)] : []
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async listChildren(input: ListAgentThreadChildrenInput): Promise<readonly AgentThreadRecord[]> {
|
|
310
|
+
// By `idx_agent_threads_children`, which holds sub-agents' threads only.
|
|
311
|
+
const rows = await this.sql<AgentThreadRow[]>`
|
|
312
|
+
SELECT *
|
|
313
|
+
FROM agent_threads
|
|
314
|
+
WHERE project_id = ${input.projectId}
|
|
315
|
+
AND parent_thread_id = ${input.parentThreadId}
|
|
316
|
+
AND kind = 'subagent'
|
|
317
|
+
${input.parentRunId === undefined ? this.sql`` : this.sql`AND parent_run_id = ${input.parentRunId}`}
|
|
318
|
+
ORDER BY created_at ASC, id ASC
|
|
319
|
+
LIMIT ${input.limit ?? 100}
|
|
320
|
+
OFFSET ${input.offset ?? 0}
|
|
321
|
+
`
|
|
322
|
+
return rows.map(rowToThreadRecord)
|
|
323
|
+
}
|
|
324
|
+
|
|
111
325
|
async list(input: ListAgentThreadsInput): Promise<ListAgentThreadsResult> {
|
|
112
326
|
if (input.statuses !== undefined && input.statuses.length === 0) {
|
|
113
327
|
return { threads: [], hasMore: false, total: 0 }
|
|
114
328
|
}
|
|
115
329
|
|
|
116
|
-
|
|
330
|
+
// A sub-agent's thread is read through its conversation; the list indexes leave it out too.
|
|
331
|
+
const whereClauses = ["project_id = $1", "kind = 'conversation'"]
|
|
117
332
|
const params: SqlParameter[] = [input.projectId]
|
|
118
333
|
let index = 2
|
|
119
334
|
|
|
@@ -128,6 +343,17 @@ export class PgAgentThreadStore implements AgentThreadStore {
|
|
|
128
343
|
params.push(input.ownerPrincipal.type, input.ownerPrincipal.id)
|
|
129
344
|
}
|
|
130
345
|
|
|
346
|
+
if (input.participantPrincipal) {
|
|
347
|
+
whereClauses.push(`EXISTS (
|
|
348
|
+
SELECT 1 FROM agent_thread_participants participant
|
|
349
|
+
WHERE participant.project_id = agent_threads.project_id
|
|
350
|
+
AND participant.thread_id = agent_threads.id
|
|
351
|
+
AND participant.principal_type = $${index++}
|
|
352
|
+
AND participant.principal_id = $${index++}
|
|
353
|
+
)`)
|
|
354
|
+
params.push(input.participantPrincipal.type, input.participantPrincipal.id)
|
|
355
|
+
}
|
|
356
|
+
|
|
131
357
|
const dir = input.order === "asc" ? "ASC" : "DESC"
|
|
132
358
|
const { rows, total, hasMore } = await queryAgentList<AgentThreadRow>({
|
|
133
359
|
sql: this.sql,
|
|
@@ -142,4 +368,154 @@ export class PgAgentThreadStore implements AgentThreadStore {
|
|
|
142
368
|
|
|
143
369
|
return { threads: rows.map(rowToThreadRecord), hasMore, total }
|
|
144
370
|
}
|
|
371
|
+
|
|
372
|
+
async addParticipants(
|
|
373
|
+
input: AddAgentThreadParticipantsInput
|
|
374
|
+
): Promise<readonly AgentThreadParticipantRecord[]> {
|
|
375
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
376
|
+
// Concurrent adds to one thread queue on its row, so the limit counts every participant.
|
|
377
|
+
const [thread] = await tx<Pick<AgentThreadRow, "id" | "kind">[]>`
|
|
378
|
+
SELECT id, kind FROM agent_threads
|
|
379
|
+
WHERE project_id = ${input.projectId} AND id = ${input.threadId}
|
|
380
|
+
FOR UPDATE
|
|
381
|
+
`
|
|
382
|
+
if (!thread) {
|
|
383
|
+
throw new AgentStorageError(
|
|
384
|
+
"thread_not_found",
|
|
385
|
+
`[SixbPg] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
assertConversationThreadRow(thread, "has participants", "SixbPg")
|
|
389
|
+
|
|
390
|
+
const existing = await selectParticipants(tx, input.projectId, [input.threadId])
|
|
391
|
+
const added = participantsToAdd(input, existing, "SixbPg")
|
|
392
|
+
const addedAt = input.addedAt ?? new Date()
|
|
393
|
+
|
|
394
|
+
for (const principal of added) {
|
|
395
|
+
await tx`
|
|
396
|
+
INSERT INTO agent_thread_participants (
|
|
397
|
+
project_id,
|
|
398
|
+
thread_id,
|
|
399
|
+
principal_type,
|
|
400
|
+
principal_id,
|
|
401
|
+
added_at,
|
|
402
|
+
added_by_type,
|
|
403
|
+
added_by_id
|
|
404
|
+
) VALUES (
|
|
405
|
+
${input.projectId},
|
|
406
|
+
${input.threadId},
|
|
407
|
+
${principal.type},
|
|
408
|
+
${principal.id},
|
|
409
|
+
${addedAt},
|
|
410
|
+
${input.addedBy.type},
|
|
411
|
+
${input.addedBy.id}
|
|
412
|
+
)
|
|
413
|
+
`
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return added.length === 0
|
|
417
|
+
? existing
|
|
418
|
+
: selectParticipants(tx, input.projectId, [input.threadId])
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async removeParticipant(input: RemoveAgentThreadParticipantInput): Promise<boolean> {
|
|
423
|
+
const rows = await this.sql<{ principal_id: string }[]>`
|
|
424
|
+
DELETE FROM agent_thread_participants
|
|
425
|
+
WHERE project_id = ${input.projectId}
|
|
426
|
+
AND thread_id = ${input.threadId}
|
|
427
|
+
AND principal_type = ${input.principal.type}
|
|
428
|
+
AND principal_id = ${input.principal.id}
|
|
429
|
+
RETURNING principal_id
|
|
430
|
+
`
|
|
431
|
+
|
|
432
|
+
return rows.length > 0
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async listParticipants(
|
|
436
|
+
input: ListAgentThreadParticipantsInput
|
|
437
|
+
): Promise<readonly AgentThreadParticipantRecord[]> {
|
|
438
|
+
return selectParticipants(this.sql, input.projectId, [...new Set(input.threadIds)])
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function selectParticipants(
|
|
443
|
+
sql: PgStoreClient,
|
|
444
|
+
projectId: string,
|
|
445
|
+
threadIds: readonly string[]
|
|
446
|
+
): Promise<AgentThreadParticipantRecord[]> {
|
|
447
|
+
if (threadIds.length === 0) {
|
|
448
|
+
return []
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const rows = await sql<AgentThreadParticipantRow[]>`
|
|
452
|
+
SELECT *
|
|
453
|
+
FROM agent_thread_participants
|
|
454
|
+
WHERE project_id = ${projectId}
|
|
455
|
+
AND thread_id = ANY(${sql.array([...threadIds])}::text[])
|
|
456
|
+
ORDER BY added_at, principal_id COLLATE "C", thread_id COLLATE "C"
|
|
457
|
+
`
|
|
458
|
+
|
|
459
|
+
return rows.map(rowToParticipantRecord)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function insertCopiedCheckpoint(
|
|
463
|
+
tx: SQLClient,
|
|
464
|
+
checkpoint: AgentContextCheckpointRecord
|
|
465
|
+
): Promise<void> {
|
|
466
|
+
await tx`
|
|
467
|
+
INSERT INTO agent_context_checkpoints (
|
|
468
|
+
project_id,
|
|
469
|
+
id,
|
|
470
|
+
thread_id,
|
|
471
|
+
created_by_run_id,
|
|
472
|
+
reason,
|
|
473
|
+
summary,
|
|
474
|
+
summary_format_version,
|
|
475
|
+
summarized_through_seq,
|
|
476
|
+
observed_head_seq,
|
|
477
|
+
estimated_input_tokens_before,
|
|
478
|
+
estimated_input_tokens_after,
|
|
479
|
+
summary_model_id,
|
|
480
|
+
created_at
|
|
481
|
+
) VALUES (
|
|
482
|
+
${checkpoint.projectId},
|
|
483
|
+
${checkpoint.id},
|
|
484
|
+
${checkpoint.threadId},
|
|
485
|
+
${checkpoint.createdByRunId},
|
|
486
|
+
${checkpoint.reason},
|
|
487
|
+
${checkpoint.summary},
|
|
488
|
+
${checkpoint.summaryFormatVersion},
|
|
489
|
+
${checkpoint.summarizedThroughSeq},
|
|
490
|
+
${checkpoint.observedHeadSeq},
|
|
491
|
+
${checkpoint.estimatedInputTokensBefore},
|
|
492
|
+
${checkpoint.estimatedInputTokensAfter},
|
|
493
|
+
${checkpoint.summaryModelId},
|
|
494
|
+
${checkpoint.createdAt}
|
|
495
|
+
)
|
|
496
|
+
`
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** The copied messages name the fork's copies of the source's artifacts. */
|
|
500
|
+
async function remapCopiedParts(
|
|
501
|
+
sql: SQLClient,
|
|
502
|
+
projectId: string,
|
|
503
|
+
messageIds: readonly string[],
|
|
504
|
+
copies: Readonly<Record<string, string>> | undefined
|
|
505
|
+
): Promise<void> {
|
|
506
|
+
if (!copies || Object.keys(copies).length === 0 || messageIds.length === 0) return
|
|
507
|
+
|
|
508
|
+
const rows = await sql<{ id: string; parts: string }[]>`
|
|
509
|
+
SELECT id, parts::text AS parts FROM agent_messages
|
|
510
|
+
WHERE project_id = ${projectId} AND id = ANY(${sql.array([...messageIds])}::text[])
|
|
511
|
+
`
|
|
512
|
+
for (const row of rows) {
|
|
513
|
+
const parts = remapArtifactIds(row.parts, copies)
|
|
514
|
+
if (parts === row.parts) continue
|
|
515
|
+
|
|
516
|
+
await sql`
|
|
517
|
+
UPDATE agent_messages SET parts = ${parts}::text::jsonb
|
|
518
|
+
WHERE project_id = ${projectId} AND id = ${row.id}
|
|
519
|
+
`
|
|
520
|
+
}
|
|
145
521
|
}
|
package/src/ai-cost-groups.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
aiModelCallGroupFragmentFromRow,
|
|
4
4
|
buildAiModelCallGroupsFromFragments,
|
|
5
5
|
normalizeAiModelCallAccountingQuery,
|
|
6
|
+
TOKENLESS_MODEL_KINDS,
|
|
6
7
|
} from "@sixb/core/internal/ai-cost-storage-provider"
|
|
7
8
|
import type { ListAiModelCallGroupsInput, ListAiModelCallGroupsResult } from "@sixb/core/storage"
|
|
8
9
|
import type { PgStoreClient } from "./transactions"
|
|
@@ -34,11 +35,15 @@ export async function listAiModelCallGroups(
|
|
|
34
35
|
return { items, total, hasMore: query.offset + items.length < total }
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
// Fixed kind names, never user input.
|
|
39
|
+
const TOKENLESS_KINDS = TOKENLESS_MODEL_KINDS.map((kind) => `'${kind}'`).join(", ")
|
|
40
|
+
|
|
37
41
|
const FILTERED_SQL = `
|
|
38
42
|
WITH filtered AS (
|
|
39
43
|
SELECT COALESCE(root_agent.execution_id, usage.execution_id) AS root_execution_id,
|
|
40
44
|
usage.execution_id, usage.provider_id, usage.requested_model_id, usage.occurred_at,
|
|
41
|
-
usage.total_tokens,
|
|
45
|
+
usage.total_tokens, usage.model_kind, usage.output_images, usage.input_characters,
|
|
46
|
+
usage.audio_duration_ms,
|
|
42
47
|
execution.executor_kind, execution.executor_id, execution.authority_primitive_id AS primitive_id,
|
|
43
48
|
COALESCE(cost.status, 'unvalued') AS valuation_status,
|
|
44
49
|
cost.currency AS amount_currency, cost.amount_nanos,
|
|
@@ -84,7 +89,12 @@ SELECT filtered.root_execution_id, execution_id, provider_id, requested_model_id
|
|
|
84
89
|
MIN(occurred_at) AS first_call_at, MAX(occurred_at) AS last_call_at,
|
|
85
90
|
CAST(COUNT(*) AS TEXT) AS call_count,
|
|
86
91
|
CAST(COUNT(total_tokens) AS TEXT) AS token_count,
|
|
92
|
+
CAST(COUNT(CASE WHEN total_tokens IS NULL AND model_kind IN (${TOKENLESS_KINDS}) THEN 1 END) AS TEXT)
|
|
93
|
+
AS tokenless_count,
|
|
87
94
|
CAST(SUM(total_tokens) AS TEXT) AS total_tokens,
|
|
95
|
+
CAST(SUM(output_images) AS TEXT) AS output_images,
|
|
96
|
+
CAST(SUM(input_characters) AS TEXT) AS input_characters,
|
|
97
|
+
CAST(SUM(audio_duration_ms) AS TEXT) AS audio_duration_ms,
|
|
88
98
|
valuation_status, amount_currency,
|
|
89
99
|
CAST(SUM(amount_nanos / 1000000000000) AS TEXT) AS amount_high,
|
|
90
100
|
CAST(SUM((amount_nanos / 1000000) % 1000000) AS TEXT) AS amount_middle,
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
InvitationRecord,
|
|
12
12
|
SessionRecord,
|
|
13
13
|
SuspendUserAndRevokeSessionsInput,
|
|
14
|
+
UpdateAuthUserResult,
|
|
14
15
|
UserIdentityRecord,
|
|
15
16
|
UserRecord,
|
|
16
17
|
} from "@sixb/core/storage"
|
|
@@ -256,16 +257,17 @@ export class PgAuthStorage implements AuthStorage {
|
|
|
256
257
|
consumedAt: completedAt,
|
|
257
258
|
})
|
|
258
259
|
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
260
|
+
const previousUser = existingUserRow ? rowToUserRecord(existingUserRow) : null
|
|
261
|
+
const user =
|
|
262
|
+
previousUser ??
|
|
263
|
+
(await this.insertUser(tx, {
|
|
264
|
+
id: newUserId ?? assertNonEmpty(input.newUserId, "User id"),
|
|
265
|
+
projectId,
|
|
266
|
+
email,
|
|
267
|
+
displayName: input.newUserDisplayName,
|
|
268
|
+
avatarUrl: input.newUserAvatarUrl,
|
|
269
|
+
createdAt: completedAt,
|
|
270
|
+
}))
|
|
269
271
|
|
|
270
272
|
const invitation = await this.acceptInvitationAndApplyGroups(tx, {
|
|
271
273
|
activeInvitation,
|
|
@@ -293,6 +295,7 @@ export class PgAuthStorage implements AuthStorage {
|
|
|
293
295
|
session,
|
|
294
296
|
invitation: invitation.invitation,
|
|
295
297
|
groupMemberships,
|
|
298
|
+
previousUser,
|
|
296
299
|
}
|
|
297
300
|
}
|
|
298
301
|
)
|
|
@@ -410,6 +413,7 @@ export class PgAuthStorage implements AuthStorage {
|
|
|
410
413
|
])
|
|
411
414
|
await this.consumeOidcAttempt(input, completedAt, projectId, tx)
|
|
412
415
|
|
|
416
|
+
const previousUser = userRow ? rowToUserRecord(userRow) : null
|
|
413
417
|
let user: UserRecord
|
|
414
418
|
if (!userRow) {
|
|
415
419
|
user = await this.insertUser(tx, {
|
|
@@ -431,7 +435,7 @@ export class PgAuthStorage implements AuthStorage {
|
|
|
431
435
|
updated_at: user.updatedAt,
|
|
432
436
|
}
|
|
433
437
|
} else {
|
|
434
|
-
user =
|
|
438
|
+
user = await this.refreshProviderProfile(tx, userRow, input, completedAt)
|
|
435
439
|
}
|
|
436
440
|
|
|
437
441
|
const invitation = await this.acceptInvitationAndApplyGroups(tx, {
|
|
@@ -476,6 +480,7 @@ export class PgAuthStorage implements AuthStorage {
|
|
|
476
480
|
identity: nextIdentity,
|
|
477
481
|
invitation: invitation.invitation,
|
|
478
482
|
groupMemberships,
|
|
483
|
+
previousUser,
|
|
479
484
|
}
|
|
480
485
|
}
|
|
481
486
|
)
|
|
@@ -489,7 +494,7 @@ export class PgAuthStorage implements AuthStorage {
|
|
|
489
494
|
|
|
490
495
|
async suspendUserAndRevokeSessions(
|
|
491
496
|
input: SuspendUserAndRevokeSessionsInput
|
|
492
|
-
): Promise<
|
|
497
|
+
): Promise<UpdateAuthUserResult> {
|
|
493
498
|
return runPgTransaction(this.sql, async (tx) => {
|
|
494
499
|
await lockAdvisoryKeys(tx, [authLockKey("sessions", input.projectId, input.userId)])
|
|
495
500
|
const existing = await getUserRowById(
|
|
@@ -523,7 +528,7 @@ export class PgAuthStorage implements AuthStorage {
|
|
|
523
528
|
revokedAt: input.suspendedAt,
|
|
524
529
|
})
|
|
525
530
|
|
|
526
|
-
return rowToUserRecord(updated)
|
|
531
|
+
return { user: rowToUserRecord(updated), previousUser: rowToUserRecord(existing) }
|
|
527
532
|
})
|
|
528
533
|
}
|
|
529
534
|
|
|
@@ -733,6 +738,33 @@ export class PgAuthStorage implements AuthStorage {
|
|
|
733
738
|
})
|
|
734
739
|
}
|
|
735
740
|
|
|
741
|
+
// The identity provider owns the name and picture it sends: every sign-in overwrites them, and
|
|
742
|
+
// one it leaves out keeps the stored value.
|
|
743
|
+
private async refreshProviderProfile(
|
|
744
|
+
sql: SQLClient,
|
|
745
|
+
row: PgAuthUserRow,
|
|
746
|
+
input: CompleteOidcSignInInput,
|
|
747
|
+
updatedAt: Date
|
|
748
|
+
): Promise<UserRecord> {
|
|
749
|
+
const displayName = input.displayName ?? row.display_name
|
|
750
|
+
const avatarUrl = input.avatarUrl ?? row.avatar_url
|
|
751
|
+
if (displayName === row.display_name && avatarUrl === row.avatar_url) {
|
|
752
|
+
return rowToUserRecord(row)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const [updated] = await sql<PgAuthUserRow[]>`
|
|
756
|
+
UPDATE auth_users
|
|
757
|
+
SET display_name = ${displayName},
|
|
758
|
+
avatar_url = ${avatarUrl},
|
|
759
|
+
updated_at = ${updatedAt}
|
|
760
|
+
WHERE project_id = ${row.project_id}
|
|
761
|
+
AND id = ${row.id}
|
|
762
|
+
RETURNING *
|
|
763
|
+
`
|
|
764
|
+
|
|
765
|
+
return rowToUserRecord(updated)
|
|
766
|
+
}
|
|
767
|
+
|
|
736
768
|
private async consumeOidcAttempt(
|
|
737
769
|
input: CompleteOidcSignInInput,
|
|
738
770
|
completedAt: Date,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AuthSessionStore,
|
|
3
3
|
CreateAuthSessionInput,
|
|
4
|
+
ListAuthSessionsInput,
|
|
5
|
+
ListAuthSessionsResult,
|
|
4
6
|
RotateSessionRefreshTokenResult,
|
|
5
7
|
SessionRecord,
|
|
6
8
|
} from "@sixb/core/storage"
|
|
@@ -56,6 +58,24 @@ export class PgAuthSessionStore implements AuthSessionStore {
|
|
|
56
58
|
return rows.map(rowToSessionRecord)
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
async listByUserId(params: ListAuthSessionsInput): Promise<ListAuthSessionsResult> {
|
|
62
|
+
const rows = (await this.sql`
|
|
63
|
+
SELECT *
|
|
64
|
+
FROM auth_sessions
|
|
65
|
+
WHERE project_id = ${params.projectId}
|
|
66
|
+
AND user_id = ${params.userId}
|
|
67
|
+
${params.before ? this.sql`AND created_at <= ${params.before.createdAt}` : this.sql``}
|
|
68
|
+
ORDER BY created_at DESC, id DESC
|
|
69
|
+
LIMIT ${params.limit + 1}
|
|
70
|
+
OFFSET ${params.before?.skip ?? 0}
|
|
71
|
+
`) as PgAuthSessionRow[]
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
sessions: rows.slice(0, params.limit).map(rowToSessionRecord),
|
|
75
|
+
hasMore: rows.length > params.limit,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
59
79
|
async findValidByTokenHash(params: {
|
|
60
80
|
readonly projectId: string
|
|
61
81
|
readonly id: string
|