@sixb/sqlite 0.1.26 → 0.2.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/dist/agents/agent-storage.d.ts +8 -3
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/computers.d.ts +22 -0
- package/dist/agents/computers.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +3 -0
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/threads.d.ts +3 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/agents/token-rates.d.ts +13 -0
- package/dist/agents/token-rates.d.ts.map +1 -0
- package/dist/ai-accounting-filters.d.ts +14 -0
- package/dist/ai-accounting-filters.d.ts.map +1 -0
- package/dist/ai-cost-groups.d.ts +1 -1
- package/dist/ai-cost-groups.d.ts.map +1 -1
- package/dist/ai-cost-storage.d.ts.map +1 -1
- package/dist/index.js +421 -75
- package/dist/index.js.map +18 -15
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/ontology-storage/commits.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts +2 -1
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/pipeline-run-storage.d.ts.map +1 -1
- package/dist/sync-run-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +10 -3
- package/src/agents/computers.ts +112 -0
- package/src/agents/rows.ts +9 -1
- package/src/agents/runs.ts +25 -3
- package/src/agents/threads.ts +100 -22
- package/src/agents/token-rates.ts +90 -0
- package/src/ai-accounting-filters.ts +51 -0
- package/src/ai-cost-groups.ts +52 -14
- package/src/ai-cost-storage.ts +101 -39
- package/src/migrations/082-agent-token-rates.sql +17 -0
- package/src/migrations/083-agent-computer-sessions.sql +27 -0
- package/src/migrations.ts +6 -0
- package/src/objects/query-compiler.ts +26 -15
- package/src/ontology-storage/commits.ts +9 -0
- package/src/ontology-storage/materializations.ts +18 -1
- package/src/pipeline-run-storage.ts +5 -7
- package/src/sync-run-storage.ts +2 -7
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { normalizeAiAccountingQuery } from "@sixb/core/internal/ai-cost-storage-provider"
|
|
2
|
+
|
|
3
|
+
// The kind, source and requester filters every accounting read shares, over the joins it makes:
|
|
4
|
+
// `execution`, `direct_agent`, `workflow_node`.
|
|
5
|
+
|
|
6
|
+
/** Where a call came from, as `aiAccountingSource` reads its attribution. */
|
|
7
|
+
export const SQLITE_SOURCE_WORKFLOW = `COALESCE(
|
|
8
|
+
workflow_node.workflow_id,
|
|
9
|
+
CASE WHEN execution.executor_kind = 'workflow' THEN execution.authority_primitive_id END
|
|
10
|
+
)`
|
|
11
|
+
export const SQLITE_SOURCE_KIND = `CASE
|
|
12
|
+
WHEN direct_agent.id IS NOT NULL THEN 'agent'
|
|
13
|
+
WHEN ${SQLITE_SOURCE_WORKFLOW} IS NOT NULL THEN 'workflow'
|
|
14
|
+
WHEN execution.executor_kind = 'action' THEN 'action'
|
|
15
|
+
ELSE 'other'
|
|
16
|
+
END`
|
|
17
|
+
|
|
18
|
+
/** The kind, source and requester filters over the columns that hold them; 12 positional params. */
|
|
19
|
+
export function sqliteAccountingFilter(columns: {
|
|
20
|
+
readonly kind: string
|
|
21
|
+
readonly sourceKind: string
|
|
22
|
+
readonly sourceWorkflow: string
|
|
23
|
+
readonly userId: string
|
|
24
|
+
readonly serviceAccountId: string
|
|
25
|
+
}): string {
|
|
26
|
+
return `(? IS NULL OR ${columns.kind} = ?)
|
|
27
|
+
AND (? IS NULL OR ${columns.sourceKind} = ?)
|
|
28
|
+
AND (? IS NULL OR ${columns.sourceWorkflow} = ?)
|
|
29
|
+
AND (
|
|
30
|
+
? IS NULL
|
|
31
|
+
OR (? = 'none' AND ${columns.userId} IS NULL AND ${columns.serviceAccountId} IS NULL)
|
|
32
|
+
OR (? = 'user' AND ${columns.userId} = ?)
|
|
33
|
+
OR (? = 'serviceAccount' AND ${columns.serviceAccountId} = ?)
|
|
34
|
+
)`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function sqliteAccountingFilterParameters(
|
|
38
|
+
query: ReturnType<typeof normalizeAiAccountingQuery>
|
|
39
|
+
): readonly (string | null)[] {
|
|
40
|
+
const kind = query.modelKind ?? null
|
|
41
|
+
const source = query.source?.kind ?? null
|
|
42
|
+
const workflow = query.source?.kind === "workflow" ? (query.source.workflowId ?? null) : null
|
|
43
|
+
const mode =
|
|
44
|
+
query.requester === undefined
|
|
45
|
+
? null
|
|
46
|
+
: query.requester === "none"
|
|
47
|
+
? "none"
|
|
48
|
+
: query.requester.type
|
|
49
|
+
const id = query.requester === undefined || query.requester === "none" ? null : query.requester.id
|
|
50
|
+
return [kind, kind, source, source, workflow, workflow, mode, mode, mode, id, mode, id]
|
|
51
|
+
}
|
package/src/ai-cost-groups.ts
CHANGED
|
@@ -3,17 +3,31 @@ import {
|
|
|
3
3
|
aiModelCallGroupFragmentFromRow,
|
|
4
4
|
buildAiModelCallGroupsFromFragments,
|
|
5
5
|
normalizeAiModelCallAccountingQuery,
|
|
6
|
+
normalizeAiModelCallGroupSort,
|
|
6
7
|
TOKENLESS_MODEL_KINDS,
|
|
7
8
|
} from "@sixb/core/internal/ai-cost-storage-provider"
|
|
8
9
|
import type { ListAiModelCallGroupsInput, ListAiModelCallGroupsResult } from "@sixb/core/storage"
|
|
10
|
+
import {
|
|
11
|
+
SQLITE_SOURCE_KIND,
|
|
12
|
+
SQLITE_SOURCE_WORKFLOW,
|
|
13
|
+
sqliteAccountingFilter,
|
|
14
|
+
sqliteAccountingFilterParameters,
|
|
15
|
+
} from "./ai-accounting-filters"
|
|
9
16
|
import type { SqliteStoreConnection } from "./transactions"
|
|
10
17
|
|
|
11
|
-
/**
|
|
18
|
+
/** Positional `?` placeholders numbered from `start`, to sit among this file's numbered ones. */
|
|
19
|
+
function numbered(sql: string, start: number): string {
|
|
20
|
+
let next = start
|
|
21
|
+
return sql.replace(/\?(?!\d)/g, () => `?${next++}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Paginate conversations, workflow runs and other initiating executions, then aggregate their matching calls without loading usage payloads. */
|
|
12
25
|
export async function listAiModelCallGroups(
|
|
13
26
|
connection: SqliteStoreConnection,
|
|
14
27
|
input: ListAiModelCallGroupsInput
|
|
15
28
|
): Promise<ListAiModelCallGroupsResult> {
|
|
16
29
|
const query = normalizeAiModelCallAccountingQuery(input)
|
|
30
|
+
const sort = normalizeAiModelCallGroupSort(input.sort)
|
|
17
31
|
const parameters = [
|
|
18
32
|
query.projectId,
|
|
19
33
|
query.from.toISOString(),
|
|
@@ -22,14 +36,15 @@ export async function listAiModelCallGroups(
|
|
|
22
36
|
query.modelId ?? null,
|
|
23
37
|
query.valuationStatus ?? null,
|
|
24
38
|
]
|
|
25
|
-
const
|
|
39
|
+
const filters = sqliteAccountingFilterParameters(query)
|
|
40
|
+
const count = connection.db.query(COUNT_SQL).get(...parameters, ...filters) as { total: string }
|
|
26
41
|
const rows = connection.db
|
|
27
42
|
.query(PAGE_SQL)
|
|
28
|
-
.all(...parameters, query.limit, query.offset) as AiModelCallGroupRow[]
|
|
43
|
+
.all(...parameters, ...filters, query.limit, query.offset, sort) as AiModelCallGroupRow[]
|
|
29
44
|
const total = Number(count?.total)
|
|
30
45
|
if (!Number.isSafeInteger(total) || total < 0)
|
|
31
46
|
throw new RangeError("[Sixb] Invalid model-call group count.")
|
|
32
|
-
const items = buildAiModelCallGroupsFromFragments(rows.map(aiModelCallGroupFragmentFromRow))
|
|
47
|
+
const items = buildAiModelCallGroupsFromFragments(rows.map(aiModelCallGroupFragmentFromRow), sort)
|
|
33
48
|
return { items, total, hasMore: query.offset + items.length < total }
|
|
34
49
|
}
|
|
35
50
|
|
|
@@ -39,8 +54,15 @@ const TOKENLESS_KINDS = TOKENLESS_MODEL_KINDS.map((kind) => `'${kind}'`).join(",
|
|
|
39
54
|
const FILTERED_SQL = `
|
|
40
55
|
WITH filtered AS (
|
|
41
56
|
SELECT COALESCE(root_agent.execution_id, usage.execution_id) AS root_execution_id,
|
|
57
|
+
-- What the call is listed under, as aiModelCallGroupKey computes it.
|
|
58
|
+
CASE
|
|
59
|
+
WHEN root_agent.thread_id IS NOT NULL THEN 'thread:' || root_agent.thread_id
|
|
60
|
+
WHEN workflow_node.workflow_run_id IS NOT NULL THEN 'workflowRun:' || workflow_node.workflow_run_id
|
|
61
|
+
ELSE COALESCE(root_agent.execution_id, usage.execution_id)
|
|
62
|
+
END AS group_key,
|
|
42
63
|
usage.execution_id, usage.provider_id, usage.requested_model_id, usage.occurred_at,
|
|
43
|
-
usage.total_tokens, usage.
|
|
64
|
+
usage.total_tokens, usage.input_tokens, usage.cache_read_input_tokens,
|
|
65
|
+
usage.model_kind, usage.output_images, usage.input_characters,
|
|
44
66
|
usage.audio_duration_ms,
|
|
45
67
|
execution.executor_kind, execution.executor_id, execution.authority_primitive_id AS primitive_id,
|
|
46
68
|
COALESCE(cost.status, 'unvalued') AS valuation_status,
|
|
@@ -53,7 +75,8 @@ WITH filtered AS (
|
|
|
53
75
|
workflow_node.node_id AS workflow_agent_step_id,
|
|
54
76
|
workflow_agent.node_run_id AS workflow_node_run_id,
|
|
55
77
|
workflow_node.workflow_id AS workflow_id,
|
|
56
|
-
workflow_node.workflow_run_id AS workflow_run_id
|
|
78
|
+
workflow_node.workflow_run_id AS workflow_run_id,
|
|
79
|
+
execution.requested_by_user_id, execution.requested_by_service_account_id
|
|
57
80
|
FROM ai_model_call_usage AS usage
|
|
58
81
|
JOIN executions AS execution ON execution.project_id = usage.project_id AND execution.id = usage.execution_id
|
|
59
82
|
LEFT JOIN agent_runs AS direct_agent
|
|
@@ -73,23 +96,36 @@ WITH filtered AS (
|
|
|
73
96
|
AND (?4 IS NULL OR usage.provider_id = ?4)
|
|
74
97
|
AND (?5 IS NULL OR usage.requested_model_id = ?5)
|
|
75
98
|
AND (?6 IS NULL OR COALESCE(cost.status, 'unvalued') = ?6)
|
|
99
|
+
AND ${numbered(
|
|
100
|
+
sqliteAccountingFilter({
|
|
101
|
+
kind: "usage.model_kind",
|
|
102
|
+
sourceKind: SQLITE_SOURCE_KIND,
|
|
103
|
+
sourceWorkflow: SQLITE_SOURCE_WORKFLOW,
|
|
104
|
+
userId: "execution.requested_by_user_id",
|
|
105
|
+
serviceAccountId: "execution.requested_by_service_account_id",
|
|
106
|
+
}),
|
|
107
|
+
7
|
|
108
|
+
)}
|
|
76
109
|
), roots AS (
|
|
77
|
-
SELECT
|
|
78
|
-
FROM filtered GROUP BY
|
|
110
|
+
SELECT group_key, MAX(occurred_at) AS last_call_at, TOTAL(amount_nanos) AS cost_nanos
|
|
111
|
+
FROM filtered GROUP BY group_key
|
|
79
112
|
)
|
|
80
113
|
`
|
|
81
114
|
const COUNT_SQL = `${FILTERED_SQL} SELECT CAST(COUNT(*) AS TEXT) AS total FROM roots`
|
|
82
115
|
const PAGE_SQL = `${FILTERED_SQL}, page AS (
|
|
83
|
-
SELECT
|
|
84
|
-
ORDER BY last_call_at DESC,
|
|
116
|
+
SELECT group_key FROM roots
|
|
117
|
+
ORDER BY CASE WHEN ?21 = 'cost' THEN cost_nanos END DESC, last_call_at DESC, group_key ASC
|
|
118
|
+
LIMIT ?19 OFFSET ?20
|
|
85
119
|
)
|
|
86
|
-
SELECT filtered.root_execution_id, execution_id, provider_id, requested_model_id,
|
|
120
|
+
SELECT filtered.group_key, root_execution_id, execution_id, provider_id, requested_model_id,
|
|
87
121
|
MIN(occurred_at) AS first_call_at, MAX(occurred_at) AS last_call_at,
|
|
88
122
|
CAST(COUNT(*) AS TEXT) AS call_count,
|
|
89
123
|
CAST(COUNT(total_tokens) AS TEXT) AS token_count,
|
|
90
124
|
CAST(COUNT(CASE WHEN total_tokens IS NULL AND model_kind IN (${TOKENLESS_KINDS}) THEN 1 END) AS TEXT)
|
|
91
125
|
AS tokenless_count,
|
|
92
126
|
CAST(SUM(total_tokens) AS TEXT) AS total_tokens,
|
|
127
|
+
CAST(SUM(input_tokens) AS TEXT) AS input_tokens,
|
|
128
|
+
CAST(SUM(cache_read_input_tokens) AS TEXT) AS cache_read_input_tokens,
|
|
93
129
|
CAST(SUM(output_images) AS TEXT) AS output_images,
|
|
94
130
|
CAST(SUM(input_characters) AS TEXT) AS input_characters,
|
|
95
131
|
CAST(SUM(audio_duration_ms) AS TEXT) AS audio_duration_ms,
|
|
@@ -106,8 +142,10 @@ SELECT filtered.root_execution_id, execution_id, provider_id, requested_model_id
|
|
|
106
142
|
MAX(workflow_agent_step_id) AS workflow_agent_step_id,
|
|
107
143
|
MAX(workflow_node_run_id) AS workflow_node_run_id,
|
|
108
144
|
MAX(workflow_id) AS workflow_id,
|
|
109
|
-
MAX(workflow_run_id) AS workflow_run_id
|
|
110
|
-
|
|
111
|
-
|
|
145
|
+
MAX(workflow_run_id) AS workflow_run_id,
|
|
146
|
+
MAX(requested_by_user_id) AS requested_by_user_id,
|
|
147
|
+
MAX(requested_by_service_account_id) AS requested_by_service_account_id
|
|
148
|
+
FROM filtered JOIN page ON page.group_key = filtered.group_key
|
|
149
|
+
GROUP BY filtered.group_key, root_execution_id, execution_id, provider_id, requested_model_id,
|
|
112
150
|
valuation_status, amount_currency
|
|
113
151
|
`
|
package/src/ai-cost-storage.ts
CHANGED
|
@@ -28,6 +28,12 @@ import {
|
|
|
28
28
|
type QueryAiAccountingOverviewInput,
|
|
29
29
|
type SummarizeAiCostExecutionsInput,
|
|
30
30
|
} from "@sixb/core/storage"
|
|
31
|
+
import {
|
|
32
|
+
SQLITE_SOURCE_KIND,
|
|
33
|
+
SQLITE_SOURCE_WORKFLOW,
|
|
34
|
+
sqliteAccountingFilter,
|
|
35
|
+
sqliteAccountingFilterParameters,
|
|
36
|
+
} from "./ai-accounting-filters"
|
|
31
37
|
import { listAiModelCallGroups } from "./ai-cost-groups"
|
|
32
38
|
import { requestedReasoningFromRow } from "./ai-usage-storage"
|
|
33
39
|
import { isUniqueConstraintError } from "./storage-errors"
|
|
@@ -96,6 +102,8 @@ export class SqliteAiCostStorage implements AiCostStorage {
|
|
|
96
102
|
query.providerId ?? null,
|
|
97
103
|
query.modelId ?? null,
|
|
98
104
|
query.modelId ?? null,
|
|
105
|
+
...sqliteAccountingFilterParameters(query),
|
|
106
|
+
input.bucket,
|
|
99
107
|
input.bucket
|
|
100
108
|
) as AggregateRow[]
|
|
101
109
|
return buildAiAccountingOverviewFromFragments(input, rows.map(aggregateFragmentFromRow))
|
|
@@ -176,13 +184,34 @@ const SQLITE_ACCOUNTING_LIST_FILTER = `
|
|
|
176
184
|
ELSE 'unvalued'
|
|
177
185
|
END
|
|
178
186
|
)
|
|
187
|
+
AND ${sqliteAccountingFilter({
|
|
188
|
+
kind: "usage.model_kind",
|
|
189
|
+
sourceKind: SQLITE_SOURCE_KIND,
|
|
190
|
+
sourceWorkflow: SQLITE_SOURCE_WORKFLOW,
|
|
191
|
+
userId: "execution.requested_by_user_id",
|
|
192
|
+
serviceAccountId: "execution.requested_by_service_account_id",
|
|
193
|
+
})}
|
|
194
|
+
`
|
|
195
|
+
|
|
196
|
+
const SQLITE_ACCOUNTING_LIST_JOINS = `
|
|
197
|
+
JOIN executions AS execution ON execution.project_id = usage.project_id AND execution.id = usage.execution_id
|
|
198
|
+
LEFT JOIN agent_runs AS direct_agent
|
|
199
|
+
ON direct_agent.project_id = usage.project_id
|
|
200
|
+
AND direct_agent.execution_id = usage.execution_id
|
|
201
|
+
LEFT JOIN workflow_agent_node_runs AS workflow_agent
|
|
202
|
+
ON workflow_agent.project_id = usage.project_id
|
|
203
|
+
AND workflow_agent.execution_id = usage.execution_id
|
|
204
|
+
LEFT JOIN workflow_node_runs AS workflow_node
|
|
205
|
+
ON workflow_node.project_id = workflow_agent.project_id
|
|
206
|
+
AND workflow_node.id = workflow_agent.node_run_id
|
|
207
|
+
LEFT JOIN ai_model_call_valuations AS cost
|
|
208
|
+
ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
|
|
179
209
|
`
|
|
180
210
|
|
|
181
211
|
const SQLITE_ACCOUNTING_LIST_COUNT_SQL = `
|
|
182
212
|
SELECT CAST(COUNT(*) AS TEXT) AS total
|
|
183
213
|
FROM ai_model_call_usage AS usage
|
|
184
|
-
|
|
185
|
-
ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
|
|
214
|
+
${SQLITE_ACCOUNTING_LIST_JOINS}
|
|
186
215
|
WHERE ${SQLITE_ACCOUNTING_LIST_FILTER}
|
|
187
216
|
`
|
|
188
217
|
|
|
@@ -218,18 +247,7 @@ const SQLITE_ACCOUNTING_LIST_PAGE_SQL = `
|
|
|
218
247
|
cost.details AS cost_details,
|
|
219
248
|
cost.rated_at AS cost_rated_at
|
|
220
249
|
FROM ai_model_call_usage AS usage
|
|
221
|
-
|
|
222
|
-
LEFT JOIN agent_runs AS direct_agent
|
|
223
|
-
ON direct_agent.project_id = usage.project_id
|
|
224
|
-
AND direct_agent.execution_id = usage.execution_id
|
|
225
|
-
LEFT JOIN workflow_agent_node_runs AS workflow_agent
|
|
226
|
-
ON workflow_agent.project_id = usage.project_id
|
|
227
|
-
AND workflow_agent.execution_id = usage.execution_id
|
|
228
|
-
LEFT JOIN workflow_node_runs AS workflow_node
|
|
229
|
-
ON workflow_node.project_id = workflow_agent.project_id
|
|
230
|
-
AND workflow_node.id = workflow_agent.node_run_id
|
|
231
|
-
LEFT JOIN ai_model_call_valuations AS cost
|
|
232
|
-
ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
|
|
250
|
+
${SQLITE_ACCOUNTING_LIST_JOINS}
|
|
233
251
|
WHERE ${SQLITE_ACCOUNTING_LIST_FILTER}
|
|
234
252
|
ORDER BY usage.occurred_at DESC, usage.id ASC
|
|
235
253
|
LIMIT ? OFFSET ?
|
|
@@ -269,7 +287,10 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
|
|
|
269
287
|
ELSE 'unvalued'
|
|
270
288
|
END AS valuation_status,
|
|
271
289
|
CASE WHEN cost.status = 'rated' THEN cost.currency ELSE NULL END AS amount_currency,
|
|
272
|
-
CASE WHEN cost.status = 'rated' THEN cost.amount_nanos ELSE NULL END AS amount_nanos
|
|
290
|
+
CASE WHEN cost.status = 'rated' THEN cost.amount_nanos ELSE NULL END AS amount_nanos,
|
|
291
|
+
execution.requested_by_user_id AS requester_user_id,
|
|
292
|
+
execution.requested_by_service_account_id AS requester_service_account_id,
|
|
293
|
+
${SQLITE_SOURCE_KIND} AS source_kind
|
|
273
294
|
FROM ai_model_call_usage AS usage
|
|
274
295
|
JOIN executions AS execution ON execution.project_id = usage.project_id AND execution.id = usage.execution_id
|
|
275
296
|
LEFT JOIN agent_runs AS direct_agent
|
|
@@ -286,10 +307,19 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
|
|
|
286
307
|
WHERE usage.project_id = ? AND usage.occurred_at >= ? AND usage.occurred_at < ?
|
|
287
308
|
AND (? IS NULL OR usage.provider_id = ?)
|
|
288
309
|
AND (? IS NULL OR usage.requested_model_id = ?)
|
|
310
|
+
), filtered AS (
|
|
311
|
+
SELECT * FROM classified
|
|
312
|
+
WHERE ${sqliteAccountingFilter({
|
|
313
|
+
kind: "model_kind",
|
|
314
|
+
sourceKind: "source_kind",
|
|
315
|
+
sourceWorkflow: "attribution_workflow_id",
|
|
316
|
+
userId: "requester_user_id",
|
|
317
|
+
serviceAccountId: "requester_service_account_id",
|
|
318
|
+
})}
|
|
289
319
|
), expanded AS (
|
|
290
320
|
SELECT 'totals' AS dimension, NULL AS key_1, NULL AS key_2, NULL AS key_3, NULL AS bucket_start,
|
|
291
|
-
|
|
292
|
-
FROM
|
|
321
|
+
filtered.*
|
|
322
|
+
FROM filtered
|
|
293
323
|
UNION ALL
|
|
294
324
|
SELECT 'series', NULL, NULL, NULL,
|
|
295
325
|
CASE ?
|
|
@@ -301,18 +331,39 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
|
|
|
301
331
|
printf('-%d days', (CAST(strftime('%w', occurred_at) AS INTEGER) + 6) % 7)
|
|
302
332
|
)
|
|
303
333
|
END,
|
|
304
|
-
|
|
305
|
-
FROM
|
|
334
|
+
filtered.*
|
|
335
|
+
FROM filtered
|
|
306
336
|
UNION ALL
|
|
307
|
-
SELECT '
|
|
337
|
+
SELECT 'modelSeries', provider_id, requested_model_id, NULL,
|
|
338
|
+
CASE ?
|
|
339
|
+
WHEN 'hour' THEN strftime('%Y-%m-%dT%H:00:00.000Z', occurred_at)
|
|
340
|
+
WHEN 'day' THEN strftime('%Y-%m-%dT00:00:00.000Z', occurred_at)
|
|
341
|
+
ELSE strftime(
|
|
342
|
+
'%Y-%m-%dT00:00:00.000Z',
|
|
343
|
+
occurred_at,
|
|
344
|
+
printf('-%d days', (CAST(strftime('%w', occurred_at) AS INTEGER) + 6) % 7)
|
|
345
|
+
)
|
|
346
|
+
END,
|
|
347
|
+
filtered.*
|
|
348
|
+
FROM filtered
|
|
308
349
|
UNION ALL
|
|
309
|
-
SELECT '
|
|
350
|
+
SELECT 'model', provider_id, requested_model_id, NULL, NULL, filtered.* FROM filtered
|
|
351
|
+
UNION ALL
|
|
352
|
+
SELECT 'person',
|
|
353
|
+
CASE
|
|
354
|
+
WHEN requester_user_id IS NOT NULL THEN 'user'
|
|
355
|
+
WHEN requester_service_account_id IS NOT NULL THEN 'serviceAccount'
|
|
356
|
+
END,
|
|
357
|
+
COALESCE(requester_user_id, requester_service_account_id), NULL, NULL, filtered.*
|
|
358
|
+
FROM filtered
|
|
310
359
|
UNION ALL
|
|
311
|
-
SELECT '
|
|
312
|
-
FROM classified WHERE attribution_agent_kind IS NOT NULL
|
|
360
|
+
SELECT 'kind', model_kind, NULL, NULL, NULL, filtered.* FROM filtered
|
|
313
361
|
UNION ALL
|
|
314
|
-
SELECT '
|
|
315
|
-
FROM
|
|
362
|
+
SELECT 'agent', attribution_agent_kind, attribution_workflow_id, attribution_agent_step_id, NULL, filtered.*
|
|
363
|
+
FROM filtered WHERE attribution_agent_kind IS NOT NULL
|
|
364
|
+
UNION ALL
|
|
365
|
+
SELECT 'workflow', attribution_workflow_id, NULL, NULL, NULL, filtered.*
|
|
366
|
+
FROM filtered WHERE attribution_workflow_id IS NOT NULL
|
|
316
367
|
)
|
|
317
368
|
SELECT dimension,
|
|
318
369
|
key_1,
|
|
@@ -377,6 +428,7 @@ function accountingListFilterParameters(
|
|
|
377
428
|
query.executionId ?? null,
|
|
378
429
|
query.valuationStatus ?? null,
|
|
379
430
|
query.valuationStatus ?? null,
|
|
431
|
+
...sqliteAccountingFilterParameters(query),
|
|
380
432
|
]
|
|
381
433
|
}
|
|
382
434
|
|
|
@@ -505,20 +557,30 @@ function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragm
|
|
|
505
557
|
const dimensionKeys =
|
|
506
558
|
row.dimension === "model"
|
|
507
559
|
? { providerId: required(row.key_1), modelId: required(row.key_2) }
|
|
508
|
-
: row.dimension === "
|
|
509
|
-
?
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
560
|
+
: row.dimension === "modelSeries"
|
|
561
|
+
? {
|
|
562
|
+
providerId: required(row.key_1),
|
|
563
|
+
modelId: required(row.key_2),
|
|
564
|
+
start: new Date(required(row.bucket_start)),
|
|
565
|
+
}
|
|
566
|
+
: row.dimension === "person"
|
|
567
|
+
? row.key_1 && row.key_2
|
|
568
|
+
? { requesterType: row.key_1 as "user" | "serviceAccount", requesterId: row.key_2 }
|
|
569
|
+
: {}
|
|
570
|
+
: row.dimension === "kind"
|
|
571
|
+
? withModelKind(aiAccountingModelKind(row.key_1))
|
|
572
|
+
: row.dimension === "agent"
|
|
573
|
+
? {
|
|
574
|
+
agentKind: required(row.key_1) as "agent" | "workflowAgent",
|
|
575
|
+
...(row.key_1 === "workflowAgent"
|
|
576
|
+
? { workflowId: required(row.key_2), agentStepId: required(row.key_3) }
|
|
577
|
+
: {}),
|
|
578
|
+
}
|
|
579
|
+
: row.dimension === "workflow"
|
|
580
|
+
? { workflowId: required(row.key_1) }
|
|
581
|
+
: row.dimension === "series"
|
|
582
|
+
? { start: new Date(required(row.bucket_start)) }
|
|
583
|
+
: {}
|
|
522
584
|
return {
|
|
523
585
|
dimension: row.dimension,
|
|
524
586
|
...dimensionKeys,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- What each model's tokenizer measured on a project's agent runs, one row per run: the bytes of a
|
|
2
|
+
-- request's text as the Agent's context estimator weighs them, against the text tokens the
|
|
3
|
+
-- provider counted. A record deletes the model's rows past its newest 20, so the table stays small.
|
|
4
|
+
-- Timestamps are ISO-8601 UTC text and compare lexicographically.
|
|
5
|
+
CREATE TABLE agent_token_rates (
|
|
6
|
+
project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
|
|
7
|
+
run_id TEXT NOT NULL CHECK (length(trim(run_id)) > 0),
|
|
8
|
+
provider_id TEXT NOT NULL CHECK (length(trim(provider_id)) > 0),
|
|
9
|
+
model_id TEXT NOT NULL CHECK (length(trim(model_id)) > 0),
|
|
10
|
+
bytes INTEGER NOT NULL CHECK (bytes > 0),
|
|
11
|
+
tokens INTEGER NOT NULL CHECK (tokens > 0),
|
|
12
|
+
measured_at TEXT NOT NULL,
|
|
13
|
+
PRIMARY KEY (project_id, run_id)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX idx_agent_token_rates_model
|
|
17
|
+
ON agent_token_rates (project_id, provider_id, model_id, measured_at DESC, run_id DESC);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- Where a computer's thread runs: the computer's name and the folder there, beside its id. A
|
|
2
|
+
-- computer kept before folders were has none until its owner moves the thread.
|
|
3
|
+
ALTER TABLE agent_threads ADD COLUMN computer_name TEXT;
|
|
4
|
+
ALTER TABLE agent_threads ADD COLUMN computer_folder TEXT;
|
|
5
|
+
UPDATE agent_threads SET computer_id = NULL WHERE computer_id IS NOT NULL;
|
|
6
|
+
-- When its turns last moved to where they run now: the files earlier turns used are not there.
|
|
7
|
+
ALTER TABLE agent_threads ADD COLUMN moved_at TEXT;
|
|
8
|
+
|
|
9
|
+
-- Each open sixb on its owner's computers, a lease it renews: while one is live, the threads that
|
|
10
|
+
-- run on its computer in its folder are reachable there.
|
|
11
|
+
CREATE TABLE agent_computer_sessions (
|
|
12
|
+
project_id TEXT NOT NULL,
|
|
13
|
+
owner_principal_type TEXT NOT NULL CHECK (owner_principal_type IN ('user', 'serviceAccount', 'system')),
|
|
14
|
+
owner_principal_id TEXT NOT NULL CHECK (length(owner_principal_id) > 0),
|
|
15
|
+
id TEXT NOT NULL CHECK (length(id) > 0),
|
|
16
|
+
computer_id TEXT NOT NULL,
|
|
17
|
+
computer_name TEXT NOT NULL,
|
|
18
|
+
folder TEXT NOT NULL,
|
|
19
|
+
expires_at TEXT NOT NULL,
|
|
20
|
+
PRIMARY KEY (project_id, owner_principal_type, owner_principal_id, id)
|
|
21
|
+
);
|
|
22
|
+
CREATE INDEX agent_computer_sessions_by_expiry ON agent_computer_sessions (project_id, expires_at);
|
|
23
|
+
|
|
24
|
+
-- Each open sixb asks every 20 seconds which of its owner's threads here have a turn active.
|
|
25
|
+
CREATE INDEX agent_threads_active_on_computer
|
|
26
|
+
ON agent_threads (project_id, owner_principal_type, owner_principal_id, computer_id, computer_folder)
|
|
27
|
+
WHERE active_run_id IS NOT NULL;
|
package/src/migrations.ts
CHANGED
|
@@ -313,6 +313,8 @@ export const sqliteStorageMigrations = defineMigrations({
|
|
|
313
313
|
sqliteSql("079-subagent-questions", subagentQuestionsSql),
|
|
314
314
|
sqliteSql("080-agent-run-cancel-requests", agentRunCancelRequestsSql),
|
|
315
315
|
sqliteSql("081-agent-thread-runs-on", agentThreadRunsOnSql),
|
|
316
|
+
sqliteSql("082-agent-token-rates", agentTokenRatesSql),
|
|
317
|
+
sqliteSql("083-agent-computer-sessions", agentComputerSessionsSql),
|
|
316
318
|
],
|
|
317
319
|
})
|
|
318
320
|
|
|
@@ -715,3 +717,7 @@ import agentRunCancelRequestsSql from "./migrations/080-agent-run-cancel-request
|
|
|
715
717
|
type: "text",
|
|
716
718
|
}
|
|
717
719
|
import agentThreadRunsOnSql from "./migrations/081-agent-thread-runs-on.sql" with { type: "text" }
|
|
720
|
+
import agentTokenRatesSql from "./migrations/082-agent-token-rates.sql" with { type: "text" }
|
|
721
|
+
import agentComputerSessionsSql from "./migrations/083-agent-computer-sessions.sql" with {
|
|
722
|
+
type: "text",
|
|
723
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
hasVectorProfile,
|
|
11
11
|
isVectorProfileQuery,
|
|
12
|
+
linkEndpointObjectTypeIds,
|
|
12
13
|
normalizeVector,
|
|
13
14
|
vectorSearchCandidateLimit,
|
|
14
15
|
} from "@sixb/core/internal/query"
|
|
@@ -190,14 +191,7 @@ function compileObjectQueryInternal(
|
|
|
190
191
|
case "page":
|
|
191
192
|
return compilePage(projectId, query.input, query.pageSize, query.pageToken, ctx)
|
|
192
193
|
case "traverse":
|
|
193
|
-
return compileTraversal(
|
|
194
|
-
projectId,
|
|
195
|
-
query.input,
|
|
196
|
-
query.linkId,
|
|
197
|
-
query.direction,
|
|
198
|
-
query.sourceObjectTypeId,
|
|
199
|
-
ctx
|
|
200
|
-
)
|
|
194
|
+
return compileTraversal(projectId, query, ctx)
|
|
201
195
|
case "set":
|
|
202
196
|
return compileSet(projectId, query.op, query.inputs, ctx)
|
|
203
197
|
case "project":
|
|
@@ -571,14 +565,13 @@ function compilePage(
|
|
|
571
565
|
|
|
572
566
|
function compileTraversal(
|
|
573
567
|
projectId: string,
|
|
574
|
-
|
|
575
|
-
linkId: string,
|
|
576
|
-
direction: "outgoing" | "incoming",
|
|
577
|
-
sourceObjectTypeId: string | undefined,
|
|
568
|
+
traversal: Extract<ObjectQuery, { kind: "traverse" }>,
|
|
578
569
|
ctx: CompileContext
|
|
579
570
|
): CompiledObjectQuery {
|
|
580
|
-
const
|
|
571
|
+
const { linkId, direction, sourceObjectTypeId } = traversal
|
|
572
|
+
const input = compileObjectQueryInternal(projectId, traversal.input, exactContext(ctx))
|
|
581
573
|
const outputAlias = direction === "outgoing" ? "target_object" : "source_object"
|
|
574
|
+
const endpointType = compileEndpointTypes(outputAlias, linkEndpointObjectTypeIds(traversal))
|
|
582
575
|
const joinSql =
|
|
583
576
|
direction === "outgoing"
|
|
584
577
|
? `
|
|
@@ -591,6 +584,7 @@ function compileTraversal(
|
|
|
591
584
|
ON target_object.project_id = edge.project_id
|
|
592
585
|
AND target_object.object_type_id = edge.target_type_id
|
|
593
586
|
AND target_object.primary_id = edge.target_id
|
|
587
|
+
${endpointType.sql}
|
|
594
588
|
`
|
|
595
589
|
: `
|
|
596
590
|
JOIN ${ctx.source.linksTable} AS edge
|
|
@@ -602,6 +596,7 @@ function compileTraversal(
|
|
|
602
596
|
ON source_object.project_id = edge.project_id
|
|
603
597
|
AND source_object.object_type_id = edge.source_type_id
|
|
604
598
|
AND source_object.primary_id = edge.source_id
|
|
599
|
+
${endpointType.sql}
|
|
605
600
|
`
|
|
606
601
|
const order = compileOrder(identityOrderFields())
|
|
607
602
|
const qualifiedOrder = compileOrder(identityOrderFields(), outputAlias)
|
|
@@ -614,7 +609,8 @@ function compileTraversal(
|
|
|
614
609
|
const args = [
|
|
615
610
|
...input.args,
|
|
616
611
|
linkId,
|
|
617
|
-
...(
|
|
612
|
+
...(direction === "incoming" && sourceObjectTypeId !== undefined ? [sourceObjectTypeId] : []),
|
|
613
|
+
...endpointType.args,
|
|
618
614
|
...qualifiedOrder.args,
|
|
619
615
|
]
|
|
620
616
|
|
|
@@ -630,6 +626,19 @@ function compileTraversal(
|
|
|
630
626
|
}
|
|
631
627
|
}
|
|
632
628
|
|
|
629
|
+
/** Only the types the ontology says this link can reach. */
|
|
630
|
+
function compileEndpointTypes(
|
|
631
|
+
alias: string,
|
|
632
|
+
objectTypeIds: readonly string[] | undefined
|
|
633
|
+
): CompiledPredicate {
|
|
634
|
+
if (!objectTypeIds?.length) return { sql: "", args: [] }
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
sql: `AND ${alias}.object_type_id IN (${objectTypeIds.map(() => "?").join(", ")})`,
|
|
638
|
+
args: [...objectTypeIds],
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
633
642
|
/** Correlation handle for an expansion: the parent row's identity columns. */
|
|
634
643
|
interface ExpansionParent {
|
|
635
644
|
project: string
|
|
@@ -730,7 +739,8 @@ function compileExpansionValue(
|
|
|
730
739
|
`${parentId} = ${parent.id}`,
|
|
731
740
|
`${edge}.link_id = ?`,
|
|
732
741
|
]
|
|
733
|
-
const
|
|
742
|
+
const endpointType = compileEndpointTypes(target, expansion.endpointObjectTypeIds)
|
|
743
|
+
const whereArgs: SqliteValue[] = [...endpointType.args, expansion.linkId]
|
|
734
744
|
if (incoming && expansion.sourceObjectTypeId !== undefined) {
|
|
735
745
|
whereParts.push(`${edge}.source_type_id = ?`)
|
|
736
746
|
whereArgs.push(expansion.sourceObjectTypeId)
|
|
@@ -742,6 +752,7 @@ function compileExpansionValue(
|
|
|
742
752
|
ON ${target}.project_id = ${edge}.project_id
|
|
743
753
|
AND ${target}.object_type_id = ${neighborType}
|
|
744
754
|
AND ${target}.primary_id = ${neighborId}
|
|
755
|
+
${endpointType.sql}
|
|
745
756
|
WHERE ${whereParts.join(" AND ")}
|
|
746
757
|
`
|
|
747
758
|
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
import {
|
|
12
12
|
assertNonblank,
|
|
13
13
|
assertNonnegativeInteger,
|
|
14
|
+
assertTimestamp,
|
|
14
15
|
commitRecord,
|
|
15
16
|
originWhere,
|
|
16
17
|
type SqliteOntologyCommitRow,
|
|
@@ -78,6 +79,9 @@ export class SqliteOntologyCommitStorage implements OntologyCommitStorage {
|
|
|
78
79
|
const limit = input.limit ?? -1
|
|
79
80
|
assertNonnegativeInteger(offset, "Ontology commit list offset")
|
|
80
81
|
if (limit !== -1) assertNonnegativeInteger(limit, "Ontology commit list limit")
|
|
82
|
+
if (input.committedAt !== undefined) {
|
|
83
|
+
assertTimestamp(input.committedAt, "Ontology commit list committedAt")
|
|
84
|
+
}
|
|
81
85
|
const filters = ["project_id = ?"]
|
|
82
86
|
const values: (string | number)[] = [input.projectId]
|
|
83
87
|
if (input.run?.kind === "action") {
|
|
@@ -87,6 +91,11 @@ export class SqliteOntologyCommitStorage implements OntologyCommitStorage {
|
|
|
87
91
|
filters.push("origin_run_id = ? AND origin_kind IN ('projection', 'telemetry')")
|
|
88
92
|
values.push(input.run.id)
|
|
89
93
|
}
|
|
94
|
+
if (input.committedAt !== undefined) {
|
|
95
|
+
// Commit times are stored canonical, so text equality is exact and the index serves it.
|
|
96
|
+
filters.push("committed_at = ?")
|
|
97
|
+
values.push(input.committedAt)
|
|
98
|
+
}
|
|
90
99
|
const where = filters.join(" AND ")
|
|
91
100
|
const totalRow = this.db
|
|
92
101
|
.query(`SELECT COUNT(*) AS total FROM ontology_commits WHERE ${where}`)
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
invalidCorrelation,
|
|
26
26
|
materializationOutboxWrite,
|
|
27
27
|
materializationPlanChunk,
|
|
28
|
+
prepareMaterializationStateRead,
|
|
28
29
|
sameNonnegativeCounts as sameCounts,
|
|
29
30
|
uniqueSorted,
|
|
30
31
|
} from "@sixb/core/internal/ontology-storage-provider"
|
|
@@ -36,16 +37,18 @@ import type {
|
|
|
36
37
|
MaterializationPlanHeader,
|
|
37
38
|
MaterializationPlanWorkRecord,
|
|
38
39
|
MaterializationSession,
|
|
40
|
+
MaterializationState,
|
|
39
41
|
MaterializationStatePage,
|
|
40
42
|
MaterializationVectorChangePage,
|
|
41
43
|
OntologyCommitRecord,
|
|
42
44
|
OntologyMaterializationStorage,
|
|
45
|
+
ReadMaterializationStateInput,
|
|
43
46
|
SourceActivationWrite,
|
|
44
47
|
StageMaterializationWorkInput,
|
|
45
48
|
StreamMaterializationStateInput,
|
|
46
49
|
StreamMaterializationVectorChangesInput,
|
|
47
50
|
} from "@sixb/core/storage"
|
|
48
|
-
import { yieldSqliteEventLoop } from "../transactions"
|
|
51
|
+
import { runDeferredReadTransaction, yieldSqliteEventLoop } from "../transactions"
|
|
49
52
|
import {
|
|
50
53
|
type SqliteMaterializationSessionState,
|
|
51
54
|
SqliteMaterializationSessions,
|
|
@@ -243,6 +246,20 @@ export class SqliteOntologyMaterializationStorage implements OntologyMaterializa
|
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
248
|
|
|
249
|
+
async readState(input: ReadMaterializationStateInput): Promise<MaterializationState> {
|
|
250
|
+
const request = prepareMaterializationStateRead(input)
|
|
251
|
+
|
|
252
|
+
// One snapshot for every statement, so the read never straddles a commit.
|
|
253
|
+
return runDeferredReadTransaction(this.db, () => {
|
|
254
|
+
const reader = new SqliteMaterializationStateReader(this.db, request.projectId)
|
|
255
|
+
return {
|
|
256
|
+
objects: reader.objectStates(request.objects),
|
|
257
|
+
links: reader.linkStates(request.links),
|
|
258
|
+
linkScopes: reader.linkSlotStates(request.linkScopes),
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
246
263
|
async stageWork(input: StageMaterializationWorkInput): Promise<void> {
|
|
247
264
|
this.sessions.stage(input)
|
|
248
265
|
await yieldSqliteEventLoop()
|