@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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/agents/agent-storage.d.ts +20 -0
  4. package/dist/agents/agent-storage.d.ts.map +1 -0
  5. package/dist/agents/index.d.ts +6 -0
  6. package/dist/agents/index.d.ts.map +1 -0
  7. package/dist/agents/messages.d.ts +13 -0
  8. package/dist/agents/messages.d.ts.map +1 -0
  9. package/dist/agents/rows.d.ts +77 -0
  10. package/dist/agents/rows.d.ts.map +1 -0
  11. package/dist/agents/runs.d.ts +25 -0
  12. package/dist/agents/runs.d.ts.map +1 -0
  13. package/dist/agents/threads.d.ts +13 -0
  14. package/dist/agents/threads.d.ts.map +1 -0
  15. package/dist/auth-storage/access-tokens.d.ts +33 -0
  16. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  17. package/dist/auth-storage/auth-storage.d.ts +41 -0
  18. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  19. package/dist/auth-storage/group-memberships.d.ts +21 -0
  20. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  21. package/dist/auth-storage/identities.d.ts +17 -0
  22. package/dist/auth-storage/identities.d.ts.map +1 -0
  23. package/dist/auth-storage/index.d.ts +3 -0
  24. package/dist/auth-storage/index.d.ts.map +1 -0
  25. package/dist/auth-storage/invitations.d.ts +29 -0
  26. package/dist/auth-storage/invitations.d.ts.map +1 -0
  27. package/dist/auth-storage/magic-links.d.ts +28 -0
  28. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  29. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  30. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  31. package/dist/auth-storage/rows.d.ts +138 -0
  32. package/dist/auth-storage/rows.d.ts.map +1 -0
  33. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  34. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  35. package/dist/auth-storage/service-accounts.d.ts +14 -0
  36. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  37. package/dist/auth-storage/sessions.d.ts +55 -0
  38. package/dist/auth-storage/sessions.d.ts.map +1 -0
  39. package/dist/auth-storage/shared.d.ts +159 -0
  40. package/dist/auth-storage/shared.d.ts.map +1 -0
  41. package/dist/auth-storage/users.d.ts +19 -0
  42. package/dist/auth-storage/users.d.ts.map +1 -0
  43. package/dist/index.d.ts +127 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +11957 -0
  46. package/dist/index.js.map +55 -0
  47. package/dist/latest-run-query.d.ts +24 -0
  48. package/dist/latest-run-query.d.ts.map +1 -0
  49. package/dist/migrations.d.ts +20 -0
  50. package/dist/migrations.d.ts.map +1 -0
  51. package/dist/ontology-storage/commits.d.ts +11 -0
  52. package/dist/ontology-storage/commits.d.ts.map +1 -0
  53. package/dist/ontology-storage/index.d.ts +22 -0
  54. package/dist/ontology-storage/index.d.ts.map +1 -0
  55. package/dist/ontology-storage/materialization-session.d.ts +63 -0
  56. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  57. package/dist/ontology-storage/materialization-state.d.ts +69 -0
  58. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  59. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  60. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  61. package/dist/ontology-storage/materializations.d.ts +33 -0
  62. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  63. package/dist/ontology-storage/outbox.d.ts +12 -0
  64. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  65. package/dist/ontology-storage/shared.d.ts +95 -0
  66. package/dist/ontology-storage/shared.d.ts.map +1 -0
  67. package/dist/ontology-storage/sources.d.ts +25 -0
  68. package/dist/ontology-storage/sources.d.ts.map +1 -0
  69. package/dist/pg-action-run-storage.d.ts +21 -0
  70. package/dist/pg-action-run-storage.d.ts.map +1 -0
  71. package/dist/pg-client.d.ts +58 -0
  72. package/dist/pg-client.d.ts.map +1 -0
  73. package/dist/pg-object-query-compiler.d.ts +55 -0
  74. package/dist/pg-object-query-compiler.d.ts.map +1 -0
  75. package/dist/pg-object-storage.d.ts +90 -0
  76. package/dist/pg-object-storage.d.ts.map +1 -0
  77. package/dist/pg-pipeline-run-storage.d.ts +18 -0
  78. package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
  79. package/dist/pg-projection-run-storage.d.ts +19 -0
  80. package/dist/pg-projection-run-storage.d.ts.map +1 -0
  81. package/dist/pg-rules-storage.d.ts +25 -0
  82. package/dist/pg-rules-storage.d.ts.map +1 -0
  83. package/dist/pg-sync-run-storage.d.ts +15 -0
  84. package/dist/pg-sync-run-storage.d.ts.map +1 -0
  85. package/dist/pg-timeseries-storage.d.ts +32 -0
  86. package/dist/pg-timeseries-storage.d.ts.map +1 -0
  87. package/dist/pg-webhook-delivery-storage.d.ts +17 -0
  88. package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
  89. package/dist/pg-webhook-run-storage.d.ts +14 -0
  90. package/dist/pg-webhook-run-storage.d.ts.map +1 -0
  91. package/dist/pg-workflow-intervention-storage.d.ts +16 -0
  92. package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
  93. package/dist/pg-workflow-run-storage.d.ts +49 -0
  94. package/dist/pg-workflow-run-storage.d.ts.map +1 -0
  95. package/dist/run-list-query.d.ts +30 -0
  96. package/dist/run-list-query.d.ts.map +1 -0
  97. package/dist/storage-errors.d.ts +16 -0
  98. package/dist/storage-errors.d.ts.map +1 -0
  99. package/dist/transactions.d.ts +10 -0
  100. package/dist/transactions.d.ts.map +1 -0
  101. package/dist/tsconfig.json +5 -0
  102. package/package.json +60 -0
  103. package/src/agents/agent-storage.ts +26 -0
  104. package/src/agents/index.ts +5 -0
  105. package/src/agents/messages.ts +144 -0
  106. package/src/agents/rows.ts +227 -0
  107. package/src/agents/runs.ts +330 -0
  108. package/src/agents/threads.ts +116 -0
  109. package/src/auth-storage/access-tokens.ts +254 -0
  110. package/src/auth-storage/auth-storage.ts +798 -0
  111. package/src/auth-storage/group-memberships.ts +47 -0
  112. package/src/auth-storage/identities.ts +85 -0
  113. package/src/auth-storage/index.ts +2 -0
  114. package/src/auth-storage/invitations.ts +280 -0
  115. package/src/auth-storage/magic-links.ts +140 -0
  116. package/src/auth-storage/oidc-attempts.ts +93 -0
  117. package/src/auth-storage/rows.ts +352 -0
  118. package/src/auth-storage/service-account-group-memberships.ts +150 -0
  119. package/src/auth-storage/service-accounts.ts +154 -0
  120. package/src/auth-storage/sessions.ts +193 -0
  121. package/src/auth-storage/shared.ts +799 -0
  122. package/src/auth-storage/users.ts +196 -0
  123. package/src/index.ts +367 -0
  124. package/src/latest-run-query.ts +42 -0
  125. package/src/migrations/001-initial-schema.sql +1195 -0
  126. package/src/migrations.ts +280 -0
  127. package/src/ontology-storage/commits.ts +115 -0
  128. package/src/ontology-storage/index.ts +35 -0
  129. package/src/ontology-storage/materialization-session.ts +426 -0
  130. package/src/ontology-storage/materialization-state.ts +1109 -0
  131. package/src/ontology-storage/materialization-writer.ts +509 -0
  132. package/src/ontology-storage/materializations.ts +939 -0
  133. package/src/ontology-storage/outbox.ts +216 -0
  134. package/src/ontology-storage/shared.ts +293 -0
  135. package/src/ontology-storage/sources.ts +658 -0
  136. package/src/pg-action-run-storage.ts +657 -0
  137. package/src/pg-client.ts +124 -0
  138. package/src/pg-object-query-compiler.ts +1545 -0
  139. package/src/pg-object-storage.ts +638 -0
  140. package/src/pg-pipeline-run-storage.ts +457 -0
  141. package/src/pg-projection-run-storage.ts +526 -0
  142. package/src/pg-rules-storage.ts +308 -0
  143. package/src/pg-sync-run-storage.ts +263 -0
  144. package/src/pg-timeseries-storage.ts +180 -0
  145. package/src/pg-webhook-delivery-storage.ts +180 -0
  146. package/src/pg-webhook-run-storage.ts +185 -0
  147. package/src/pg-workflow-intervention-storage.ts +339 -0
  148. package/src/pg-workflow-run-storage.ts +1065 -0
  149. package/src/run-list-query.ts +108 -0
  150. package/src/sql.d.ts +4 -0
  151. package/src/storage-errors.ts +72 -0
  152. 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
+ }