@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
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACTIVITY_MAX_VISIT_MS,
|
|
3
|
+
type ActivitySourceState,
|
|
4
|
+
activityActor,
|
|
5
|
+
activityCalendarDays,
|
|
6
|
+
parseActivityAttribution,
|
|
7
|
+
planActivityAppend,
|
|
8
|
+
type StoredActivityEvent,
|
|
9
|
+
serializeActivityAttribution,
|
|
10
|
+
validateActivityAppend,
|
|
11
|
+
} from "@sixb/core/internal/storage"
|
|
12
|
+
import type {
|
|
13
|
+
ActivityCalendarInput,
|
|
14
|
+
ActivityCursor,
|
|
15
|
+
ActivityDay,
|
|
16
|
+
ActivityInteraction,
|
|
17
|
+
ActivityPageView,
|
|
18
|
+
ActivityPeriodInput,
|
|
19
|
+
ActivitySessionVisits,
|
|
20
|
+
ActivityStorage,
|
|
21
|
+
ActivitySummary,
|
|
22
|
+
ActivityUsersPage,
|
|
23
|
+
ActivityVisit,
|
|
24
|
+
ActivityVisitsPage,
|
|
25
|
+
AppendActivityInput,
|
|
26
|
+
AppendActivityResult,
|
|
27
|
+
ListActivitySessionVisitsInput,
|
|
28
|
+
ListActivityUsersInput,
|
|
29
|
+
ListActivityVisitsInput,
|
|
30
|
+
ReadActivityVisitInput,
|
|
31
|
+
StoredActivityVisitPage,
|
|
32
|
+
} from "@sixb/core/storage"
|
|
33
|
+
import type { SQLClient } from "./pg-client"
|
|
34
|
+
import { lockAdvisoryKeys, type PgStoreClient, runPgTransaction } from "./transactions"
|
|
35
|
+
|
|
36
|
+
// Sorts after every stored timestamp, so a first page starts from the newest row.
|
|
37
|
+
const NO_CURSOR = { at: new Date("9999-12-31T23:59:59.999Z"), id: "" }
|
|
38
|
+
|
|
39
|
+
/** PostgreSQL activity. An advisory lock per session serializes its batches across replicas. */
|
|
40
|
+
export class PgActivityStorage implements ActivityStorage {
|
|
41
|
+
constructor(private readonly sql: PgStoreClient) {}
|
|
42
|
+
|
|
43
|
+
async append(input: AppendActivityInput): Promise<AppendActivityResult> {
|
|
44
|
+
validateActivityAppend(input)
|
|
45
|
+
|
|
46
|
+
return runPgTransaction(this.sql, async (sql) => {
|
|
47
|
+
await lockAdvisoryKeys(sql, [
|
|
48
|
+
JSON.stringify(["activity", input.projectId, input.actor.type, input.sessionId]),
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
const plan = planActivityAppend(
|
|
52
|
+
input,
|
|
53
|
+
await readSource(sql, input),
|
|
54
|
+
await readStoredEvents(sql, input)
|
|
55
|
+
)
|
|
56
|
+
if (plan.status !== "append") {
|
|
57
|
+
return { status: plan.status === "duplicate" ? "accepted" : "throttled", accepted: 0 }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { state } = plan
|
|
61
|
+
const lastSeenAt = new Date(state.lastSeenAt)
|
|
62
|
+
|
|
63
|
+
if (plan.newVisit) {
|
|
64
|
+
const attribution =
|
|
65
|
+
plan.newVisit.attribution === null
|
|
66
|
+
? null
|
|
67
|
+
: serializeActivityAttribution(plan.newVisit.attribution)
|
|
68
|
+
|
|
69
|
+
await sql`
|
|
70
|
+
INSERT INTO activity_visits (
|
|
71
|
+
project_id, id, actor_type, user_id, grant_id, session_id,
|
|
72
|
+
started_at, last_seen_at, page_views, interactions, attribution
|
|
73
|
+
) VALUES (
|
|
74
|
+
${input.projectId}, ${state.visitId}, ${input.actor.type},
|
|
75
|
+
${input.actor.type === "user" ? input.actor.userId : null},
|
|
76
|
+
${input.actor.type === "share" ? input.actor.grantId : null},
|
|
77
|
+
${input.sessionId}, ${lastSeenAt}, ${lastSeenAt}, ${plan.pageViews.length},
|
|
78
|
+
${plan.interactions.length}, ${attribution}::text::jsonb
|
|
79
|
+
)
|
|
80
|
+
`
|
|
81
|
+
} else {
|
|
82
|
+
await sql`
|
|
83
|
+
UPDATE activity_visits
|
|
84
|
+
SET
|
|
85
|
+
last_seen_at = ${lastSeenAt},
|
|
86
|
+
page_views = page_views + ${plan.pageViews.length},
|
|
87
|
+
interactions = interactions + ${plan.interactions.length}
|
|
88
|
+
WHERE project_id = ${input.projectId} AND id = ${state.visitId}
|
|
89
|
+
`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const pageView of plan.pageViews) {
|
|
93
|
+
await sql`
|
|
94
|
+
INSERT INTO activity_page_views (
|
|
95
|
+
project_id, visit_id, sequence, id, route, received_at, attribution
|
|
96
|
+
) VALUES (
|
|
97
|
+
${input.projectId}, ${pageView.visitId}, ${pageView.sequence}, ${pageView.id},
|
|
98
|
+
${pageView.route}, ${pageView.receivedAt},
|
|
99
|
+
${serializeActivityAttribution(pageView.attribution)}::text::jsonb
|
|
100
|
+
)
|
|
101
|
+
`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const interaction of plan.interactions) {
|
|
105
|
+
await sql`
|
|
106
|
+
INSERT INTO activity_interactions (
|
|
107
|
+
project_id, visit_id, sequence, id, name, route, received_at
|
|
108
|
+
) VALUES (
|
|
109
|
+
${input.projectId}, ${interaction.visitId}, ${interaction.sequence},
|
|
110
|
+
${interaction.id}, ${interaction.name}, ${interaction.route}, ${interaction.receivedAt}
|
|
111
|
+
)
|
|
112
|
+
`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await sql`
|
|
116
|
+
INSERT INTO activity_sources (
|
|
117
|
+
project_id, actor_type, session_id, visit_id, last_seen_at, sequence,
|
|
118
|
+
window_start, window_count
|
|
119
|
+
) VALUES (
|
|
120
|
+
${input.projectId}, ${input.actor.type}, ${input.sessionId}, ${state.visitId},
|
|
121
|
+
${lastSeenAt}, ${state.sequence}, ${new Date(state.windowStart)}, ${state.windowCount}
|
|
122
|
+
)
|
|
123
|
+
ON CONFLICT (project_id, actor_type, session_id) DO UPDATE SET
|
|
124
|
+
visit_id = excluded.visit_id,
|
|
125
|
+
last_seen_at = excluded.last_seen_at,
|
|
126
|
+
sequence = excluded.sequence,
|
|
127
|
+
window_start = excluded.window_start,
|
|
128
|
+
window_count = excluded.window_count
|
|
129
|
+
`
|
|
130
|
+
|
|
131
|
+
return { status: "accepted", accepted: plan.accepted }
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async list(input: ListActivityVisitsInput): Promise<ActivityVisitsPage> {
|
|
136
|
+
const before = input.before ?? NO_CURSOR
|
|
137
|
+
const rows = await this.sql<VisitRow[]>`
|
|
138
|
+
SELECT ${this.visitColumns()}
|
|
139
|
+
FROM activity_visits v
|
|
140
|
+
WHERE ${this.overlapping(input)}
|
|
141
|
+
AND (
|
|
142
|
+
v.last_seen_at < ${before.at}
|
|
143
|
+
OR (v.last_seen_at = ${before.at} AND v.id < ${before.id})
|
|
144
|
+
)
|
|
145
|
+
ORDER BY v.last_seen_at DESC, v.id DESC
|
|
146
|
+
LIMIT ${input.limit + 1}
|
|
147
|
+
`
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
visits: rows.slice(0, input.limit).map(visitFromRow),
|
|
151
|
+
hasMore: rows.length > input.limit,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async users(input: ListActivityUsersInput): Promise<ActivityUsersPage> {
|
|
156
|
+
const before: ActivityCursor = input.before ?? NO_CURSOR
|
|
157
|
+
const rows = await this.sql<UserRow[]>`
|
|
158
|
+
SELECT
|
|
159
|
+
v.user_id,
|
|
160
|
+
MAX(v.last_seen_at) AS last_seen_at,
|
|
161
|
+
COUNT(DISTINCT v.id) AS visits,
|
|
162
|
+
COUNT(p.id) AS page_views
|
|
163
|
+
FROM activity_visits v
|
|
164
|
+
LEFT JOIN activity_page_views p
|
|
165
|
+
ON p.project_id = v.project_id
|
|
166
|
+
AND p.visit_id = v.id
|
|
167
|
+
AND p.received_at >= ${input.from}
|
|
168
|
+
AND p.received_at < ${input.to}
|
|
169
|
+
WHERE ${this.overlapping(input)} AND v.actor_type = 'user'
|
|
170
|
+
GROUP BY v.user_id
|
|
171
|
+
HAVING MAX(v.last_seen_at) < ${before.at}
|
|
172
|
+
OR (MAX(v.last_seen_at) = ${before.at} AND v.user_id < ${before.id})
|
|
173
|
+
ORDER BY last_seen_at DESC, v.user_id DESC
|
|
174
|
+
LIMIT ${input.limit + 1}
|
|
175
|
+
`
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
users: rows.slice(0, input.limit).map((row) => ({
|
|
179
|
+
userId: row.user_id,
|
|
180
|
+
lastSeenAt: new Date(row.last_seen_at),
|
|
181
|
+
visits: Number(row.visits),
|
|
182
|
+
pageViews: Number(row.page_views),
|
|
183
|
+
})),
|
|
184
|
+
hasMore: rows.length > input.limit,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async summarize(input: ActivityPeriodInput): Promise<ActivitySummary> {
|
|
189
|
+
const [row] = await this.sql<SummaryRow[]>`
|
|
190
|
+
SELECT
|
|
191
|
+
COUNT(DISTINCT v.user_id) AS users,
|
|
192
|
+
COUNT(DISTINCT v.id) AS visits,
|
|
193
|
+
COUNT(DISTINCT v.id) FILTER (WHERE v.actor_type = 'share') AS shared_visits,
|
|
194
|
+
COUNT(p.id) AS page_views
|
|
195
|
+
FROM activity_visits v
|
|
196
|
+
LEFT JOIN activity_page_views p
|
|
197
|
+
ON p.project_id = v.project_id
|
|
198
|
+
AND p.visit_id = v.id
|
|
199
|
+
AND p.received_at >= ${input.from}
|
|
200
|
+
AND p.received_at < ${input.to}
|
|
201
|
+
WHERE ${this.overlapping(input)}
|
|
202
|
+
`
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
users: Number(row?.users ?? 0),
|
|
206
|
+
visits: Number(row?.visits ?? 0),
|
|
207
|
+
sharedVisits: Number(row?.shared_visits ?? 0),
|
|
208
|
+
pageViews: Number(row?.page_views ?? 0),
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async calendar(input: ActivityCalendarInput): Promise<readonly ActivityDay[]> {
|
|
213
|
+
const visits = await this.sql<{ started_at: Date | string; last_seen_at: Date | string }[]>`
|
|
214
|
+
SELECT v.started_at, v.last_seen_at
|
|
215
|
+
FROM activity_visits v
|
|
216
|
+
WHERE ${this.overlapping(input)}
|
|
217
|
+
`
|
|
218
|
+
const days = await this.sql<{ day: string; page_views: string }[]>`
|
|
219
|
+
SELECT
|
|
220
|
+
to_char(p.received_at AT TIME ZONE 'UTC', 'YYYY-MM-DD') AS day,
|
|
221
|
+
COUNT(*) AS page_views
|
|
222
|
+
FROM activity_visits v
|
|
223
|
+
JOIN activity_page_views p ON p.project_id = v.project_id AND p.visit_id = v.id
|
|
224
|
+
WHERE ${this.overlapping(input)}
|
|
225
|
+
AND p.received_at >= ${input.from}
|
|
226
|
+
AND p.received_at < ${input.to}
|
|
227
|
+
GROUP BY day
|
|
228
|
+
`
|
|
229
|
+
|
|
230
|
+
return activityCalendarDays(
|
|
231
|
+
input,
|
|
232
|
+
visits.map((visit) => ({
|
|
233
|
+
startedAt: new Date(visit.started_at),
|
|
234
|
+
lastSeenAt: new Date(visit.last_seen_at),
|
|
235
|
+
})),
|
|
236
|
+
new Map(days.map((row) => [row.day, Number(row.page_views)]))
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async read(input: ReadActivityVisitInput): Promise<StoredActivityVisitPage | null> {
|
|
241
|
+
const [visit] = await this.sql<(VisitRow & { readonly session_id: string })[]>`
|
|
242
|
+
SELECT ${this.visitColumns()}, v.session_id
|
|
243
|
+
FROM activity_visits v
|
|
244
|
+
WHERE v.project_id = ${input.projectId} AND v.id = ${input.id}
|
|
245
|
+
`
|
|
246
|
+
if (!visit) {
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Page views and interactions share the visit's sequence, so one bound covers both.
|
|
251
|
+
const rows = await this.sql<EventRow[]>`
|
|
252
|
+
SELECT id, visit_id, sequence, route, NULL::text AS name, received_at
|
|
253
|
+
FROM activity_page_views
|
|
254
|
+
WHERE project_id = ${input.projectId}
|
|
255
|
+
AND visit_id = ${input.id}
|
|
256
|
+
UNION ALL
|
|
257
|
+
SELECT id, visit_id, sequence, route, name, received_at
|
|
258
|
+
FROM activity_interactions
|
|
259
|
+
WHERE project_id = ${input.projectId}
|
|
260
|
+
AND visit_id = ${input.id}
|
|
261
|
+
ORDER BY sequence
|
|
262
|
+
LIMIT ${input.limit + 1}
|
|
263
|
+
`
|
|
264
|
+
|
|
265
|
+
const pageViews: ActivityPageView[] = []
|
|
266
|
+
const interactions: ActivityInteraction[] = []
|
|
267
|
+
for (const row of rows.slice(0, input.limit)) {
|
|
268
|
+
if (row.name === null) {
|
|
269
|
+
pageViews.push(pageViewFromRow(row))
|
|
270
|
+
} else {
|
|
271
|
+
interactions.push({ ...pageViewFromRow(row), name: row.name })
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
visit: visitFromRow(visit),
|
|
277
|
+
sessionId: visit.session_id,
|
|
278
|
+
pageViews,
|
|
279
|
+
interactions,
|
|
280
|
+
hasMore: rows.length > input.limit,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async sessionVisits(
|
|
285
|
+
input: ListActivitySessionVisitsInput
|
|
286
|
+
): Promise<readonly ActivitySessionVisits[]> {
|
|
287
|
+
if (input.sessionIds.length === 0) {
|
|
288
|
+
return []
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// A session opens a visit per return after inactivity, so each has a few to rank.
|
|
292
|
+
const rows = await this.sql<SessionVisitRow[]>`
|
|
293
|
+
SELECT *
|
|
294
|
+
FROM (
|
|
295
|
+
SELECT
|
|
296
|
+
${this.visitColumns()},
|
|
297
|
+
v.session_id,
|
|
298
|
+
ROW_NUMBER() OVER (
|
|
299
|
+
PARTITION BY v.session_id ORDER BY v.started_at DESC, v.id DESC
|
|
300
|
+
) AS position,
|
|
301
|
+
COUNT(*) OVER (PARTITION BY v.session_id) AS total
|
|
302
|
+
FROM activity_visits v
|
|
303
|
+
WHERE v.project_id = ${input.projectId}
|
|
304
|
+
AND v.actor_type = 'user'
|
|
305
|
+
AND v.session_id = ANY(${this.sql.array([...input.sessionIds])}::text[])
|
|
306
|
+
AND v.user_id = ${input.userId}
|
|
307
|
+
) ranked
|
|
308
|
+
WHERE position <= ${input.limit}
|
|
309
|
+
ORDER BY session_id, position
|
|
310
|
+
`
|
|
311
|
+
|
|
312
|
+
const bySession = new Map<string, { visits: number; latest: ActivityVisit[] }>()
|
|
313
|
+
for (const row of rows) {
|
|
314
|
+
const session = bySession.get(row.session_id) ?? { visits: Number(row.total), latest: [] }
|
|
315
|
+
session.latest.push(visitFromRow(row))
|
|
316
|
+
bySession.set(row.session_id, session)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return [...bySession].map(([sessionId, session]) => ({ sessionId, ...session }))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The visits `v` active in the period, and only the account's when one is asked for. A visit
|
|
324
|
+
* lasts less than a day, so it started at most a day before the period: that bounds the index
|
|
325
|
+
* range.
|
|
326
|
+
*/
|
|
327
|
+
private overlapping(input: ActivityPeriodInput | ActivityCalendarInput) {
|
|
328
|
+
const user = input.userId === undefined ? this.sql`` : this.sql`AND v.user_id = ${input.userId}`
|
|
329
|
+
const earliestStart = new Date(input.from.getTime() - ACTIVITY_MAX_VISIT_MS)
|
|
330
|
+
|
|
331
|
+
return this.sql`
|
|
332
|
+
v.project_id = ${input.projectId}
|
|
333
|
+
AND v.started_at >= ${earliestStart}
|
|
334
|
+
AND v.started_at < ${input.to}
|
|
335
|
+
AND v.last_seen_at >= ${input.from}
|
|
336
|
+
${user}
|
|
337
|
+
`
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private visitColumns() {
|
|
341
|
+
return this.sql`
|
|
342
|
+
v.id, v.actor_type, v.user_id, v.grant_id, v.started_at, v.last_seen_at, v.page_views,
|
|
343
|
+
v.interactions, v.attribution::text AS attribution
|
|
344
|
+
`
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function readSource(
|
|
349
|
+
sql: SQLClient,
|
|
350
|
+
input: AppendActivityInput
|
|
351
|
+
): Promise<ActivitySourceState | undefined> {
|
|
352
|
+
const [row] = await sql<SourceRow[]>`
|
|
353
|
+
SELECT
|
|
354
|
+
s.visit_id, v.started_at AS visit_started_at, s.last_seen_at, s.sequence,
|
|
355
|
+
s.window_start, s.window_count
|
|
356
|
+
FROM activity_sources s
|
|
357
|
+
LEFT JOIN activity_visits v ON v.project_id = s.project_id AND v.id = s.visit_id
|
|
358
|
+
WHERE s.project_id = ${input.projectId}
|
|
359
|
+
AND s.actor_type = ${input.actor.type}
|
|
360
|
+
AND s.session_id = ${input.sessionId}
|
|
361
|
+
`
|
|
362
|
+
if (!row) {
|
|
363
|
+
return undefined
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// A deleted visit leaves its source behind; the next batch starts another visit.
|
|
367
|
+
return {
|
|
368
|
+
visitId: row.visit_id,
|
|
369
|
+
visitStartedAt: row.visit_started_at === null ? null : new Date(row.visit_started_at).getTime(),
|
|
370
|
+
lastSeenAt: new Date(row.last_seen_at).getTime(),
|
|
371
|
+
sequence: Number(row.sequence),
|
|
372
|
+
windowStart: new Date(row.window_start).getTime(),
|
|
373
|
+
windowCount: Number(row.window_count),
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** The session's page views and interactions with an id of the batch, to recognize retries. */
|
|
378
|
+
async function readStoredEvents(
|
|
379
|
+
sql: SQLClient,
|
|
380
|
+
input: AppendActivityInput
|
|
381
|
+
): Promise<StoredActivityEvent[]> {
|
|
382
|
+
const ids = input.events.map((event) => event.id)
|
|
383
|
+
const rows = await sql<
|
|
384
|
+
{ id: string; route: string; name: string | null; attribution: string | null }[]
|
|
385
|
+
>`
|
|
386
|
+
SELECT p.id, p.route, NULL::text AS name, p.attribution::text AS attribution
|
|
387
|
+
FROM activity_page_views p
|
|
388
|
+
JOIN activity_visits v ON v.project_id = p.project_id AND v.id = p.visit_id
|
|
389
|
+
WHERE p.project_id = ${input.projectId}
|
|
390
|
+
AND p.id = ANY(${sql.array(ids)}::text[])
|
|
391
|
+
AND v.actor_type = ${input.actor.type}
|
|
392
|
+
AND v.session_id = ${input.sessionId}
|
|
393
|
+
UNION ALL
|
|
394
|
+
SELECT i.id, i.route, i.name, NULL::text AS attribution
|
|
395
|
+
FROM activity_interactions i
|
|
396
|
+
JOIN activity_visits v ON v.project_id = i.project_id AND v.id = i.visit_id
|
|
397
|
+
WHERE i.project_id = ${input.projectId}
|
|
398
|
+
AND i.id = ANY(${sql.array(ids)}::text[])
|
|
399
|
+
AND v.actor_type = ${input.actor.type}
|
|
400
|
+
AND v.session_id = ${input.sessionId}
|
|
401
|
+
`
|
|
402
|
+
|
|
403
|
+
return rows.map((row) => {
|
|
404
|
+
if (row.name !== null) {
|
|
405
|
+
return { id: row.id, route: row.route, name: row.name }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const attribution = parseActivityAttribution(row.attribution)
|
|
409
|
+
return { id: row.id, route: row.route, ...(attribution ? { attribution } : {}) }
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
interface SourceRow {
|
|
414
|
+
readonly visit_id: string
|
|
415
|
+
readonly visit_started_at: Date | string | null
|
|
416
|
+
readonly last_seen_at: Date | string
|
|
417
|
+
readonly sequence: number
|
|
418
|
+
readonly window_start: Date | string
|
|
419
|
+
readonly window_count: number
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
interface VisitRow {
|
|
423
|
+
readonly id: string
|
|
424
|
+
readonly actor_type: string
|
|
425
|
+
readonly user_id: string | null
|
|
426
|
+
readonly grant_id: string | null
|
|
427
|
+
readonly started_at: Date | string
|
|
428
|
+
readonly last_seen_at: Date | string
|
|
429
|
+
readonly page_views: number
|
|
430
|
+
readonly interactions: number
|
|
431
|
+
readonly attribution: string | null
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
interface SessionVisitRow extends VisitRow {
|
|
435
|
+
readonly session_id: string
|
|
436
|
+
readonly total: number | string
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
interface UserRow {
|
|
440
|
+
readonly user_id: string
|
|
441
|
+
readonly last_seen_at: Date | string
|
|
442
|
+
readonly visits: string
|
|
443
|
+
readonly page_views: string
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
interface SummaryRow {
|
|
447
|
+
readonly users: string
|
|
448
|
+
readonly visits: string
|
|
449
|
+
readonly shared_visits: string
|
|
450
|
+
readonly page_views: string
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
interface PageViewRow {
|
|
454
|
+
readonly id: string
|
|
455
|
+
readonly visit_id: string
|
|
456
|
+
readonly sequence: number
|
|
457
|
+
readonly route: string
|
|
458
|
+
readonly received_at: Date | string
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** A page view or, with a name, an interaction. */
|
|
462
|
+
interface EventRow extends PageViewRow {
|
|
463
|
+
readonly name: string | null
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function visitFromRow(row: VisitRow): ActivityVisit {
|
|
467
|
+
return {
|
|
468
|
+
id: row.id,
|
|
469
|
+
actor: activityActor(row.actor_type, row.user_id, row.grant_id),
|
|
470
|
+
attribution: parseActivityAttribution(row.attribution),
|
|
471
|
+
startedAt: new Date(row.started_at),
|
|
472
|
+
lastSeenAt: new Date(row.last_seen_at),
|
|
473
|
+
pageViews: Number(row.page_views),
|
|
474
|
+
interactions: Number(row.interactions),
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function pageViewFromRow(row: PageViewRow): ActivityPageView {
|
|
479
|
+
return {
|
|
480
|
+
id: row.id,
|
|
481
|
+
visitId: row.visit_id,
|
|
482
|
+
sequence: Number(row.sequence),
|
|
483
|
+
route: row.route,
|
|
484
|
+
receivedAt: new Date(row.received_at),
|
|
485
|
+
}
|
|
486
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AiAccountingAggregateFragment,
|
|
3
3
|
type AiAccountingRecordSetItem,
|
|
4
|
+
aiAccountingModelKind,
|
|
4
5
|
aiModelCallCostDetails,
|
|
5
6
|
aiModelCallCostMatchesUsage,
|
|
6
7
|
buildAiAccountingOverviewFromFragments,
|
|
@@ -103,6 +104,7 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
103
104
|
WITH filtered AS (
|
|
104
105
|
SELECT usage.provider_id,
|
|
105
106
|
usage.requested_model_id,
|
|
107
|
+
usage.model_kind,
|
|
106
108
|
usage.occurred_at,
|
|
107
109
|
usage.input_tokens,
|
|
108
110
|
usage.output_tokens,
|
|
@@ -112,6 +114,10 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
112
114
|
usage.text_output_tokens,
|
|
113
115
|
usage.reasoning_output_tokens,
|
|
114
116
|
usage.audio_duration_ms,
|
|
117
|
+
usage.image_output_tokens,
|
|
118
|
+
usage.audio_output_tokens,
|
|
119
|
+
usage.output_images,
|
|
120
|
+
usage.input_characters,
|
|
115
121
|
usage.reporting_status,
|
|
116
122
|
CASE
|
|
117
123
|
WHEN direct_agent.kind = 'conversation' THEN 'agent'
|
|
@@ -174,6 +180,8 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
174
180
|
UNION ALL
|
|
175
181
|
SELECT 'model', provider_id, requested_model_id, NULL, NULL, classified.* FROM classified
|
|
176
182
|
UNION ALL
|
|
183
|
+
SELECT 'kind', model_kind, NULL, NULL, NULL, classified.* FROM classified
|
|
184
|
+
UNION ALL
|
|
177
185
|
SELECT 'agent', attribution_agent_kind, attribution_workflow_id, attribution_agent_step_id, NULL, classified.*
|
|
178
186
|
FROM classified WHERE attribution_agent_kind IS NOT NULL
|
|
179
187
|
UNION ALL
|
|
@@ -206,7 +214,15 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
206
214
|
COUNT(reasoning_output_tokens)::text AS reasoning_output_tokens_count,
|
|
207
215
|
SUM(reasoning_output_tokens)::text AS reasoning_output_tokens_sum,
|
|
208
216
|
COUNT(audio_duration_ms)::text AS audio_duration_ms_count,
|
|
217
|
+
COUNT(image_output_tokens)::text AS image_output_tokens_count,
|
|
218
|
+
COUNT(audio_output_tokens)::text AS audio_output_tokens_count,
|
|
219
|
+
COUNT(output_images)::text AS output_images_count,
|
|
220
|
+
COUNT(input_characters)::text AS input_characters_count,
|
|
209
221
|
SUM(audio_duration_ms)::text AS audio_duration_ms_sum,
|
|
222
|
+
SUM(image_output_tokens)::text AS image_output_tokens_sum,
|
|
223
|
+
SUM(audio_output_tokens)::text AS audio_output_tokens_sum,
|
|
224
|
+
SUM(output_images)::text AS output_images_sum,
|
|
225
|
+
SUM(input_characters)::text AS input_characters_sum,
|
|
210
226
|
COUNT(*) FILTER (WHERE valuation_status = 'rated')::text AS rated_call_count,
|
|
211
227
|
COUNT(*) FILTER (WHERE valuation_status = 'unpriceable')::text
|
|
212
228
|
AS unpriceable_call_count,
|
|
@@ -347,6 +363,10 @@ interface UsageRow {
|
|
|
347
363
|
readonly text_output_tokens: number | string | null
|
|
348
364
|
readonly reasoning_output_tokens: number | string | null
|
|
349
365
|
readonly audio_duration_ms: number | string | null
|
|
366
|
+
readonly image_output_tokens: number | string | null
|
|
367
|
+
readonly audio_output_tokens: number | string | null
|
|
368
|
+
readonly output_images: number | string | null
|
|
369
|
+
readonly input_characters: number | string | null
|
|
350
370
|
readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
|
|
351
371
|
readonly raw_usage: ReadonlyJsonObject | string | null
|
|
352
372
|
readonly provider_ids: Record<string, unknown> | string | null
|
|
@@ -402,7 +422,15 @@ interface AggregateRow {
|
|
|
402
422
|
readonly reasoning_output_tokens_count: string
|
|
403
423
|
readonly reasoning_output_tokens_sum: string | null
|
|
404
424
|
readonly audio_duration_ms_count: string
|
|
425
|
+
readonly image_output_tokens_count: string
|
|
426
|
+
readonly audio_output_tokens_count: string
|
|
427
|
+
readonly output_images_count: string
|
|
428
|
+
readonly input_characters_count: string
|
|
405
429
|
readonly audio_duration_ms_sum: string | null
|
|
430
|
+
readonly image_output_tokens_sum: string | null
|
|
431
|
+
readonly audio_output_tokens_sum: string | null
|
|
432
|
+
readonly output_images_sum: string | null
|
|
433
|
+
readonly input_characters_sum: string | null
|
|
406
434
|
readonly rated_call_count: string
|
|
407
435
|
readonly unpriceable_call_count: string
|
|
408
436
|
readonly unvalued_call_count: string
|
|
@@ -447,18 +475,20 @@ function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragm
|
|
|
447
475
|
const dimensionKeys =
|
|
448
476
|
row.dimension === "model"
|
|
449
477
|
? { providerId: required(row.key_1), modelId: required(row.key_2) }
|
|
450
|
-
: row.dimension === "
|
|
451
|
-
?
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
: row.dimension === "
|
|
460
|
-
? {
|
|
461
|
-
:
|
|
478
|
+
: row.dimension === "kind"
|
|
479
|
+
? withModelKind(aiAccountingModelKind(row.key_1))
|
|
480
|
+
: row.dimension === "agent"
|
|
481
|
+
? {
|
|
482
|
+
agentKind: required(row.key_1) as "agent" | "workflowAgent",
|
|
483
|
+
...(row.key_1 === "workflowAgent"
|
|
484
|
+
? { workflowId: required(row.key_2), agentStepId: required(row.key_3) }
|
|
485
|
+
: {}),
|
|
486
|
+
}
|
|
487
|
+
: row.dimension === "workflow"
|
|
488
|
+
? { workflowId: required(row.key_1) }
|
|
489
|
+
: row.dimension === "series"
|
|
490
|
+
? { start: new Date(required(row.bucket_start)) }
|
|
491
|
+
: {}
|
|
462
492
|
return {
|
|
463
493
|
dimension: row.dimension,
|
|
464
494
|
...dimensionKeys,
|
|
@@ -486,6 +516,10 @@ function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragm
|
|
|
486
516
|
row.reasoning_output_tokens_sum
|
|
487
517
|
),
|
|
488
518
|
audioDurationMs: meterFragment(row.audio_duration_ms_count, row.audio_duration_ms_sum),
|
|
519
|
+
imageOutputTokens: meterFragment(row.image_output_tokens_count, row.image_output_tokens_sum),
|
|
520
|
+
audioOutputTokens: meterFragment(row.audio_output_tokens_count, row.audio_output_tokens_sum),
|
|
521
|
+
outputImages: meterFragment(row.output_images_count, row.output_images_sum),
|
|
522
|
+
inputCharacters: meterFragment(row.input_characters_count, row.input_characters_sum),
|
|
489
523
|
},
|
|
490
524
|
costs: {
|
|
491
525
|
...(row.amount_currency === null || row.amount_nanos === null
|
|
@@ -587,6 +621,18 @@ function usageFromRow(row: UsageRow, requesterGroupIds: readonly string[]): AiMo
|
|
|
587
621
|
...(number(row.audio_duration_ms) === undefined
|
|
588
622
|
? {}
|
|
589
623
|
: { audioDurationMs: number(row.audio_duration_ms) }),
|
|
624
|
+
...(number(row.image_output_tokens) === undefined
|
|
625
|
+
? {}
|
|
626
|
+
: { imageOutputTokens: number(row.image_output_tokens) }),
|
|
627
|
+
...(number(row.audio_output_tokens) === undefined
|
|
628
|
+
? {}
|
|
629
|
+
: { audioOutputTokens: number(row.audio_output_tokens) }),
|
|
630
|
+
...(number(row.output_images) === undefined
|
|
631
|
+
? {}
|
|
632
|
+
: { outputImages: number(row.output_images) }),
|
|
633
|
+
...(number(row.input_characters) === undefined
|
|
634
|
+
? {}
|
|
635
|
+
: { inputCharacters: number(row.input_characters) }),
|
|
590
636
|
reportingStatus: row.reporting_status,
|
|
591
637
|
},
|
|
592
638
|
...(row.raw_usage === null ? {} : { rawUsage: parseJsonObject(row.raw_usage) }),
|
|
@@ -777,3 +823,7 @@ function required<T>(value: T | null | undefined): T {
|
|
|
777
823
|
}
|
|
778
824
|
return value
|
|
779
825
|
}
|
|
826
|
+
|
|
827
|
+
function withModelKind(modelKind: AiAccountingAggregateFragment["modelKind"]) {
|
|
828
|
+
return modelKind === undefined ? {} : { modelKind }
|
|
829
|
+
}
|