@sixb/pg 0.1.11 → 0.1.12

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 (46) hide show
  1. package/README.md +34 -0
  2. package/dist/agents/rows.d.ts +3 -1
  3. package/dist/agents/rows.d.ts.map +1 -1
  4. package/dist/agents/threads.d.ts +3 -2
  5. package/dist/agents/threads.d.ts.map +1 -1
  6. package/dist/index.js +553 -54
  7. package/dist/index.js.map +16 -14
  8. package/dist/migrations.d.ts.map +1 -1
  9. package/dist/objects/query-compiler.d.ts +8 -1
  10. package/dist/objects/query-compiler.d.ts.map +1 -1
  11. package/dist/objects/read-scope.d.ts +1 -0
  12. package/dist/objects/read-scope.d.ts.map +1 -1
  13. package/dist/objects/reader.d.ts +1 -0
  14. package/dist/objects/reader.d.ts.map +1 -1
  15. package/dist/objects/rows.d.ts +1 -0
  16. package/dist/objects/rows.d.ts.map +1 -1
  17. package/dist/objects/storage.d.ts +1 -1
  18. package/dist/objects/storage.d.ts.map +1 -1
  19. package/dist/ontology-storage/index.d.ts +4 -0
  20. package/dist/ontology-storage/index.d.ts.map +1 -1
  21. package/dist/ontology-storage/materializations.d.ts +1 -0
  22. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  23. package/dist/ontology-storage/vector-indexing.d.ts +20 -0
  24. package/dist/ontology-storage/vector-indexing.d.ts.map +1 -0
  25. package/dist/ontology-storage/vectors.d.ts +16 -0
  26. package/dist/ontology-storage/vectors.d.ts.map +1 -0
  27. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  28. package/package.json +3 -3
  29. package/src/agents/rows.ts +21 -0
  30. package/src/agents/threads.ts +47 -2
  31. package/src/migrations/041-agent-thread-workspaces.sql +2 -0
  32. package/src/migrations/042-vector-profiles.sql +28 -0
  33. package/src/migrations/043-vector-indexing.sql +24 -0
  34. package/src/migrations/044-vector-batching.sql +3 -0
  35. package/src/migrations/045-agent-thread-sandbox-state.sql +2 -0
  36. package/src/migrations.ts +14 -0
  37. package/src/objects/query-compiler.ts +102 -11
  38. package/src/objects/read-scope.ts +26 -25
  39. package/src/objects/reader.ts +33 -1
  40. package/src/objects/rows.ts +5 -1
  41. package/src/objects/storage.ts +20 -2
  42. package/src/ontology-storage/index.ts +15 -0
  43. package/src/ontology-storage/materializations.ts +13 -0
  44. package/src/ontology-storage/vector-indexing.ts +168 -0
  45. package/src/ontology-storage/vectors.ts +172 -0
  46. package/src/pg-ai-cost-storage.ts +4 -0
