@codecaine-ai/agent-kernel-db 0.0.0-stage → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +82 -2
- package/package.json +38 -4
- package/src/actions/agent-runs.ts +146 -0
- package/src/actions/containers.ts +138 -0
- package/src/actions/index.ts +10 -0
- package/src/actions/node-sessions.ts +414 -0
- package/src/actions/pi-agent-sessions.ts +93 -0
- package/src/actions/prompt-revisions.ts +122 -0
- package/src/actions/read-api.ts +314 -0
- package/src/actions/trace-blobs.ts +71 -0
- package/src/actions/trace-events.ts +158 -0
- package/src/actions/tx-local.ts +254 -0
- package/src/actions/usage.ts +79 -0
- package/src/bootstrap.ts +143 -0
- package/src/client.ts +64 -0
- package/src/index.ts +12 -0
- package/src/manifest.ts +140 -0
- package/src/read-compat.ts +35 -0
- package/src/schema/agent-runs.ts +80 -0
- package/src/schema/containers.ts +72 -0
- package/src/schema/index.ts +31 -0
- package/src/schema/pi-agent-sessions.ts +82 -0
- package/src/schema/prompt-revisions.ts +34 -0
- package/src/schema/trace-blobs.ts +20 -0
- package/src/schema/trace-events.ts +30 -0
- package/src/types.ts +29 -0
- package/src/upgrade.ts +138 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read API helpers — container-first. All reads are keyed by containerId;
|
|
3
|
+
* there is no app-session identity in this package.
|
|
4
|
+
*/
|
|
5
|
+
import { and, asc, count, desc, eq, gt, inArray, max, type SQL } from "drizzle-orm";
|
|
6
|
+
|
|
7
|
+
import type { KernelDatabase } from "../client";
|
|
8
|
+
import { piAgentSessionSelection } from "../read-compat";
|
|
9
|
+
import { agentRuns } from "../schema/agent-runs";
|
|
10
|
+
import { containers } from "../schema/containers";
|
|
11
|
+
import { piAgentSessions } from "../schema/pi-agent-sessions";
|
|
12
|
+
import { traceEvents } from "../schema/trace-events";
|
|
13
|
+
import type {
|
|
14
|
+
AgentRun,
|
|
15
|
+
Container,
|
|
16
|
+
PiAgentSession,
|
|
17
|
+
TraceEventRow,
|
|
18
|
+
} from "../types";
|
|
19
|
+
|
|
20
|
+
export interface KernelTraceReadOptions {
|
|
21
|
+
after?: string | null;
|
|
22
|
+
limit?: number;
|
|
23
|
+
maxContainers?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `kind` is optional here so callers that build rows by hand (fixtures in
|
|
28
|
+
* linked consumers) keep compiling; reads always fill it, and a missing
|
|
29
|
+
* value means "pi".
|
|
30
|
+
*/
|
|
31
|
+
export interface PiAgentSessionWithEventCount
|
|
32
|
+
extends Omit<PiAgentSession, "kind"> {
|
|
33
|
+
kind?: PiAgentSession["kind"];
|
|
34
|
+
eventCount: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface KernelTraceReadRows {
|
|
38
|
+
rootContainer: Container;
|
|
39
|
+
containers: Container[];
|
|
40
|
+
piSessions: PiAgentSessionWithEventCount[];
|
|
41
|
+
agentRuns: AgentRun[];
|
|
42
|
+
events: TraceEventRow[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface KernelTraceDeleteResult {
|
|
46
|
+
containerIds: string[];
|
|
47
|
+
piSessionIds: string[];
|
|
48
|
+
deleted: {
|
|
49
|
+
traceEvents: number;
|
|
50
|
+
agentRuns: number;
|
|
51
|
+
piAgentSessions: number;
|
|
52
|
+
containers: number;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clampLimit(limit: number | undefined, fallback: number, max: number): number {
|
|
57
|
+
if (limit === undefined || !Number.isFinite(limit)) return fallback;
|
|
58
|
+
return Math.max(1, Math.min(Math.floor(limit), max));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SessionContainerStatsRow {
|
|
62
|
+
container: Container;
|
|
63
|
+
piSessionCount: number;
|
|
64
|
+
eventCount: number;
|
|
65
|
+
latestEventAt: string | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ListSessionContainersOptions {
|
|
69
|
+
/** Scope to one kernel's containers. */
|
|
70
|
+
kernelId?: string;
|
|
71
|
+
/** Container kind to list; defaults to "session". */
|
|
72
|
+
kind?: string;
|
|
73
|
+
limit?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Containers of kind "session" (newest first) with the per-container pi
|
|
78
|
+
* session count, event count, and latest event timestamp — the rows behind
|
|
79
|
+
* the trace-sessions index.
|
|
80
|
+
*/
|
|
81
|
+
export async function listSessionContainersWithStats(
|
|
82
|
+
db: KernelDatabase,
|
|
83
|
+
opts: ListSessionContainersOptions = {},
|
|
84
|
+
): Promise<SessionContainerStatsRow[]> {
|
|
85
|
+
const limit = clampLimit(opts.limit, 100, 500);
|
|
86
|
+
const kind = opts.kind ?? "session";
|
|
87
|
+
const conditions: SQL[] = [eq(containers.kind, kind)];
|
|
88
|
+
if (opts.kernelId) conditions.push(eq(containers.kernelId, opts.kernelId));
|
|
89
|
+
|
|
90
|
+
const rows = await db
|
|
91
|
+
.select()
|
|
92
|
+
.from(containers)
|
|
93
|
+
.where(and(...conditions))
|
|
94
|
+
.orderBy(desc(containers.createdAt))
|
|
95
|
+
.limit(limit);
|
|
96
|
+
if (rows.length === 0) return [];
|
|
97
|
+
|
|
98
|
+
const containerIds = rows.map((row) => row.id);
|
|
99
|
+
const piCounts = await db
|
|
100
|
+
.select({ containerId: piAgentSessions.containerId, count: count() })
|
|
101
|
+
.from(piAgentSessions)
|
|
102
|
+
.where(inArray(piAgentSessions.containerId, containerIds))
|
|
103
|
+
.groupBy(piAgentSessions.containerId);
|
|
104
|
+
const eventStats = await db
|
|
105
|
+
.select({
|
|
106
|
+
containerId: traceEvents.containerId,
|
|
107
|
+
count: count(),
|
|
108
|
+
latestEventAt: max(traceEvents.timestamp),
|
|
109
|
+
})
|
|
110
|
+
.from(traceEvents)
|
|
111
|
+
.where(inArray(traceEvents.containerId, containerIds))
|
|
112
|
+
.groupBy(traceEvents.containerId);
|
|
113
|
+
|
|
114
|
+
const piCountByContainer = new Map(
|
|
115
|
+
piCounts.map((row) => [row.containerId, Number(row.count ?? 0)]),
|
|
116
|
+
);
|
|
117
|
+
const eventStatsByContainer = new Map(
|
|
118
|
+
eventStats.map((row) => [
|
|
119
|
+
row.containerId,
|
|
120
|
+
{ count: Number(row.count ?? 0), latestEventAt: row.latestEventAt ?? null },
|
|
121
|
+
]),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return rows.map((container) => {
|
|
125
|
+
const stats = eventStatsByContainer.get(container.id);
|
|
126
|
+
return {
|
|
127
|
+
container,
|
|
128
|
+
piSessionCount: piCountByContainer.get(container.id) ?? 0,
|
|
129
|
+
eventCount: stats?.count ?? 0,
|
|
130
|
+
latestEventAt: stats?.latestEventAt ?? null,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Breadth-first walk of a container subtree, root first. */
|
|
136
|
+
export async function listContainerTree(
|
|
137
|
+
db: KernelDatabase,
|
|
138
|
+
rootContainerId: string,
|
|
139
|
+
opts: { maxContainers?: number } = {},
|
|
140
|
+
): Promise<Container[]> {
|
|
141
|
+
const maxContainers = clampLimit(opts.maxContainers, 500, 5000);
|
|
142
|
+
const [root] = await db
|
|
143
|
+
.select()
|
|
144
|
+
.from(containers)
|
|
145
|
+
.where(eq(containers.id, rootContainerId))
|
|
146
|
+
.limit(1);
|
|
147
|
+
|
|
148
|
+
if (!root) return [];
|
|
149
|
+
|
|
150
|
+
const rows: Container[] = [root];
|
|
151
|
+
const seen = new Set<string>([root.id]);
|
|
152
|
+
let frontier = [root.id];
|
|
153
|
+
|
|
154
|
+
while (frontier.length > 0 && rows.length < maxContainers) {
|
|
155
|
+
const remaining = maxContainers - rows.length;
|
|
156
|
+
const children = await db
|
|
157
|
+
.select()
|
|
158
|
+
.from(containers)
|
|
159
|
+
.where(inArray(containers.parentContainerId, frontier))
|
|
160
|
+
.orderBy(asc(containers.createdAt))
|
|
161
|
+
.limit(remaining);
|
|
162
|
+
|
|
163
|
+
frontier = [];
|
|
164
|
+
for (const child of children) {
|
|
165
|
+
if (seen.has(child.id)) continue;
|
|
166
|
+
seen.add(child.id);
|
|
167
|
+
rows.push(child);
|
|
168
|
+
frontier.push(child.id);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return rows;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Everything the viewer needs for one container subtree: the container rows,
|
|
177
|
+
* their sessions (with event counts), runs, and events — keyed by containerId
|
|
178
|
+
* only.
|
|
179
|
+
*/
|
|
180
|
+
export async function getKernelTraceReadRows(
|
|
181
|
+
db: KernelDatabase,
|
|
182
|
+
rootContainerId: string,
|
|
183
|
+
opts: KernelTraceReadOptions = {},
|
|
184
|
+
): Promise<KernelTraceReadRows | undefined> {
|
|
185
|
+
const containerRows = await listContainerTree(db, rootContainerId, {
|
|
186
|
+
maxContainers: opts.maxContainers,
|
|
187
|
+
});
|
|
188
|
+
const rootContainer = containerRows[0];
|
|
189
|
+
if (!rootContainer) return undefined;
|
|
190
|
+
|
|
191
|
+
const containerIds = containerRows.map((container) => container.id);
|
|
192
|
+
const limit = clampLimit(opts.limit, 5000, 10000);
|
|
193
|
+
|
|
194
|
+
const piSessionRows: PiAgentSession[] = await db
|
|
195
|
+
.select(piAgentSessionSelection(db))
|
|
196
|
+
.from(piAgentSessions)
|
|
197
|
+
.where(inArray(piAgentSessions.containerId, containerIds))
|
|
198
|
+
.orderBy(asc(piAgentSessions.createdAt));
|
|
199
|
+
|
|
200
|
+
const piSessionIds = piSessionRows.map((pi) => pi.id);
|
|
201
|
+
const eventCountRows =
|
|
202
|
+
piSessionIds.length > 0
|
|
203
|
+
? await db
|
|
204
|
+
.select({
|
|
205
|
+
piSessionId: traceEvents.piSessionId,
|
|
206
|
+
eventCount: count(),
|
|
207
|
+
})
|
|
208
|
+
.from(traceEvents)
|
|
209
|
+
.where(inArray(traceEvents.piSessionId, piSessionIds))
|
|
210
|
+
.groupBy(traceEvents.piSessionId)
|
|
211
|
+
: [];
|
|
212
|
+
const eventCountsByPiSession = new Map<string, number>();
|
|
213
|
+
for (const row of eventCountRows) {
|
|
214
|
+
if (row.piSessionId) {
|
|
215
|
+
eventCountsByPiSession.set(row.piSessionId, Number(row.eventCount ?? 0));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const piSessions: PiAgentSessionWithEventCount[] = piSessionRows.map(
|
|
219
|
+
(session) => ({
|
|
220
|
+
...session,
|
|
221
|
+
eventCount: eventCountsByPiSession.get(session.id) ?? 0,
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const runRows: AgentRun[] = await db
|
|
226
|
+
.select()
|
|
227
|
+
.from(agentRuns)
|
|
228
|
+
.where(inArray(agentRuns.containerId, containerIds))
|
|
229
|
+
.orderBy(asc(agentRuns.startedAt));
|
|
230
|
+
|
|
231
|
+
const eventConditions: SQL[] = [
|
|
232
|
+
inArray(traceEvents.containerId, containerIds),
|
|
233
|
+
];
|
|
234
|
+
if (opts.after) eventConditions.push(gt(traceEvents.timestamp, opts.after));
|
|
235
|
+
|
|
236
|
+
const eventRows: TraceEventRow[] = await db
|
|
237
|
+
.select()
|
|
238
|
+
.from(traceEvents)
|
|
239
|
+
.where(and(...eventConditions))
|
|
240
|
+
.orderBy(asc(traceEvents.timestamp), asc(traceEvents.eventId))
|
|
241
|
+
.limit(limit);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
rootContainer,
|
|
245
|
+
containers: containerRows,
|
|
246
|
+
piSessions,
|
|
247
|
+
agentRuns: runRows,
|
|
248
|
+
events: eventRows,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Delete a container subtree and all rows hanging off it. */
|
|
253
|
+
export async function deleteKernelTraceRows(
|
|
254
|
+
db: KernelDatabase,
|
|
255
|
+
rootContainerId: string,
|
|
256
|
+
opts: Pick<KernelTraceReadOptions, "maxContainers"> = {},
|
|
257
|
+
): Promise<KernelTraceDeleteResult | undefined> {
|
|
258
|
+
const containerRows = await listContainerTree(db, rootContainerId, {
|
|
259
|
+
maxContainers: opts.maxContainers,
|
|
260
|
+
});
|
|
261
|
+
const rootContainer = containerRows[0];
|
|
262
|
+
if (!rootContainer) return undefined;
|
|
263
|
+
|
|
264
|
+
const containerIds = containerRows.map((container) => container.id);
|
|
265
|
+
|
|
266
|
+
return db.transaction(async (tx) => {
|
|
267
|
+
const piSessionRows = await tx
|
|
268
|
+
.select({ id: piAgentSessions.id })
|
|
269
|
+
.from(piAgentSessions)
|
|
270
|
+
.where(inArray(piAgentSessions.containerId, containerIds));
|
|
271
|
+
const piSessionIds = piSessionRows.map((session) => session.id);
|
|
272
|
+
|
|
273
|
+
const eventConditions: SQL[] = [
|
|
274
|
+
inArray(traceEvents.containerId, containerIds),
|
|
275
|
+
];
|
|
276
|
+
const deletedEvents = await tx
|
|
277
|
+
.delete(traceEvents)
|
|
278
|
+
.where(and(...eventConditions))
|
|
279
|
+
.returning({ eventId: traceEvents.eventId });
|
|
280
|
+
|
|
281
|
+
const deletedRuns = await tx
|
|
282
|
+
.delete(agentRuns)
|
|
283
|
+
.where(inArray(agentRuns.containerId, containerIds))
|
|
284
|
+
.returning({ id: agentRuns.id });
|
|
285
|
+
|
|
286
|
+
const deletedPiSessions =
|
|
287
|
+
piSessionIds.length > 0
|
|
288
|
+
? await tx
|
|
289
|
+
.delete(piAgentSessions)
|
|
290
|
+
.where(inArray(piAgentSessions.id, piSessionIds))
|
|
291
|
+
.returning({ id: piAgentSessions.id })
|
|
292
|
+
: [];
|
|
293
|
+
|
|
294
|
+
const deletedContainers: Array<{ id: string }> = [];
|
|
295
|
+
for (const container of [...containerRows].reverse()) {
|
|
296
|
+
const rows = await tx
|
|
297
|
+
.delete(containers)
|
|
298
|
+
.where(eq(containers.id, container.id))
|
|
299
|
+
.returning({ id: containers.id });
|
|
300
|
+
deletedContainers.push(...rows);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
containerIds,
|
|
305
|
+
piSessionIds,
|
|
306
|
+
deleted: {
|
|
307
|
+
traceEvents: deletedEvents.length,
|
|
308
|
+
agentRuns: deletedRuns.length,
|
|
309
|
+
piAgentSessions: deletedPiSessions.length,
|
|
310
|
+
containers: deletedContainers.length,
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { eq, inArray } from "drizzle-orm";
|
|
3
|
+
import type { KernelDatabase } from "../client";
|
|
4
|
+
import { traceBlobs } from "../schema/trace-blobs";
|
|
5
|
+
import type { NewTraceBlob, TraceBlob } from "../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Content address for trace blob bytes: "b1-" + sha256hex of the raw bytes.
|
|
9
|
+
* The kernel capture code hashes payloads with this before storing them.
|
|
10
|
+
*/
|
|
11
|
+
export function hashTraceBlobBytes(bytes: Uint8Array): string {
|
|
12
|
+
return `b1-${createHash("sha256").update(bytes).digest("hex")}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Idempotent multi-row insert keyed by hash. Blobs are content-addressed and
|
|
17
|
+
* immutable, so a conflicting insert is benign — the existing row (including
|
|
18
|
+
* its createdAt) is left untouched. Returns the number of rows actually
|
|
19
|
+
* inserted (0 when every blob already existed).
|
|
20
|
+
*/
|
|
21
|
+
export async function upsertTraceBlobs(
|
|
22
|
+
db: KernelDatabase,
|
|
23
|
+
blobs: NewTraceBlob[],
|
|
24
|
+
): Promise<number> {
|
|
25
|
+
if (blobs.length === 0) return 0;
|
|
26
|
+
|
|
27
|
+
const inserted = await db
|
|
28
|
+
.insert(traceBlobs)
|
|
29
|
+
.values(blobs)
|
|
30
|
+
.onConflictDoNothing({ target: traceBlobs.hash })
|
|
31
|
+
.returning({ hash: traceBlobs.hash });
|
|
32
|
+
return inserted.length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getTraceBlob(
|
|
36
|
+
db: KernelDatabase,
|
|
37
|
+
hash: string,
|
|
38
|
+
): Promise<TraceBlob | null> {
|
|
39
|
+
const [row] = await db
|
|
40
|
+
.select()
|
|
41
|
+
.from(traceBlobs)
|
|
42
|
+
.where(eq(traceBlobs.hash, hash))
|
|
43
|
+
.limit(1);
|
|
44
|
+
return row ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Blob row metadata — everything except the data payload. */
|
|
48
|
+
export type TraceBlobMeta = Omit<TraceBlob, "data">;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Metadata rows (no data column) for the given hashes, for existence and
|
|
52
|
+
* size checks without pulling payload bytes off disk. Missing hashes are
|
|
53
|
+
* simply absent from the result.
|
|
54
|
+
*/
|
|
55
|
+
export async function getTraceBlobMetas(
|
|
56
|
+
db: KernelDatabase,
|
|
57
|
+
hashes: string[],
|
|
58
|
+
): Promise<TraceBlobMeta[]> {
|
|
59
|
+
if (hashes.length === 0) return [];
|
|
60
|
+
|
|
61
|
+
return db
|
|
62
|
+
.select({
|
|
63
|
+
hash: traceBlobs.hash,
|
|
64
|
+
kind: traceBlobs.kind,
|
|
65
|
+
mimeType: traceBlobs.mimeType,
|
|
66
|
+
byteLength: traceBlobs.byteLength,
|
|
67
|
+
createdAt: traceBlobs.createdAt,
|
|
68
|
+
})
|
|
69
|
+
.from(traceBlobs)
|
|
70
|
+
.where(inArray(traceBlobs.hash, hashes));
|
|
71
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { and, asc, eq, gt, inArray, sql } from "drizzle-orm";
|
|
2
|
+
import type { TraceEvent } from "@codecaine-ai/agent-kernel-protocol";
|
|
3
|
+
import type { KernelDatabase } from "../client";
|
|
4
|
+
import { traceEvents } from "../schema/trace-events";
|
|
5
|
+
import type { TraceEventRow } from "../types";
|
|
6
|
+
import { runImmediateTransaction, traceEventToRow } from "./tx-local";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Idempotent batch insert keyed by event_id (INSERT OR IGNORE) — replaying
|
|
10
|
+
* a batch never duplicates rows. The transport-only piSessionUuid on the
|
|
11
|
+
* envelope is resolved into the pi_session_id column at write time.
|
|
12
|
+
*/
|
|
13
|
+
export async function insertTraceEventsBatch(
|
|
14
|
+
db: KernelDatabase,
|
|
15
|
+
events: TraceEvent[],
|
|
16
|
+
): Promise<number> {
|
|
17
|
+
if (events.length === 0) return 0;
|
|
18
|
+
|
|
19
|
+
const inserted = await db
|
|
20
|
+
.insert(traceEvents)
|
|
21
|
+
.values(events.map(traceEventToRow))
|
|
22
|
+
.onConflictDoNothing({ target: traceEvents.eventId })
|
|
23
|
+
.returning({ eventId: traceEvents.eventId });
|
|
24
|
+
return inserted.length;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ListTraceEventsOptions {
|
|
28
|
+
typeFilter?: string[];
|
|
29
|
+
after?: string;
|
|
30
|
+
limit?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function listTraceEventsForContainer(
|
|
34
|
+
db: KernelDatabase,
|
|
35
|
+
containerId: string,
|
|
36
|
+
opts: ListTraceEventsOptions = {},
|
|
37
|
+
): Promise<TraceEventRow[]> {
|
|
38
|
+
const { typeFilter, after, limit = 100 } = opts;
|
|
39
|
+
|
|
40
|
+
const conditions = [eq(traceEvents.containerId, containerId)];
|
|
41
|
+
if (typeFilter && typeFilter.length > 0) {
|
|
42
|
+
conditions.push(inArray(traceEvents.type, typeFilter));
|
|
43
|
+
}
|
|
44
|
+
if (after) {
|
|
45
|
+
conditions.push(gt(traceEvents.timestamp, after));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return db
|
|
49
|
+
.select()
|
|
50
|
+
.from(traceEvents)
|
|
51
|
+
.where(and(...conditions))
|
|
52
|
+
.orderBy(asc(traceEvents.timestamp), asc(traceEvents.eventId))
|
|
53
|
+
.limit(Math.min(limit, 1000));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function listTraceEventsForRun(
|
|
57
|
+
db: KernelDatabase,
|
|
58
|
+
runId: string,
|
|
59
|
+
opts: ListTraceEventsOptions = {},
|
|
60
|
+
): Promise<TraceEventRow[]> {
|
|
61
|
+
const { typeFilter, after, limit = 100 } = opts;
|
|
62
|
+
|
|
63
|
+
const conditions = [eq(traceEvents.runId, runId)];
|
|
64
|
+
if (typeFilter && typeFilter.length > 0) {
|
|
65
|
+
conditions.push(inArray(traceEvents.type, typeFilter));
|
|
66
|
+
}
|
|
67
|
+
if (after) {
|
|
68
|
+
conditions.push(gt(traceEvents.timestamp, after));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return db
|
|
72
|
+
.select()
|
|
73
|
+
.from(traceEvents)
|
|
74
|
+
.where(and(...conditions))
|
|
75
|
+
.orderBy(asc(traceEvents.timestamp), asc(traceEvents.eventId))
|
|
76
|
+
.limit(Math.min(limit, 1000));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Envelope for a stored row; pi_session_id travels back as piSessionUuid. */
|
|
80
|
+
function traceEventFromRow(row: TraceEventRow): TraceEvent {
|
|
81
|
+
return {
|
|
82
|
+
eventId: row.eventId,
|
|
83
|
+
containerId: row.containerId,
|
|
84
|
+
type: row.type,
|
|
85
|
+
source: row.source,
|
|
86
|
+
traceLevel: row.traceLevel as TraceEvent["traceLevel"],
|
|
87
|
+
eventData: row.eventData as TraceEvent["eventData"],
|
|
88
|
+
timestamp: row.timestamp,
|
|
89
|
+
...(row.runId !== null && { runId: row.runId }),
|
|
90
|
+
...(row.piSessionId !== null && { piSessionUuid: row.piSessionId }),
|
|
91
|
+
...(row.agentId !== null && { agentId: row.agentId }),
|
|
92
|
+
...(row.userId !== null && { userId: row.userId }),
|
|
93
|
+
...(row.spanId !== null && { spanId: row.spanId }),
|
|
94
|
+
...(row.parentEventId !== null && { parentEventId: row.parentEventId }),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Every event of one run as envelopes, oldest first (uses idx_events_run).
|
|
100
|
+
* Unlike listTraceEventsForRun there is no row limit: a model-node replay
|
|
101
|
+
* rebuilds its result from the whole run.
|
|
102
|
+
*/
|
|
103
|
+
export async function getTraceEventsForRun(
|
|
104
|
+
db: KernelDatabase,
|
|
105
|
+
runId: string,
|
|
106
|
+
types?: readonly string[],
|
|
107
|
+
): Promise<TraceEvent[]> {
|
|
108
|
+
const conditions = [eq(traceEvents.runId, runId)];
|
|
109
|
+
if (types && types.length > 0) {
|
|
110
|
+
conditions.push(inArray(traceEvents.type, [...types]));
|
|
111
|
+
}
|
|
112
|
+
const rows = await db
|
|
113
|
+
.select()
|
|
114
|
+
.from(traceEvents)
|
|
115
|
+
.where(and(...conditions))
|
|
116
|
+
.orderBy(asc(traceEvents.timestamp), asc(traceEvents.eventId));
|
|
117
|
+
return rows.map(traceEventFromRow);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Promotable upsert for nested tool ends: inserts, or replaces an existing
|
|
122
|
+
* row with the same event_id only when the stored row's event_data.timing is
|
|
123
|
+
* 'approximate' and the new one is 'live'. Any other conflict keeps the
|
|
124
|
+
* stored row.
|
|
125
|
+
*
|
|
126
|
+
* INSERT … ON CONFLICT(event_id) DO UPDATE
|
|
127
|
+
* SET event_data = excluded.event_data, timestamp = excluded.timestamp
|
|
128
|
+
* WHERE json_extract(trace_events.event_data, '$.timing') = 'approximate'
|
|
129
|
+
* AND json_extract(excluded.event_data, '$.timing') = 'live'
|
|
130
|
+
*/
|
|
131
|
+
export async function upsertPromotableTraceEvent(
|
|
132
|
+
db: KernelDatabase,
|
|
133
|
+
event: TraceEvent,
|
|
134
|
+
): Promise<"inserted" | "promoted" | "kept"> {
|
|
135
|
+
return runImmediateTransaction(db, (tx) => {
|
|
136
|
+
const existed =
|
|
137
|
+
tx
|
|
138
|
+
.select({ eventId: traceEvents.eventId })
|
|
139
|
+
.from(traceEvents)
|
|
140
|
+
.where(eq(traceEvents.eventId, event.eventId))
|
|
141
|
+
.get() !== undefined;
|
|
142
|
+
const written = tx
|
|
143
|
+
.insert(traceEvents)
|
|
144
|
+
.values(traceEventToRow(event))
|
|
145
|
+
.onConflictDoUpdate({
|
|
146
|
+
target: traceEvents.eventId,
|
|
147
|
+
set: {
|
|
148
|
+
eventData: sql`excluded.event_data`,
|
|
149
|
+
timestamp: sql`excluded.timestamp`,
|
|
150
|
+
},
|
|
151
|
+
setWhere: sql`json_extract(${traceEvents.eventData}, '$.timing') = 'approximate' AND json_extract(excluded.event_data, '$.timing') = 'live'`,
|
|
152
|
+
})
|
|
153
|
+
.returning({ eventId: traceEvents.eventId })
|
|
154
|
+
.all();
|
|
155
|
+
if (!existed) return "inserted" as const;
|
|
156
|
+
return written.length > 0 ? ("promoted" as const) : ("kept" as const);
|
|
157
|
+
});
|
|
158
|
+
}
|