@sixb/pg 0.0.1
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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/agents/agent-storage.d.ts +20 -0
- package/dist/agents/agent-storage.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/messages.d.ts +13 -0
- package/dist/agents/messages.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +77 -0
- package/dist/agents/rows.d.ts.map +1 -0
- package/dist/agents/runs.d.ts +25 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +13 -0
- package/dist/agents/threads.d.ts.map +1 -0
- package/dist/auth-storage/access-tokens.d.ts +33 -0
- package/dist/auth-storage/access-tokens.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +41 -0
- package/dist/auth-storage/auth-storage.d.ts.map +1 -0
- package/dist/auth-storage/group-memberships.d.ts +21 -0
- package/dist/auth-storage/group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/identities.d.ts +17 -0
- package/dist/auth-storage/identities.d.ts.map +1 -0
- package/dist/auth-storage/index.d.ts +3 -0
- package/dist/auth-storage/index.d.ts.map +1 -0
- package/dist/auth-storage/invitations.d.ts +29 -0
- package/dist/auth-storage/invitations.d.ts.map +1 -0
- package/dist/auth-storage/magic-links.d.ts +28 -0
- package/dist/auth-storage/magic-links.d.ts.map +1 -0
- package/dist/auth-storage/oidc-attempts.d.ts +18 -0
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +138 -0
- package/dist/auth-storage/rows.d.ts.map +1 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts +14 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +55 -0
- package/dist/auth-storage/sessions.d.ts.map +1 -0
- package/dist/auth-storage/shared.d.ts +159 -0
- package/dist/auth-storage/shared.d.ts.map +1 -0
- package/dist/auth-storage/users.d.ts +19 -0
- package/dist/auth-storage/users.d.ts.map +1 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11957 -0
- package/dist/index.js.map +55 -0
- package/dist/latest-run-query.d.ts +24 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +20 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +11 -0
- package/dist/ontology-storage/commits.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +22 -0
- package/dist/ontology-storage/index.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-session.d.ts +63 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +69 -0
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
- package/dist/ontology-storage/materializations.d.ts +33 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +12 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +95 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +25 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +21 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -0
- package/dist/pg-client.d.ts +58 -0
- package/dist/pg-client.d.ts.map +1 -0
- package/dist/pg-object-query-compiler.d.ts +55 -0
- package/dist/pg-object-query-compiler.d.ts.map +1 -0
- package/dist/pg-object-storage.d.ts +90 -0
- package/dist/pg-object-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +18 -0
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
- package/dist/pg-projection-run-storage.d.ts +19 -0
- package/dist/pg-projection-run-storage.d.ts.map +1 -0
- package/dist/pg-rules-storage.d.ts +25 -0
- package/dist/pg-rules-storage.d.ts.map +1 -0
- package/dist/pg-sync-run-storage.d.ts +15 -0
- package/dist/pg-sync-run-storage.d.ts.map +1 -0
- package/dist/pg-timeseries-storage.d.ts +32 -0
- package/dist/pg-timeseries-storage.d.ts.map +1 -0
- package/dist/pg-webhook-delivery-storage.d.ts +17 -0
- package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
- package/dist/pg-webhook-run-storage.d.ts +14 -0
- package/dist/pg-webhook-run-storage.d.ts.map +1 -0
- package/dist/pg-workflow-intervention-storage.d.ts +16 -0
- package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
- package/dist/pg-workflow-run-storage.d.ts +49 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +30 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +16 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/transactions.d.ts +10 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/package.json +60 -0
- package/src/agents/agent-storage.ts +26 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +144 -0
- package/src/agents/rows.ts +227 -0
- package/src/agents/runs.ts +330 -0
- package/src/agents/threads.ts +116 -0
- package/src/auth-storage/access-tokens.ts +254 -0
- package/src/auth-storage/auth-storage.ts +798 -0
- package/src/auth-storage/group-memberships.ts +47 -0
- package/src/auth-storage/identities.ts +85 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +280 -0
- package/src/auth-storage/magic-links.ts +140 -0
- package/src/auth-storage/oidc-attempts.ts +93 -0
- package/src/auth-storage/rows.ts +352 -0
- package/src/auth-storage/service-account-group-memberships.ts +150 -0
- package/src/auth-storage/service-accounts.ts +154 -0
- package/src/auth-storage/sessions.ts +193 -0
- package/src/auth-storage/shared.ts +799 -0
- package/src/auth-storage/users.ts +196 -0
- package/src/index.ts +367 -0
- package/src/latest-run-query.ts +42 -0
- package/src/migrations/001-initial-schema.sql +1195 -0
- package/src/migrations.ts +280 -0
- package/src/ontology-storage/commits.ts +115 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +426 -0
- package/src/ontology-storage/materialization-state.ts +1109 -0
- package/src/ontology-storage/materialization-writer.ts +509 -0
- package/src/ontology-storage/materializations.ts +939 -0
- package/src/ontology-storage/outbox.ts +216 -0
- package/src/ontology-storage/shared.ts +293 -0
- package/src/ontology-storage/sources.ts +658 -0
- package/src/pg-action-run-storage.ts +657 -0
- package/src/pg-client.ts +124 -0
- package/src/pg-object-query-compiler.ts +1545 -0
- package/src/pg-object-storage.ts +638 -0
- package/src/pg-pipeline-run-storage.ts +457 -0
- package/src/pg-projection-run-storage.ts +526 -0
- package/src/pg-rules-storage.ts +308 -0
- package/src/pg-sync-run-storage.ts +263 -0
- package/src/pg-timeseries-storage.ts +180 -0
- package/src/pg-webhook-delivery-storage.ts +180 -0
- package/src/pg-webhook-run-storage.ts +185 -0
- package/src/pg-workflow-intervention-storage.ts +339 -0
- package/src/pg-workflow-run-storage.ts +1065 -0
- package/src/run-list-query.ts +108 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +72 -0
- package/src/transactions.ts +58 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { AGENT_MESSAGE_CONTENT_VERSION } from "@sixb/core/internal/agents"
|
|
2
|
+
import {
|
|
3
|
+
type AgentMessageRecord,
|
|
4
|
+
type AgentMessageStore,
|
|
5
|
+
AgentStorageError,
|
|
6
|
+
type AppendAgentMessageInput,
|
|
7
|
+
type ListAgentMessagesInput,
|
|
8
|
+
type ListAgentMessagesResult,
|
|
9
|
+
} from "@sixb/core/storage"
|
|
10
|
+
import type { SqlParameter } from "../pg-client"
|
|
11
|
+
import { isUniqueViolation } from "../storage-errors"
|
|
12
|
+
import { type PgStoreClient, runPgTransaction } from "../transactions"
|
|
13
|
+
import { type AgentMessageRow, queryAgentList, rowToMessageRecord } from "./rows"
|
|
14
|
+
|
|
15
|
+
export class PgAgentMessageStore implements AgentMessageStore {
|
|
16
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
17
|
+
|
|
18
|
+
async append(input: AppendAgentMessageInput): Promise<AgentMessageRecord> {
|
|
19
|
+
const createdAt = input.createdAt ?? new Date()
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
return await runPgTransaction(this.sql, async (tx) => {
|
|
23
|
+
const [thread] = await tx<{ id: string }[]>`
|
|
24
|
+
SELECT id FROM agent_threads
|
|
25
|
+
WHERE project_id = ${input.projectId} AND id = ${input.threadId}
|
|
26
|
+
FOR UPDATE
|
|
27
|
+
`
|
|
28
|
+
|
|
29
|
+
if (!thread) {
|
|
30
|
+
throw new AgentStorageError(
|
|
31
|
+
"thread_not_found",
|
|
32
|
+
`[SixbPg] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (input.runId !== null) {
|
|
37
|
+
const [run] = await tx<{ id: string }[]>`
|
|
38
|
+
SELECT id FROM agent_runs WHERE project_id = ${input.projectId} AND id = ${input.runId}
|
|
39
|
+
`
|
|
40
|
+
|
|
41
|
+
if (!run) {
|
|
42
|
+
throw new AgentStorageError(
|
|
43
|
+
"run_not_found",
|
|
44
|
+
`[SixbPg] Agent run '${input.runId}' not found for project '${input.projectId}'.`
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const [seqRow] = await tx<{ next: number | string }[]>`
|
|
50
|
+
SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM agent_messages
|
|
51
|
+
WHERE project_id = ${input.projectId} AND thread_id = ${input.threadId}
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
const [row] = await tx<AgentMessageRow[]>`
|
|
55
|
+
INSERT INTO agent_messages (
|
|
56
|
+
project_id,
|
|
57
|
+
id,
|
|
58
|
+
thread_id,
|
|
59
|
+
run_id,
|
|
60
|
+
role,
|
|
61
|
+
author_principal_type,
|
|
62
|
+
author_principal_id,
|
|
63
|
+
seq,
|
|
64
|
+
parts,
|
|
65
|
+
metadata,
|
|
66
|
+
content_version,
|
|
67
|
+
created_at,
|
|
68
|
+
completed_at
|
|
69
|
+
) VALUES (
|
|
70
|
+
${input.projectId},
|
|
71
|
+
${input.id},
|
|
72
|
+
${input.threadId},
|
|
73
|
+
${input.runId},
|
|
74
|
+
${input.role},
|
|
75
|
+
${input.authorPrincipal?.type ?? null},
|
|
76
|
+
${input.authorPrincipal?.id ?? null},
|
|
77
|
+
${Number(seqRow.next)},
|
|
78
|
+
${JSON.stringify(input.parts)}::text::jsonb,
|
|
79
|
+
${input.metadata === undefined ? null : JSON.stringify(input.metadata)}::text::jsonb,
|
|
80
|
+
${AGENT_MESSAGE_CONTENT_VERSION},
|
|
81
|
+
${createdAt},
|
|
82
|
+
${input.completedAt ?? null}
|
|
83
|
+
)
|
|
84
|
+
RETURNING *
|
|
85
|
+
`
|
|
86
|
+
|
|
87
|
+
await tx`
|
|
88
|
+
UPDATE agent_threads
|
|
89
|
+
SET message_count = message_count + 1, last_message_at = ${createdAt}, updated_at = ${createdAt}
|
|
90
|
+
WHERE project_id = ${input.projectId} AND id = ${input.threadId}
|
|
91
|
+
`
|
|
92
|
+
|
|
93
|
+
return rowToMessageRecord(row)
|
|
94
|
+
})
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (isUniqueViolation(error)) {
|
|
97
|
+
throw new AgentStorageError(
|
|
98
|
+
"duplicate_id",
|
|
99
|
+
`[SixbPg] Agent message '${input.id}' already exists for project '${input.projectId}'.`
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw error
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async getById(params: { projectId: string; id: string }): Promise<AgentMessageRecord | null> {
|
|
108
|
+
const [row] = await this.sql<AgentMessageRow[]>`
|
|
109
|
+
SELECT * FROM agent_messages WHERE project_id = ${params.projectId} AND id = ${params.id}
|
|
110
|
+
`
|
|
111
|
+
|
|
112
|
+
return row ? rowToMessageRecord(row) : null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async list(input: ListAgentMessagesInput): Promise<ListAgentMessagesResult> {
|
|
116
|
+
if (input.roles !== undefined && input.roles.length === 0) {
|
|
117
|
+
return { messages: [], hasMore: false, total: 0 }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const whereClauses = ["project_id = $1", "thread_id = $2"]
|
|
121
|
+
const params: SqlParameter[] = [input.projectId, input.threadId]
|
|
122
|
+
let index = 3
|
|
123
|
+
|
|
124
|
+
if (input.roles) {
|
|
125
|
+
const placeholders = input.roles.map(() => `$${index++}`)
|
|
126
|
+
whereClauses.push(`role IN (${placeholders.join(", ")})`)
|
|
127
|
+
params.push(...input.roles)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const dir = input.order === "desc" ? "DESC" : "ASC"
|
|
131
|
+
const { rows, total, hasMore } = await queryAgentList<AgentMessageRow>({
|
|
132
|
+
sql: this.sql,
|
|
133
|
+
table: "agent_messages",
|
|
134
|
+
whereClauses,
|
|
135
|
+
params,
|
|
136
|
+
nextIndex: index,
|
|
137
|
+
orderBy: `seq ${dir}`,
|
|
138
|
+
limit: input.limit,
|
|
139
|
+
offset: input.offset,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return { messages: rows.map(rowToMessageRecord), hasMore, total }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { type AgentMessagePart, type Principal, SYSTEM_PRINCIPAL } from "@sixb/core"
|
|
2
|
+
import {
|
|
3
|
+
type AgentMessageRecord,
|
|
4
|
+
type AgentRunDiagnostic,
|
|
5
|
+
type AgentRunRecord,
|
|
6
|
+
type AgentRunUsage,
|
|
7
|
+
type AgentThreadRecord,
|
|
8
|
+
coerceAgentRunFinishReason,
|
|
9
|
+
} from "@sixb/core/storage"
|
|
10
|
+
import type { SQLClient, SqlParameter } from "../pg-client"
|
|
11
|
+
|
|
12
|
+
// ── row shapes (porsager returns timestamptz as Date, jsonb parsed, int as number — typed loosely
|
|
13
|
+
// to tolerate the string forms some drivers/poolers surface) ──────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface AgentThreadRow {
|
|
16
|
+
project_id: string
|
|
17
|
+
id: string
|
|
18
|
+
agent_id: string
|
|
19
|
+
owner_principal_type: Principal["type"]
|
|
20
|
+
owner_principal_id: string
|
|
21
|
+
title: string | null
|
|
22
|
+
status: AgentThreadRecord["status"]
|
|
23
|
+
active_run_id: string | null
|
|
24
|
+
last_message_at: Date | string | null
|
|
25
|
+
message_count: number | string
|
|
26
|
+
created_at: Date | string
|
|
27
|
+
updated_at: Date | string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AgentRunRow {
|
|
31
|
+
project_id: string
|
|
32
|
+
id: string
|
|
33
|
+
thread_id: string
|
|
34
|
+
agent_id: string
|
|
35
|
+
trigger_message_id: string
|
|
36
|
+
requested_by_principal_type: Principal["type"] | null
|
|
37
|
+
requested_by_principal_id: string | null
|
|
38
|
+
execution_principal_type: "serviceAccount" | null
|
|
39
|
+
execution_principal_id: string | null
|
|
40
|
+
status: AgentRunRecord["status"]
|
|
41
|
+
model_id: string | null
|
|
42
|
+
finish_reason: string | null
|
|
43
|
+
usage_input_tokens: number | string | null
|
|
44
|
+
usage_output_tokens: number | string | null
|
|
45
|
+
usage_total_tokens: number | string | null
|
|
46
|
+
usage_reasoning_tokens: number | string | null
|
|
47
|
+
usage_cached_input_tokens: number | string | null
|
|
48
|
+
error: string | null
|
|
49
|
+
diagnostics: AgentRunDiagnostic[] | string | null
|
|
50
|
+
attempt: number | string
|
|
51
|
+
execution_token: string | null
|
|
52
|
+
execution_queue_lease_expires_at: Date | string | null
|
|
53
|
+
created_at: Date | string
|
|
54
|
+
started_at: Date | string | null
|
|
55
|
+
completed_at: Date | string | null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AgentMessageRow {
|
|
59
|
+
project_id: string
|
|
60
|
+
id: string
|
|
61
|
+
thread_id: string
|
|
62
|
+
run_id: string | null
|
|
63
|
+
role: AgentMessageRecord["role"]
|
|
64
|
+
author_principal_type: Principal["type"] | null
|
|
65
|
+
author_principal_id: string | null
|
|
66
|
+
seq: number | string
|
|
67
|
+
parts: AgentMessagePart[] | string
|
|
68
|
+
metadata: unknown
|
|
69
|
+
content_version: number | string
|
|
70
|
+
created_at: Date | string
|
|
71
|
+
completed_at: Date | string | null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── row → record mappers ────────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export function rowToThreadRecord(row: AgentThreadRow): AgentThreadRecord {
|
|
77
|
+
return {
|
|
78
|
+
id: row.id,
|
|
79
|
+
projectId: row.project_id,
|
|
80
|
+
agentId: row.agent_id,
|
|
81
|
+
ownerPrincipal: { type: row.owner_principal_type, id: row.owner_principal_id },
|
|
82
|
+
title: row.title ?? undefined,
|
|
83
|
+
status: row.status,
|
|
84
|
+
activeRunId: row.active_run_id,
|
|
85
|
+
lastMessageAt: row.last_message_at ? new Date(row.last_message_at) : undefined,
|
|
86
|
+
messageCount: Number(row.message_count),
|
|
87
|
+
createdAt: new Date(row.created_at),
|
|
88
|
+
updatedAt: new Date(row.updated_at),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function rowToRunRecord(row: AgentRunRow): AgentRunRecord {
|
|
93
|
+
return {
|
|
94
|
+
id: row.id,
|
|
95
|
+
projectId: row.project_id,
|
|
96
|
+
threadId: row.thread_id,
|
|
97
|
+
agentId: row.agent_id,
|
|
98
|
+
triggerMessageId: row.trigger_message_id,
|
|
99
|
+
requestedByPrincipal:
|
|
100
|
+
principalFromColumns(row.requested_by_principal_type, row.requested_by_principal_id) ??
|
|
101
|
+
SYSTEM_PRINCIPAL,
|
|
102
|
+
executionPrincipal: serviceAccountPrincipalFromColumns(
|
|
103
|
+
row.execution_principal_type,
|
|
104
|
+
row.execution_principal_id
|
|
105
|
+
),
|
|
106
|
+
status: row.status,
|
|
107
|
+
modelId: row.model_id ?? undefined,
|
|
108
|
+
finishReason: coerceAgentRunFinishReason(row.finish_reason),
|
|
109
|
+
usage: rowToUsage(row),
|
|
110
|
+
error: row.error ?? undefined,
|
|
111
|
+
diagnostics: normalizeDiagnostics(row.diagnostics),
|
|
112
|
+
attempt: Number(row.attempt),
|
|
113
|
+
execution:
|
|
114
|
+
row.execution_token && row.execution_queue_lease_expires_at
|
|
115
|
+
? {
|
|
116
|
+
token: row.execution_token,
|
|
117
|
+
queueLeaseExpiresAt: new Date(row.execution_queue_lease_expires_at),
|
|
118
|
+
}
|
|
119
|
+
: undefined,
|
|
120
|
+
createdAt: new Date(row.created_at),
|
|
121
|
+
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
122
|
+
completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function rowToMessageRecord(row: AgentMessageRow): AgentMessageRecord {
|
|
127
|
+
return {
|
|
128
|
+
id: row.id,
|
|
129
|
+
projectId: row.project_id,
|
|
130
|
+
threadId: row.thread_id,
|
|
131
|
+
runId: row.run_id,
|
|
132
|
+
role: row.role,
|
|
133
|
+
authorPrincipal: principalFromColumns(row.author_principal_type, row.author_principal_id),
|
|
134
|
+
seq: Number(row.seq),
|
|
135
|
+
parts:
|
|
136
|
+
typeof row.parts === "string" ? (JSON.parse(row.parts) as AgentMessagePart[]) : row.parts,
|
|
137
|
+
// A null/absent column and an explicit JSON `null` both mean "no metadata" — jsonb cannot tell
|
|
138
|
+
// SQL NULL from a json null on read, so both backends normalise to `undefined` for parity.
|
|
139
|
+
metadata: normalizeMetadata(row.metadata),
|
|
140
|
+
contentVersion: Number(row.content_version),
|
|
141
|
+
createdAt: new Date(row.created_at),
|
|
142
|
+
completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function principalFromColumns(
|
|
147
|
+
type: Principal["type"] | null | undefined,
|
|
148
|
+
id: string | null | undefined
|
|
149
|
+
): Principal | undefined {
|
|
150
|
+
return type && id ? { type, id } : undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function serviceAccountPrincipalFromColumns(
|
|
154
|
+
type: "serviceAccount" | null | undefined,
|
|
155
|
+
id: string | null | undefined
|
|
156
|
+
): Extract<Principal, { readonly type: "serviceAccount" }> | undefined {
|
|
157
|
+
return type === "serviceAccount" && id ? { type, id } : undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function rowToUsage(row: AgentRunRow): AgentRunUsage | undefined {
|
|
161
|
+
const usage: AgentRunUsage = {
|
|
162
|
+
...(row.usage_input_tokens === null ? {} : { inputTokens: Number(row.usage_input_tokens) }),
|
|
163
|
+
...(row.usage_output_tokens === null ? {} : { outputTokens: Number(row.usage_output_tokens) }),
|
|
164
|
+
...(row.usage_total_tokens === null ? {} : { totalTokens: Number(row.usage_total_tokens) }),
|
|
165
|
+
...(row.usage_reasoning_tokens === null
|
|
166
|
+
? {}
|
|
167
|
+
: { reasoningTokens: Number(row.usage_reasoning_tokens) }),
|
|
168
|
+
...(row.usage_cached_input_tokens === null
|
|
169
|
+
? {}
|
|
170
|
+
: { cachedInputTokens: Number(row.usage_cached_input_tokens) }),
|
|
171
|
+
}
|
|
172
|
+
return Object.keys(usage).length === 0 ? undefined : usage
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeMetadata(value: unknown): AgentMessageRecord["metadata"] {
|
|
176
|
+
if (value === null || value === undefined) {
|
|
177
|
+
return undefined
|
|
178
|
+
}
|
|
179
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value
|
|
180
|
+
return parsed === null ? undefined : (parsed as AgentMessageRecord["metadata"])
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeDiagnostics(
|
|
184
|
+
value: AgentRunRow["diagnostics"]
|
|
185
|
+
): readonly AgentRunDiagnostic[] | undefined {
|
|
186
|
+
if (value === null) {
|
|
187
|
+
return undefined
|
|
188
|
+
}
|
|
189
|
+
return typeof value === "string" ? (JSON.parse(value) as AgentRunDiagnostic[]) : value
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── offset pagination (threads / messages order by their own column, not started_at) ─────────────
|
|
193
|
+
|
|
194
|
+
export async function queryAgentList<TRow>(input: {
|
|
195
|
+
readonly sql: SQLClient
|
|
196
|
+
readonly table: "agent_threads" | "agent_messages"
|
|
197
|
+
readonly whereClauses: readonly string[]
|
|
198
|
+
readonly params: readonly SqlParameter[]
|
|
199
|
+
readonly nextIndex: number
|
|
200
|
+
readonly orderBy: string
|
|
201
|
+
readonly limit?: number
|
|
202
|
+
readonly offset?: number
|
|
203
|
+
}): Promise<{ readonly rows: readonly TRow[]; readonly total: number; readonly hasMore: boolean }> {
|
|
204
|
+
const where = `WHERE ${input.whereClauses.join(" AND ")}`
|
|
205
|
+
const offset = input.offset ?? 0
|
|
206
|
+
|
|
207
|
+
const [totalRow] = await input.sql.unsafe<{ count: string | number }[]>(
|
|
208
|
+
`SELECT COUNT(*)::bigint AS count FROM ${input.table} ${where}`,
|
|
209
|
+
[...input.params] as SqlParameter[]
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
const queryParams = [...input.params] as SqlParameter[]
|
|
213
|
+
let query = `SELECT * FROM ${input.table} ${where} ORDER BY ${input.orderBy}`
|
|
214
|
+
let nextIndex = input.nextIndex
|
|
215
|
+
|
|
216
|
+
if (input.limit !== undefined) {
|
|
217
|
+
query += ` LIMIT $${nextIndex++} OFFSET $${nextIndex++}`
|
|
218
|
+
queryParams.push(input.limit, offset)
|
|
219
|
+
} else if (offset > 0) {
|
|
220
|
+
query += ` OFFSET $${nextIndex++}`
|
|
221
|
+
queryParams.push(offset)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const rows = await input.sql.unsafe<TRow[]>(query, queryParams)
|
|
225
|
+
const total = Number(totalRow?.count ?? 0)
|
|
226
|
+
return { rows, total, hasMore: offset + rows.length < total }
|
|
227
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AgentRunRecord,
|
|
3
|
+
type AgentRunStore,
|
|
4
|
+
AgentStorageError,
|
|
5
|
+
type ConfirmAgentRunExecutionOwnershipInput,
|
|
6
|
+
type CreateAgentRunInput,
|
|
7
|
+
type FinishAgentRunInput,
|
|
8
|
+
type FinishQueuedAgentRunInput,
|
|
9
|
+
type ListAgentRunsInput,
|
|
10
|
+
type ListAgentRunsResult,
|
|
11
|
+
type ReclaimAgentRunInput,
|
|
12
|
+
type StartAgentRunInput,
|
|
13
|
+
} from "@sixb/core/storage"
|
|
14
|
+
import type { SQLClient, SqlParameter } from "../pg-client"
|
|
15
|
+
import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "../run-list-query"
|
|
16
|
+
import { isUniqueViolation } from "../storage-errors"
|
|
17
|
+
import { type PgStoreClient, runPgTransaction } from "../transactions"
|
|
18
|
+
import { type AgentRunRow, rowToRunRecord } from "./rows"
|
|
19
|
+
|
|
20
|
+
const PG_RUN_ID_BATCH_SIZE = 10_000
|
|
21
|
+
|
|
22
|
+
export class PgAgentRunStore implements AgentRunStore {
|
|
23
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
24
|
+
|
|
25
|
+
async create(input: CreateAgentRunInput): Promise<AgentRunRecord> {
|
|
26
|
+
const createdAt = input.createdAt ?? new Date()
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
return await runPgTransaction(this.sql, async (tx) => {
|
|
30
|
+
const [thread] = await tx<{ active_run_id: string | null }[]>`
|
|
31
|
+
SELECT active_run_id FROM agent_threads
|
|
32
|
+
WHERE project_id = ${input.projectId} AND id = ${input.threadId}
|
|
33
|
+
FOR UPDATE
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
if (!thread) {
|
|
37
|
+
throw new AgentStorageError(
|
|
38
|
+
"thread_not_found",
|
|
39
|
+
`[SixbPg] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (thread.active_run_id !== null) {
|
|
44
|
+
throw new AgentStorageError(
|
|
45
|
+
"active_run_exists",
|
|
46
|
+
`[SixbPg] Agent thread '${input.threadId}' already has an active run '${thread.active_run_id}'.`
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const [row] = await tx<AgentRunRow[]>`
|
|
51
|
+
INSERT INTO agent_runs (
|
|
52
|
+
project_id,
|
|
53
|
+
id,
|
|
54
|
+
thread_id,
|
|
55
|
+
agent_id,
|
|
56
|
+
trigger_message_id,
|
|
57
|
+
requested_by_principal_type,
|
|
58
|
+
requested_by_principal_id,
|
|
59
|
+
status,
|
|
60
|
+
attempt,
|
|
61
|
+
created_at
|
|
62
|
+
) VALUES (
|
|
63
|
+
${input.projectId},
|
|
64
|
+
${input.id},
|
|
65
|
+
${input.threadId},
|
|
66
|
+
${input.agentId},
|
|
67
|
+
${input.triggerMessageId},
|
|
68
|
+
${input.requestedByPrincipal.type},
|
|
69
|
+
${input.requestedByPrincipal.id},
|
|
70
|
+
${"queued"},
|
|
71
|
+
${0},
|
|
72
|
+
${createdAt}
|
|
73
|
+
)
|
|
74
|
+
RETURNING *
|
|
75
|
+
`
|
|
76
|
+
|
|
77
|
+
await tx`
|
|
78
|
+
UPDATE agent_threads SET active_run_id = ${input.id}, updated_at = ${createdAt}
|
|
79
|
+
WHERE project_id = ${input.projectId} AND id = ${input.threadId}
|
|
80
|
+
`
|
|
81
|
+
|
|
82
|
+
return rowToRunRecord(row)
|
|
83
|
+
})
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (isUniqueViolation(error)) {
|
|
86
|
+
throw new AgentStorageError(
|
|
87
|
+
"duplicate_id",
|
|
88
|
+
`[SixbPg] Agent run '${input.id}' already exists for project '${input.projectId}'.`
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw error
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async start(input: StartAgentRunInput): Promise<AgentRunRecord> {
|
|
97
|
+
const startedAt = input.startedAt ?? new Date()
|
|
98
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
99
|
+
await this.lockStatus(tx, input.projectId, input.id, "queued")
|
|
100
|
+
const [row] = await tx<AgentRunRow[]>`
|
|
101
|
+
UPDATE agent_runs
|
|
102
|
+
SET
|
|
103
|
+
status = ${"running"},
|
|
104
|
+
execution_principal_type = ${input.executionPrincipal?.type ?? null},
|
|
105
|
+
execution_principal_id = ${input.executionPrincipal?.id ?? null},
|
|
106
|
+
model_id = ${input.modelId ?? null},
|
|
107
|
+
attempt = ${1},
|
|
108
|
+
execution_token = ${input.execution.token},
|
|
109
|
+
execution_queue_lease_expires_at = ${input.execution.queueLeaseExpiresAt},
|
|
110
|
+
started_at = ${startedAt}
|
|
111
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
112
|
+
RETURNING *
|
|
113
|
+
`
|
|
114
|
+
return rowToRunRecord(row)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async finishQueued(input: FinishQueuedAgentRunInput): Promise<AgentRunRecord> {
|
|
119
|
+
const completedAt = input.completedAt ?? new Date()
|
|
120
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
121
|
+
const run = await this.lockStatus(tx, input.projectId, input.id, "queued")
|
|
122
|
+
const [row] = await tx<AgentRunRow[]>`
|
|
123
|
+
UPDATE agent_runs
|
|
124
|
+
SET status = ${input.status}, error = ${input.error ?? null}, completed_at = ${completedAt}
|
|
125
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
126
|
+
RETURNING *
|
|
127
|
+
`
|
|
128
|
+
await this.releaseThread(tx, run, completedAt)
|
|
129
|
+
return rowToRunRecord(row)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async reclaim(input: ReclaimAgentRunInput): Promise<AgentRunRecord> {
|
|
134
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
135
|
+
await this.lockRunning(tx, input.projectId, input.id)
|
|
136
|
+
const [row] = await tx<AgentRunRow[]>`
|
|
137
|
+
UPDATE agent_runs
|
|
138
|
+
SET
|
|
139
|
+
attempt = attempt + 1,
|
|
140
|
+
execution_token = ${input.execution.token},
|
|
141
|
+
execution_queue_lease_expires_at = ${input.execution.queueLeaseExpiresAt}
|
|
142
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
143
|
+
RETURNING *
|
|
144
|
+
`
|
|
145
|
+
|
|
146
|
+
return rowToRunRecord(row)
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async confirmExecutionOwnership(
|
|
151
|
+
input: ConfirmAgentRunExecutionOwnershipInput
|
|
152
|
+
): Promise<AgentRunRecord> {
|
|
153
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
154
|
+
const run = await this.lockRunning(tx, input.projectId, input.id)
|
|
155
|
+
if (run.execution_token !== input.executionToken) {
|
|
156
|
+
throw new AgentStorageError(
|
|
157
|
+
"execution_lost",
|
|
158
|
+
`[SixbPg] Execution token is no longer current on agent run '${input.id}'.`
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const [row] = await tx<AgentRunRow[]>`
|
|
163
|
+
UPDATE agent_runs
|
|
164
|
+
SET execution_queue_lease_expires_at = GREATEST(
|
|
165
|
+
execution_queue_lease_expires_at,
|
|
166
|
+
${input.queueLeaseExpiresAt}
|
|
167
|
+
)
|
|
168
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
169
|
+
RETURNING *
|
|
170
|
+
`
|
|
171
|
+
return rowToRunRecord(row)
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async finish(input: FinishAgentRunInput): Promise<AgentRunRecord> {
|
|
176
|
+
const completedAt = input.completedAt ?? new Date()
|
|
177
|
+
const errorValue = input.status === "succeeded" ? null : (input.error ?? null)
|
|
178
|
+
|
|
179
|
+
return runPgTransaction(this.sql, async (tx) => {
|
|
180
|
+
const run = await this.lockRunning(tx, input.projectId, input.id)
|
|
181
|
+
|
|
182
|
+
if (run.execution_token !== input.executionToken) {
|
|
183
|
+
throw new AgentStorageError(
|
|
184
|
+
"execution_lost",
|
|
185
|
+
`[SixbPg] Execution token is no longer current on agent run '${input.id}'.`
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const [row] = await tx<AgentRunRow[]>`
|
|
190
|
+
UPDATE agent_runs
|
|
191
|
+
SET
|
|
192
|
+
status = ${input.status},
|
|
193
|
+
model_id = COALESCE(${input.modelId ?? null}, model_id),
|
|
194
|
+
finish_reason = ${input.finishReason ?? null},
|
|
195
|
+
usage_input_tokens = ${input.usage?.inputTokens ?? null},
|
|
196
|
+
usage_output_tokens = ${input.usage?.outputTokens ?? null},
|
|
197
|
+
usage_total_tokens = ${input.usage?.totalTokens ?? null},
|
|
198
|
+
usage_reasoning_tokens = ${input.usage?.reasoningTokens ?? null},
|
|
199
|
+
usage_cached_input_tokens = ${input.usage?.cachedInputTokens ?? null},
|
|
200
|
+
error = ${errorValue},
|
|
201
|
+
diagnostics = ${input.diagnostics === undefined ? null : JSON.stringify(input.diagnostics)}::text::jsonb,
|
|
202
|
+
execution_token = ${null},
|
|
203
|
+
execution_queue_lease_expires_at = ${null},
|
|
204
|
+
completed_at = ${completedAt}
|
|
205
|
+
WHERE project_id = ${input.projectId} AND id = ${input.id}
|
|
206
|
+
RETURNING *
|
|
207
|
+
`
|
|
208
|
+
|
|
209
|
+
await this.releaseThread(tx, run, completedAt)
|
|
210
|
+
|
|
211
|
+
return rowToRunRecord(row)
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async getById(params: { projectId: string; id: string }): Promise<AgentRunRecord | null> {
|
|
216
|
+
const [row] = await this.sql<AgentRunRow[]>`
|
|
217
|
+
SELECT * FROM agent_runs WHERE project_id = ${params.projectId} AND id = ${params.id}
|
|
218
|
+
`
|
|
219
|
+
|
|
220
|
+
return row ? rowToRunRecord(row) : null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async getByIds(params: {
|
|
224
|
+
projectId: string
|
|
225
|
+
ids: readonly string[]
|
|
226
|
+
}): Promise<readonly AgentRunRecord[]> {
|
|
227
|
+
const ids = [...new Set(params.ids)]
|
|
228
|
+
if (ids.length === 0) {
|
|
229
|
+
return []
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const rows: AgentRunRow[] = []
|
|
233
|
+
for (let offset = 0; offset < ids.length; offset += PG_RUN_ID_BATCH_SIZE) {
|
|
234
|
+
const batch = ids.slice(offset, offset + PG_RUN_ID_BATCH_SIZE)
|
|
235
|
+
const placeholders = batch.map((_, index) => `$${index + 2}`).join(", ")
|
|
236
|
+
rows.push(
|
|
237
|
+
...(await this.sql.unsafe<AgentRunRow[]>(
|
|
238
|
+
`SELECT * FROM agent_runs WHERE project_id = $1 AND id IN (${placeholders})`,
|
|
239
|
+
[params.projectId, ...batch] as SqlParameter[]
|
|
240
|
+
))
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
const byId = new Map(rows.map((row) => [row.id, rowToRunRecord(row)]))
|
|
244
|
+
return ids.flatMap((id) => {
|
|
245
|
+
const run = byId.get(id)
|
|
246
|
+
return run ? [run] : []
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async list(input: ListAgentRunsInput): Promise<ListAgentRunsResult> {
|
|
251
|
+
if (hasEmptyStatuses(input)) {
|
|
252
|
+
return { runs: [], hasMore: false, total: 0 }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const whereClauses = ["project_id = $1"]
|
|
256
|
+
const params: SqlParameter[] = [input.projectId]
|
|
257
|
+
let index = 2
|
|
258
|
+
|
|
259
|
+
if (input.threadId) {
|
|
260
|
+
whereClauses.push(`thread_id = $${index++}`)
|
|
261
|
+
params.push(input.threadId)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (input.agentId) {
|
|
265
|
+
whereClauses.push(`agent_id = $${index++}`)
|
|
266
|
+
params.push(input.agentId)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
index = appendRunListFilters(
|
|
270
|
+
whereClauses,
|
|
271
|
+
params,
|
|
272
|
+
index,
|
|
273
|
+
input,
|
|
274
|
+
"COALESCE(started_at, created_at)"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const { rows, total, hasMore } = await queryRunList<AgentRunRow>({
|
|
278
|
+
sql: this.sql,
|
|
279
|
+
tableName: "agent_runs",
|
|
280
|
+
whereClauses,
|
|
281
|
+
params,
|
|
282
|
+
nextIndex: index,
|
|
283
|
+
order: input.order,
|
|
284
|
+
limit: input.limit,
|
|
285
|
+
offset: input.offset,
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
return { runs: rows.map(rowToRunRecord), hasMore, total }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private lockRunning(sql: SQLClient, projectId: string, id: string): Promise<AgentRunRow> {
|
|
292
|
+
return this.lockStatus(sql, projectId, id, "running")
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async lockStatus(
|
|
296
|
+
sql: SQLClient,
|
|
297
|
+
projectId: string,
|
|
298
|
+
id: string,
|
|
299
|
+
status: AgentRunRecord["status"]
|
|
300
|
+
): Promise<AgentRunRow> {
|
|
301
|
+
const [row] = await sql<AgentRunRow[]>`
|
|
302
|
+
SELECT * FROM agent_runs
|
|
303
|
+
WHERE project_id = ${projectId} AND id = ${id}
|
|
304
|
+
FOR UPDATE
|
|
305
|
+
`
|
|
306
|
+
|
|
307
|
+
if (!row) {
|
|
308
|
+
throw new AgentStorageError(
|
|
309
|
+
"run_not_found",
|
|
310
|
+
`[SixbPg] Agent run '${id}' not found for project '${projectId}'.`
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (row.status !== status) {
|
|
315
|
+
throw new AgentStorageError(
|
|
316
|
+
"invalid_state",
|
|
317
|
+
`[SixbPg] Agent run '${id}' is not ${status} (status '${row.status}').`
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return row
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private async releaseThread(sql: SQLClient, run: AgentRunRow, completedAt: Date): Promise<void> {
|
|
325
|
+
await sql`
|
|
326
|
+
UPDATE agent_threads SET active_run_id = ${null}, updated_at = ${completedAt}
|
|
327
|
+
WHERE project_id = ${run.project_id} AND id = ${run.thread_id} AND active_run_id = ${run.id}
|
|
328
|
+
`
|
|
329
|
+
}
|
|
330
|
+
}
|