@@ -0,0 +1,172 @@
1
+ import { MaterializationConflictError } from "@sixb/core/internal/materialization"
2
+ import type {
3
+ MaterializationSession,
4
+ ObjectVectorState,
5
+ OntologyVectorStorage,
6
+ } from "@sixb/core/storage"
7
+ import type { SQLClient } from "../pg-client"
8
+ import { jsonParameter as json, type PgRootOperation } from "./shared"
9
+
10
+ interface VectorMetadataRow {
11
+ readonly profile: string
12
+ readonly configuration: string
13
+ readonly source: string[]
14
+ readonly source_fingerprint: string
15
+ readonly last_commit_id: string
16
+ }
17
+
18
+ export class PgOntologyVectorStorage implements OntologyVectorStorage {
19
+ constructor(
20
+ private readonly sql: SQLClient,
21
+ private readonly runRootOperation: PgRootOperation,
22
+ private readonly assertSession: (
23
+ session: MaterializationSession,
24
+ projectId: string,
25
+ commitId?: string
26
+ ) => void
27
+ ) {}
28
+
29
+ async list(
30
+ input: Parameters<OntologyVectorStorage["list"]>[0]
31
+ ): Promise<readonly ObjectVectorState[]> {
32
+ return this.runRootOperation(async (sql) => {
33
+ const rows = await sql<VectorMetadataRow[]>`
34
+ SELECT profile, configuration, source, source_fingerprint, last_commit_id
35
+ FROM object_vectors
36
+ WHERE project_id = ${input.projectId}
37
+ AND object_type_id = ${input.ref.objectTypeId} AND primary_id = ${input.ref.primaryId}
38
+ ORDER BY profile
39
+ `
40
+ return rows.map((row) => ({
41
+ ref: { ...input.ref },
42
+ profile: row.profile,
43
+ configuration: row.configuration,
44
+ source: row.source,
45
+ sourceFingerprint: row.source_fingerprint,
46
+ lastCommitId: row.last_commit_id,
47
+ }))
48
+ })
49
+ }
50
+
51
+ async listBatch(
52
+ input: Parameters<OntologyVectorStorage["listBatch"]>[0]
53
+ ): Promise<readonly ObjectVectorState[]> {
54
+ if (!input.refs.length) return []
55
+ return this.runRootOperation(async (sql) => {
56
+ const rows = await sql<
57
+ (VectorMetadataRow & { object_type_id: string; primary_id: string })[]
58
+ >`
59
+ SELECT stored.object_type_id, stored.primary_id, stored.profile, stored.configuration,
60
+ stored.source, stored.source_fingerprint, stored.last_commit_id
61
+ FROM (SELECT DISTINCT ref->>'objectTypeId' AS object_type_id, ref->>'primaryId' AS primary_id
62
+ FROM jsonb_array_elements(${json(sql, input.refs)}::jsonb) AS requested(ref)) AS requested
63
+ JOIN object_vectors AS stored ON stored.project_id = ${input.projectId}
64
+ AND stored.object_type_id = requested.object_type_id AND stored.primary_id = requested.primary_id
65
+ ORDER BY stored.object_type_id, stored.primary_id, stored.profile
66
+ `
67
+ return rows.map((row) => ({
68
+ ref: { objectTypeId: row.object_type_id, primaryId: row.primary_id },
69
+ profile: row.profile,
70
+ configuration: row.configuration,
71
+ source: row.source,
72
+ sourceFingerprint: row.source_fingerprint,
73
+ lastCommitId: row.last_commit_id,
74
+ }))
75
+ })
76
+ }
77
+
78
+ async removeBatch(input: Parameters<OntologyVectorStorage["removeBatch"]>[0]): Promise<void> {
79
+ this.assertSession(input.session, input.projectId)
80
+ if (!input.entries.length) return
81
+ const removed = await this.sql`
82
+ DELETE FROM object_vectors AS stored
83
+ USING jsonb_array_elements(${json(this.sql, input.entries)}::jsonb) AS requested(entry)
84
+ WHERE stored.project_id = ${input.projectId}
85
+ AND stored.object_type_id = entry#>>'{ref,objectTypeId}'
86
+ AND stored.primary_id = entry#>>'{ref,primaryId}' AND stored.profile = entry->>'profile'
87
+ AND stored.last_commit_id = entry->>'expectedCommitId'
88
+ RETURNING stored.profile
89
+ `
90
+ if (removed.length !== input.entries.length) throw vectorConflict()
91
+ }
92
+
93
+ async write(input: Parameters<OntologyVectorStorage["write"]>[0]): Promise<void> {
94
+ await this.writeBatch({ session: input.session, projectId: input.projectId, entries: [input] })
95
+ }
96
+
97
+ async writeBatch(input: Parameters<OntologyVectorStorage["writeBatch"]>[0]): Promise<void> {
98
+ const entries = input.entries.map(({ value, expectedCommitId }) => {
99
+ this.assertSession(input.session, input.projectId, value.lastCommitId)
100
+ return {
101
+ object_type_id: value.ref.objectTypeId,
102
+ primary_id: value.ref.primaryId,
103
+ profile: value.profile,
104
+ configuration: value.configuration,
105
+ source: [...value.source],
106
+ source_fingerprint: value.sourceFingerprint,
107
+ embedding: [...value.values],
108
+ last_commit_id: value.lastCommitId,
109
+ expected_commit_id: expectedCommitId,
110
+ }
111
+ })
112
+ const inserts = entries.filter((entry) => entry.expected_commit_id === null)
113
+ const updates = entries.filter((entry) => entry.expected_commit_id !== null)
114
+
115
+ // Keep insert-if-absent and revision-checked update distinct: an upsert could recreate
116
+ // a deleted vector or overwrite a representation produced after preparation.
117
+ if (inserts.length) {
118
+ const rows = await this.sql`
119
+ INSERT INTO object_vectors (
120
+ project_id, object_type_id, primary_id, profile, configuration,
121
+ source, source_fingerprint, embedding, last_commit_id
122
+ )
123
+ SELECT ${input.projectId}, object_type_id, primary_id, profile, configuration,
124
+ source, source_fingerprint, embedding, last_commit_id
125
+ FROM jsonb_to_recordset(${json(this.sql, inserts)}::jsonb) AS incoming(
126
+ object_type_id text, primary_id text, profile text, configuration text,
127
+ source text[], source_fingerprint text, embedding real[], last_commit_id text
128
+ )
129
+ ORDER BY object_type_id, primary_id, profile
130
+ ON CONFLICT (project_id, object_type_id, primary_id, profile) DO NOTHING
131
+ RETURNING profile`
132
+ if (rows.length !== inserts.length) throw vectorConflict()
133
+ }
134
+
135
+ if (updates.length) {
136
+ const rows = await this.sql`
137
+ UPDATE object_vectors AS stored SET
138
+ configuration = incoming.configuration, source = incoming.source,
139
+ source_fingerprint = incoming.source_fingerprint, embedding = incoming.embedding,
140
+ last_commit_id = incoming.last_commit_id
141
+ FROM jsonb_to_recordset(${json(this.sql, updates)}::jsonb) AS incoming(
142
+ object_type_id text, primary_id text, profile text, configuration text,
143
+ source text[], source_fingerprint text, embedding real[], last_commit_id text,
144
+ expected_commit_id text
145
+ )
146
+ WHERE stored.project_id = ${input.projectId}
147
+ AND stored.object_type_id = incoming.object_type_id AND stored.primary_id = incoming.primary_id
148
+ AND stored.profile = incoming.profile AND stored.last_commit_id = incoming.expected_commit_id
149
+ RETURNING stored.profile`
150
+ if (rows.length !== updates.length) throw vectorConflict()
151
+ }
152
+ }
153
+
154
+ async remove(input: Parameters<OntologyVectorStorage["remove"]>[0]): Promise<void> {
155
+ this.assertSession(input.session, input.projectId)
156
+ const rows = await this.sql`
157
+ DELETE FROM object_vectors
158
+ WHERE project_id = ${input.projectId}
159
+ AND object_type_id = ${input.ref.objectTypeId} AND primary_id = ${input.ref.primaryId}
160
+ AND profile = ${input.profile} AND last_commit_id = ${input.expectedCommitId}
161
+ RETURNING profile
162
+ `
163
+ if (rows.length !== 1) throw vectorConflict()
164
+ }
165
+ }
166
+
167
+ function vectorConflict(): MaterializationConflictError {
168
+ return new MaterializationConflictError(
169
+ "effective-state",
170
+ "Vector changed since it was prepared."
171
+ )
172
+ }
@@ -188,6 +188,8 @@ export class PgAiCostStorage implements AiCostStorage {
188
188
  COUNT(*)::text AS model_call_count,
189
189
  COUNT(*) FILTER (WHERE reporting_status <> 'unavailable')::text
190
190
  AS reported_usage_call_count,
191
+ COUNT(*) FILTER (WHERE input_tokens IS NOT NULL AND output_tokens IS NOT NULL)::text
192
+ AS complete_usage_call_count,
191
193
  COUNT(input_tokens)::text AS input_tokens_count,
192
194
  SUM(input_tokens)::text AS input_tokens_sum,
193
195
  COUNT(output_tokens)::text AS output_tokens_count,
@@ -379,6 +381,7 @@ interface AggregateRow {
379
381
  readonly amount_currency: string | null
380
382
  readonly model_call_count: string
381
383
  readonly reported_usage_call_count: string
384
+ readonly complete_usage_call_count: string
382
385
  readonly input_tokens_count: string
383
386
  readonly input_tokens_sum: string | null
384
387
  readonly output_tokens_count: string
@@ -455,6 +458,7 @@ function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragm
455
458
  modelCallCount: row.model_call_count,
456
459
  usage: {
457
460
  reportedCallCount: row.reported_usage_call_count,
461
+ completeCallCount: row.complete_usage_call_count,
458
462
  inputTokens: meterFragment(row.input_tokens_count, row.input_tokens_sum),
459
463
  outputTokens: meterFragment(row.output_tokens_count, row.output_tokens_sum),
460
464
  uncachedInputTokens: meterFragment(