@agentlensai/server 0.6.0 → 0.7.0
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/db/benchmark-store.d.ts +74 -0
- package/dist/db/benchmark-store.d.ts.map +1 -0
- package/dist/db/benchmark-store.js +268 -0
- package/dist/db/benchmark-store.js.map +1 -0
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +48 -0
- package/dist/db/migrate.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/benchmark/engine.d.ts +24 -0
- package/dist/lib/benchmark/engine.d.ts.map +1 -0
- package/dist/lib/benchmark/engine.js +159 -0
- package/dist/lib/benchmark/engine.js.map +1 -0
- package/dist/lib/benchmark/metric-aggregator.d.ts +38 -0
- package/dist/lib/benchmark/metric-aggregator.d.ts.map +1 -0
- package/dist/lib/benchmark/metric-aggregator.js +159 -0
- package/dist/lib/benchmark/metric-aggregator.js.map +1 -0
- package/dist/lib/benchmark/statistical.d.ts +51 -0
- package/dist/lib/benchmark/statistical.d.ts.map +1 -0
- package/dist/lib/benchmark/statistical.js +381 -0
- package/dist/lib/benchmark/statistical.js.map +1 -0
- package/dist/lib/replay/builder.d.ts +28 -0
- package/dist/lib/replay/builder.d.ts.map +1 -0
- package/dist/lib/replay/builder.js +482 -0
- package/dist/lib/replay/builder.js.map +1 -0
- package/dist/routes/benchmarks.d.ts +18 -0
- package/dist/routes/benchmarks.d.ts.map +1 -0
- package/dist/routes/benchmarks.js +312 -0
- package/dist/routes/benchmarks.js.map +1 -0
- package/dist/routes/replay.d.ts +28 -0
- package/dist/routes/replay.d.ts.map +1 -0
- package/dist/routes/replay.js +140 -0
- package/dist/routes/replay.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark REST Endpoints (Story 3.5)
|
|
3
|
+
*
|
|
4
|
+
* POST /api/benchmarks — Create benchmark
|
|
5
|
+
* GET /api/benchmarks — List benchmarks
|
|
6
|
+
* GET /api/benchmarks/:id — Get benchmark detail
|
|
7
|
+
* PUT /api/benchmarks/:id/status — Transition status
|
|
8
|
+
* GET /api/benchmarks/:id/results — Get comparison results
|
|
9
|
+
* DELETE /api/benchmarks/:id — Delete draft/cancelled
|
|
10
|
+
*/
|
|
11
|
+
import { Hono } from 'hono';
|
|
12
|
+
import { BENCHMARK_METRICS } from '@agentlensai/core';
|
|
13
|
+
import { getTenantStore } from './tenant-helper.js';
|
|
14
|
+
import { BenchmarkStore, } from '../db/benchmark-store.js';
|
|
15
|
+
import { BenchmarkEngine } from '../lib/benchmark/engine.js';
|
|
16
|
+
// ─── Validation Helpers ────────────────────────────────────
|
|
17
|
+
const VALID_METRICS = new Set(BENCHMARK_METRICS);
|
|
18
|
+
const VALID_STATUSES = new Set(['draft', 'running', 'completed', 'cancelled']);
|
|
19
|
+
function validateCreateInput(body) {
|
|
20
|
+
if (!body || typeof body !== 'object') {
|
|
21
|
+
return { error: 'Request body is required' };
|
|
22
|
+
}
|
|
23
|
+
// Name required
|
|
24
|
+
if (!body.name || typeof body.name !== 'string' || body.name.trim() === '') {
|
|
25
|
+
return { error: 'name is required and must be a non-empty string' };
|
|
26
|
+
}
|
|
27
|
+
// Variants: 2-10 with name+tag
|
|
28
|
+
if (!Array.isArray(body.variants)) {
|
|
29
|
+
return { error: 'variants must be an array' };
|
|
30
|
+
}
|
|
31
|
+
if (body.variants.length < 2 || body.variants.length > 10) {
|
|
32
|
+
return { error: 'Must have between 2 and 10 variants' };
|
|
33
|
+
}
|
|
34
|
+
for (let i = 0; i < body.variants.length; i++) {
|
|
35
|
+
const v = body.variants[i];
|
|
36
|
+
if (!v || typeof v !== 'object') {
|
|
37
|
+
return { error: `variants[${i}] must be an object` };
|
|
38
|
+
}
|
|
39
|
+
if (!v.name || typeof v.name !== 'string' || v.name.trim() === '') {
|
|
40
|
+
return { error: `variants[${i}].name is required` };
|
|
41
|
+
}
|
|
42
|
+
if (!v.tag || typeof v.tag !== 'string' || v.tag.trim() === '') {
|
|
43
|
+
return { error: `variants[${i}].tag is required` };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Metrics: optional array, but if provided must be valid
|
|
47
|
+
let metrics = BENCHMARK_METRICS.filter(m => m !== 'health_score'); // default: all supported
|
|
48
|
+
if (body.metrics !== undefined) {
|
|
49
|
+
if (!Array.isArray(body.metrics) || body.metrics.length === 0) {
|
|
50
|
+
return { error: 'metrics must be a non-empty array' };
|
|
51
|
+
}
|
|
52
|
+
for (const m of body.metrics) {
|
|
53
|
+
if (!VALID_METRICS.has(m)) {
|
|
54
|
+
return { error: `Invalid metric: ${m}` };
|
|
55
|
+
}
|
|
56
|
+
if (m === 'health_score') {
|
|
57
|
+
return { error: `Metric "health_score" is not yet supported for benchmarks. It requires pre-computed health snapshots.` };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
metrics = body.metrics;
|
|
61
|
+
}
|
|
62
|
+
// minSessionsPerVariant: optional, must be ≥ 1
|
|
63
|
+
let minSessions;
|
|
64
|
+
if (body.minSessionsPerVariant !== undefined) {
|
|
65
|
+
const val = Number(body.minSessionsPerVariant);
|
|
66
|
+
if (!Number.isInteger(val) || val < 1) {
|
|
67
|
+
return { error: 'minSessionsPerVariant must be an integer ≥ 1' };
|
|
68
|
+
}
|
|
69
|
+
minSessions = val;
|
|
70
|
+
}
|
|
71
|
+
const input = {
|
|
72
|
+
name: body.name.trim(),
|
|
73
|
+
description: body.description ?? undefined,
|
|
74
|
+
agentId: body.agentId ?? undefined,
|
|
75
|
+
metrics,
|
|
76
|
+
minSessionsPerVariant: minSessions,
|
|
77
|
+
timeRange: body.timeRange,
|
|
78
|
+
variants: body.variants.map((v) => ({
|
|
79
|
+
name: v.name.trim(),
|
|
80
|
+
description: v.description ?? undefined,
|
|
81
|
+
tag: v.tag.trim(),
|
|
82
|
+
agentId: v.agentId ?? undefined,
|
|
83
|
+
})),
|
|
84
|
+
};
|
|
85
|
+
return { input };
|
|
86
|
+
}
|
|
87
|
+
// ─── Route Factory ─────────────────────────────────────────
|
|
88
|
+
export function benchmarkRoutes(store, db) {
|
|
89
|
+
const app = new Hono();
|
|
90
|
+
const engine = new BenchmarkEngine();
|
|
91
|
+
function getBenchmarkStore(c) {
|
|
92
|
+
if (!db)
|
|
93
|
+
return null;
|
|
94
|
+
return new BenchmarkStore(db);
|
|
95
|
+
}
|
|
96
|
+
function getTenantId(c) {
|
|
97
|
+
const apiKey = c.get('apiKey');
|
|
98
|
+
return apiKey?.tenantId ?? 'default';
|
|
99
|
+
}
|
|
100
|
+
// ─── POST / — Create benchmark ─────────────────────────
|
|
101
|
+
app.post('/', async (c) => {
|
|
102
|
+
const benchmarkStore = getBenchmarkStore(c);
|
|
103
|
+
if (!benchmarkStore) {
|
|
104
|
+
return c.json({ error: 'Database not available', status: 500 }, 500);
|
|
105
|
+
}
|
|
106
|
+
const tenantId = getTenantId(c);
|
|
107
|
+
const body = await c.req.json().catch(() => null);
|
|
108
|
+
const { error, input } = validateCreateInput(body);
|
|
109
|
+
if (error || !input) {
|
|
110
|
+
return c.json({ error, status: 400 }, 400);
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const benchmark = benchmarkStore.create(tenantId, input);
|
|
114
|
+
return c.json(benchmark, 201);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
const message = err instanceof Error ? err.message : 'Failed to create benchmark';
|
|
118
|
+
return c.json({ error: message, status: 400 }, 400);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// ─── GET / — List benchmarks ────────────────────────────
|
|
122
|
+
app.get('/', async (c) => {
|
|
123
|
+
const benchmarkStore = getBenchmarkStore(c);
|
|
124
|
+
if (!benchmarkStore) {
|
|
125
|
+
return c.json({ error: 'Database not available', status: 500 }, 500);
|
|
126
|
+
}
|
|
127
|
+
const tenantId = getTenantId(c);
|
|
128
|
+
// Parse query params
|
|
129
|
+
const statusRaw = c.req.query('status');
|
|
130
|
+
const agentId = c.req.query('agentId') || undefined;
|
|
131
|
+
const limitStr = c.req.query('limit');
|
|
132
|
+
const offsetStr = c.req.query('offset');
|
|
133
|
+
// Validate status
|
|
134
|
+
let status;
|
|
135
|
+
if (statusRaw) {
|
|
136
|
+
if (!VALID_STATUSES.has(statusRaw)) {
|
|
137
|
+
return c.json({ error: `Invalid status: ${statusRaw}`, status: 400 }, 400);
|
|
138
|
+
}
|
|
139
|
+
status = statusRaw;
|
|
140
|
+
}
|
|
141
|
+
// Validate limit (1-100, default 20)
|
|
142
|
+
let limit = 20;
|
|
143
|
+
if (limitStr !== undefined && limitStr !== '') {
|
|
144
|
+
const val = parseInt(limitStr, 10);
|
|
145
|
+
if (isNaN(val) || val < 1 || val > 100) {
|
|
146
|
+
return c.json({ error: 'limit must be an integer between 1 and 100', status: 400 }, 400);
|
|
147
|
+
}
|
|
148
|
+
limit = val;
|
|
149
|
+
}
|
|
150
|
+
// Validate offset
|
|
151
|
+
let offset = 0;
|
|
152
|
+
if (offsetStr !== undefined && offsetStr !== '') {
|
|
153
|
+
const val = parseInt(offsetStr, 10);
|
|
154
|
+
if (isNaN(val) || val < 0) {
|
|
155
|
+
return c.json({ error: 'offset must be a non-negative integer', status: 400 }, 400);
|
|
156
|
+
}
|
|
157
|
+
offset = val;
|
|
158
|
+
}
|
|
159
|
+
const filters = { status, agentId, limit, offset };
|
|
160
|
+
const { benchmarks, total } = benchmarkStore.list(tenantId, filters);
|
|
161
|
+
return c.json({
|
|
162
|
+
benchmarks,
|
|
163
|
+
total,
|
|
164
|
+
hasMore: offset + benchmarks.length < total,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
// ─── GET /:id — Get benchmark detail ────────────────────
|
|
168
|
+
app.get('/:id', async (c) => {
|
|
169
|
+
const benchmarkStore = getBenchmarkStore(c);
|
|
170
|
+
if (!benchmarkStore) {
|
|
171
|
+
return c.json({ error: 'Database not available', status: 500 }, 500);
|
|
172
|
+
}
|
|
173
|
+
const tenantId = getTenantId(c);
|
|
174
|
+
const id = c.req.param('id');
|
|
175
|
+
const benchmark = benchmarkStore.getById(tenantId, id);
|
|
176
|
+
if (!benchmark) {
|
|
177
|
+
return c.json({ error: 'Benchmark not found', status: 404 }, 404);
|
|
178
|
+
}
|
|
179
|
+
// Enrich variants with session counts
|
|
180
|
+
const tenantStore = getTenantStore(store, c);
|
|
181
|
+
const variantsWithCounts = await Promise.all(benchmark.variants.map(async (v) => {
|
|
182
|
+
try {
|
|
183
|
+
const { total } = await tenantStore.querySessions({
|
|
184
|
+
tenantId: v.tenantId,
|
|
185
|
+
agentId: v.agentId,
|
|
186
|
+
tags: [v.tag],
|
|
187
|
+
from: benchmark.timeRange?.from,
|
|
188
|
+
to: benchmark.timeRange?.to,
|
|
189
|
+
limit: 1, // Minimal fetch — we only need the count
|
|
190
|
+
});
|
|
191
|
+
return { ...v, sessionCount: total };
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return { ...v, sessionCount: 0 };
|
|
195
|
+
}
|
|
196
|
+
}));
|
|
197
|
+
return c.json({
|
|
198
|
+
...benchmark,
|
|
199
|
+
variants: variantsWithCounts,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
// ─── PUT /:id/status — Transition status ────────────────
|
|
203
|
+
app.put('/:id/status', async (c) => {
|
|
204
|
+
const benchmarkStore = getBenchmarkStore(c);
|
|
205
|
+
if (!benchmarkStore) {
|
|
206
|
+
return c.json({ error: 'Database not available', status: 500 }, 500);
|
|
207
|
+
}
|
|
208
|
+
const tenantId = getTenantId(c);
|
|
209
|
+
const id = c.req.param('id');
|
|
210
|
+
const body = await c.req.json().catch(() => null);
|
|
211
|
+
if (!body || !body.status) {
|
|
212
|
+
return c.json({ error: 'status is required', status: 400 }, 400);
|
|
213
|
+
}
|
|
214
|
+
if (!VALID_STATUSES.has(body.status)) {
|
|
215
|
+
return c.json({ error: `Invalid status: ${body.status}`, status: 400 }, 400);
|
|
216
|
+
}
|
|
217
|
+
const newStatus = body.status;
|
|
218
|
+
// Get current benchmark
|
|
219
|
+
const current = benchmarkStore.getById(tenantId, id);
|
|
220
|
+
if (!current) {
|
|
221
|
+
return c.json({ error: 'Benchmark not found', status: 404 }, 404);
|
|
222
|
+
}
|
|
223
|
+
// When transitioning to "running": validate ≥1 session per variant
|
|
224
|
+
if (newStatus === 'running') {
|
|
225
|
+
const tenantStore = getTenantStore(store, c);
|
|
226
|
+
for (const v of current.variants) {
|
|
227
|
+
const { sessions } = await tenantStore.querySessions({
|
|
228
|
+
tenantId: v.tenantId,
|
|
229
|
+
agentId: v.agentId,
|
|
230
|
+
tags: [v.tag],
|
|
231
|
+
from: current.timeRange?.from,
|
|
232
|
+
to: current.timeRange?.to,
|
|
233
|
+
limit: 1,
|
|
234
|
+
});
|
|
235
|
+
if (sessions.length === 0) {
|
|
236
|
+
return c.json({
|
|
237
|
+
error: `Variant "${v.name}" has no sessions. Each variant must have at least 1 session to start.`,
|
|
238
|
+
status: 409,
|
|
239
|
+
}, 409);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const updated = benchmarkStore.updateStatus(tenantId, id, newStatus);
|
|
245
|
+
// When transitioning to "completed": compute and cache results
|
|
246
|
+
if (newStatus === 'completed') {
|
|
247
|
+
const tenantStore = getTenantStore(store, c);
|
|
248
|
+
await engine.computeResults(updated, tenantStore, benchmarkStore);
|
|
249
|
+
}
|
|
250
|
+
return c.json(updated);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
const message = err instanceof Error ? err.message : 'Failed to update status';
|
|
254
|
+
if (message.includes('Invalid status transition')) {
|
|
255
|
+
return c.json({ error: message, status: 409 }, 409);
|
|
256
|
+
}
|
|
257
|
+
return c.json({ error: message, status: 400 }, 400);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
// ─── GET /:id/results — Get comparison results ──────────
|
|
261
|
+
app.get('/:id/results', async (c) => {
|
|
262
|
+
const benchmarkStore = getBenchmarkStore(c);
|
|
263
|
+
if (!benchmarkStore) {
|
|
264
|
+
return c.json({ error: 'Database not available', status: 500 }, 500);
|
|
265
|
+
}
|
|
266
|
+
const tenantId = getTenantId(c);
|
|
267
|
+
const id = c.req.param('id');
|
|
268
|
+
const benchmark = benchmarkStore.getById(tenantId, id);
|
|
269
|
+
if (!benchmark) {
|
|
270
|
+
return c.json({ error: 'Benchmark not found', status: 404 }, 404);
|
|
271
|
+
}
|
|
272
|
+
// Draft benchmarks can't have results
|
|
273
|
+
if (benchmark.status === 'draft') {
|
|
274
|
+
return c.json({ error: 'Cannot get results for a draft benchmark. Start it first.', status: 400 }, 400);
|
|
275
|
+
}
|
|
276
|
+
const tenantStore = getTenantStore(store, c);
|
|
277
|
+
const results = await engine.computeResults(benchmark, tenantStore, benchmarkStore);
|
|
278
|
+
// Strip distributions unless requested
|
|
279
|
+
const includeDistributions = c.req.query('includeDistributions') === 'true';
|
|
280
|
+
if (!includeDistributions) {
|
|
281
|
+
for (const v of results.variants) {
|
|
282
|
+
for (const key of Object.keys(v.metrics)) {
|
|
283
|
+
const stats = v.metrics[key];
|
|
284
|
+
if (stats) {
|
|
285
|
+
delete stats.values;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return c.json(results);
|
|
291
|
+
});
|
|
292
|
+
// ─── DELETE /:id — Delete benchmark ─────────────────────
|
|
293
|
+
app.delete('/:id', async (c) => {
|
|
294
|
+
const benchmarkStore = getBenchmarkStore(c);
|
|
295
|
+
if (!benchmarkStore) {
|
|
296
|
+
return c.json({ error: 'Database not available', status: 500 }, 500);
|
|
297
|
+
}
|
|
298
|
+
const tenantId = getTenantId(c);
|
|
299
|
+
const id = c.req.param('id');
|
|
300
|
+
const benchmark = benchmarkStore.getById(tenantId, id);
|
|
301
|
+
if (!benchmark) {
|
|
302
|
+
return c.json({ error: 'Benchmark not found', status: 404 }, 404);
|
|
303
|
+
}
|
|
304
|
+
if (benchmark.status === 'running' || benchmark.status === 'completed') {
|
|
305
|
+
return c.json({ error: `Cannot delete a ${benchmark.status} benchmark`, status: 409 }, 409);
|
|
306
|
+
}
|
|
307
|
+
benchmarkStore.delete(tenantId, id);
|
|
308
|
+
return c.body(null, 204);
|
|
309
|
+
});
|
|
310
|
+
return app;
|
|
311
|
+
}
|
|
312
|
+
//# sourceMappingURL=benchmarks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"benchmarks.js","sourceRoot":"","sources":["../../src/routes/benchmarks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay REST Endpoint (Stories 2.2, 2.3)
|
|
3
|
+
*
|
|
4
|
+
* GET /api/sessions/:id/replay — Returns ReplayState for session replay UI.
|
|
5
|
+
*
|
|
6
|
+
* Supports pagination, event type filtering, and server-side LRU caching.
|
|
7
|
+
*/
|
|
8
|
+
import { Hono } from 'hono';
|
|
9
|
+
import type { IEventStore, ReplayState } from '@agentlensai/core';
|
|
10
|
+
import type { AuthVariables } from '../middleware/auth.js';
|
|
11
|
+
interface CacheEntry {
|
|
12
|
+
state: ReplayState;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
}
|
|
15
|
+
/** Simple Map-based LRU cache with TTL. Exported for testing. */
|
|
16
|
+
export declare const replayCache: Map<string, CacheEntry>;
|
|
17
|
+
/**
|
|
18
|
+
* Register the replay route directly on the provided Hono app.
|
|
19
|
+
* Path: GET /api/sessions/:id/replay
|
|
20
|
+
*
|
|
21
|
+
* Uses `registerReplayRoutes(app, store)` pattern (like health routes)
|
|
22
|
+
* since the path nests under /api/sessions which already has auth middleware.
|
|
23
|
+
*/
|
|
24
|
+
export declare function registerReplayRoutes(app: Hono<{
|
|
25
|
+
Variables: AuthVariables;
|
|
26
|
+
}>, store: IEventStore): void;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=replay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../../src/routes/replay.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAa,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAU3D,UAAU,UAAU;IAClB,KAAK,EAAE,WAAW,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,iEAAiE;AACjE,eAAO,MAAM,WAAW,yBAAgC,CAAC;AAmDzD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,IAAI,CAAC;IAAE,SAAS,EAAE,aAAa,CAAA;CAAE,CAAC,EACvC,KAAK,EAAE,WAAW,GACjB,IAAI,CAgGN"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay REST Endpoint (Stories 2.2, 2.3)
|
|
3
|
+
*
|
|
4
|
+
* GET /api/sessions/:id/replay — Returns ReplayState for session replay UI.
|
|
5
|
+
*
|
|
6
|
+
* Supports pagination, event type filtering, and server-side LRU caching.
|
|
7
|
+
*/
|
|
8
|
+
import { Hono } from 'hono';
|
|
9
|
+
import { EVENT_TYPES } from '@agentlensai/core';
|
|
10
|
+
import { getTenantStore } from './tenant-helper.js';
|
|
11
|
+
import { ReplayBuilder } from '../lib/replay/builder.js';
|
|
12
|
+
// ─── LRU Cache (Story 2.3) ────────────────────────────────
|
|
13
|
+
const MAX_CACHE_SIZE = 100;
|
|
14
|
+
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
15
|
+
const LLM_HISTORY_CAP = 50;
|
|
16
|
+
/** Simple Map-based LRU cache with TTL. Exported for testing. */
|
|
17
|
+
export const replayCache = new Map();
|
|
18
|
+
function cacheKey(tenantId, sessionId) {
|
|
19
|
+
return `${tenantId}:${sessionId}`;
|
|
20
|
+
}
|
|
21
|
+
function getCached(tenantId, sessionId) {
|
|
22
|
+
const key = cacheKey(tenantId, sessionId);
|
|
23
|
+
const entry = replayCache.get(key);
|
|
24
|
+
if (!entry)
|
|
25
|
+
return null;
|
|
26
|
+
// TTL check
|
|
27
|
+
if (Date.now() - entry.createdAt > CACHE_TTL_MS) {
|
|
28
|
+
replayCache.delete(key);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
// Move to end (most-recently-used)
|
|
32
|
+
replayCache.delete(key);
|
|
33
|
+
replayCache.set(key, entry);
|
|
34
|
+
return entry.state;
|
|
35
|
+
}
|
|
36
|
+
function putCache(tenantId, sessionId, state) {
|
|
37
|
+
const key = cacheKey(tenantId, sessionId);
|
|
38
|
+
// Evict if at capacity (remove oldest = first entry)
|
|
39
|
+
if (replayCache.size >= MAX_CACHE_SIZE && !replayCache.has(key)) {
|
|
40
|
+
const firstKey = replayCache.keys().next().value;
|
|
41
|
+
if (firstKey !== undefined) {
|
|
42
|
+
replayCache.delete(firstKey);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
replayCache.set(key, { state, createdAt: Date.now() });
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Cap LLM history in context to the last N entries (memory guard).
|
|
49
|
+
*/
|
|
50
|
+
function capLlmHistory(state) {
|
|
51
|
+
for (const step of state.steps) {
|
|
52
|
+
if (step.context.llmHistory.length > LLM_HISTORY_CAP) {
|
|
53
|
+
step.context.llmHistory = step.context.llmHistory.slice(-LLM_HISTORY_CAP);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return state;
|
|
57
|
+
}
|
|
58
|
+
// ─── Route Registration ────────────────────────────────────
|
|
59
|
+
/**
|
|
60
|
+
* Register the replay route directly on the provided Hono app.
|
|
61
|
+
* Path: GET /api/sessions/:id/replay
|
|
62
|
+
*
|
|
63
|
+
* Uses `registerReplayRoutes(app, store)` pattern (like health routes)
|
|
64
|
+
* since the path nests under /api/sessions which already has auth middleware.
|
|
65
|
+
*/
|
|
66
|
+
export function registerReplayRoutes(app, store) {
|
|
67
|
+
app.get('/api/sessions/:id/replay', async (c) => {
|
|
68
|
+
const tenantStore = getTenantStore(store, c);
|
|
69
|
+
const sessionId = c.req.param('id');
|
|
70
|
+
// ── Parse & validate query params ──
|
|
71
|
+
// offset
|
|
72
|
+
const offsetStr = c.req.query('offset');
|
|
73
|
+
let offset = 0;
|
|
74
|
+
if (offsetStr !== undefined && offsetStr !== '') {
|
|
75
|
+
const parsed = parseInt(offsetStr, 10);
|
|
76
|
+
if (isNaN(parsed) || parsed < 0) {
|
|
77
|
+
return c.json({ error: 'Invalid offset: must be a non-negative integer', status: 400 }, 400);
|
|
78
|
+
}
|
|
79
|
+
offset = parsed;
|
|
80
|
+
}
|
|
81
|
+
// limit
|
|
82
|
+
const limitStr = c.req.query('limit');
|
|
83
|
+
let limit = 1000;
|
|
84
|
+
if (limitStr !== undefined && limitStr !== '') {
|
|
85
|
+
const parsed = parseInt(limitStr, 10);
|
|
86
|
+
if (isNaN(parsed) || parsed < 1 || parsed > 5000) {
|
|
87
|
+
return c.json({ error: 'Invalid limit: must be an integer between 1 and 5000', status: 400 }, 400);
|
|
88
|
+
}
|
|
89
|
+
limit = parsed;
|
|
90
|
+
}
|
|
91
|
+
// eventTypes
|
|
92
|
+
const eventTypesStr = c.req.query('eventTypes');
|
|
93
|
+
let eventTypes;
|
|
94
|
+
if (eventTypesStr !== undefined && eventTypesStr !== '') {
|
|
95
|
+
const types = eventTypesStr.split(',').map((s) => s.trim());
|
|
96
|
+
const validTypes = new Set(EVENT_TYPES);
|
|
97
|
+
for (const t of types) {
|
|
98
|
+
if (!validTypes.has(t)) {
|
|
99
|
+
return c.json({ error: `Invalid event type: ${t}`, status: 400 }, 400);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
eventTypes = types;
|
|
103
|
+
}
|
|
104
|
+
// includeContext
|
|
105
|
+
const includeContextStr = c.req.query('includeContext');
|
|
106
|
+
let includeContext = true;
|
|
107
|
+
if (includeContextStr !== undefined && includeContextStr !== '') {
|
|
108
|
+
if (includeContextStr === 'false' || includeContextStr === '0') {
|
|
109
|
+
includeContext = false;
|
|
110
|
+
}
|
|
111
|
+
else if (includeContextStr !== 'true' && includeContextStr !== '1') {
|
|
112
|
+
return c.json({ error: 'Invalid includeContext: must be true or false', status: 400 }, 400);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const builder = new ReplayBuilder(tenantStore);
|
|
117
|
+
const state = await builder.build(sessionId, {
|
|
118
|
+
offset,
|
|
119
|
+
limit,
|
|
120
|
+
eventTypes,
|
|
121
|
+
includeContext,
|
|
122
|
+
});
|
|
123
|
+
if (!state) {
|
|
124
|
+
return c.json({ error: 'Session not found', status: 404 }, 404);
|
|
125
|
+
}
|
|
126
|
+
// Apply memory guard: cap LLM history
|
|
127
|
+
capLlmHistory(state);
|
|
128
|
+
// Cache the state (LLM history has been capped for memory efficiency)
|
|
129
|
+
const apiKeyInfo = c.get('apiKey');
|
|
130
|
+
const tenantId = apiKeyInfo?.tenantId ?? 'default';
|
|
131
|
+
putCache(tenantId, sessionId, state);
|
|
132
|
+
return c.json(state);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const message = error instanceof Error ? error.message : 'Internal error';
|
|
136
|
+
return c.json({ error: message, status: 500 }, 500);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../../src/routes/replay.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentlensai/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "AgentLens API server — event ingestion, querying, dashboard, and alerting for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"dev": "tsx watch src/index.ts"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@agentlensai/core": "
|
|
48
|
+
"@agentlensai/core": "^0.7.0",
|
|
49
49
|
"drizzle-orm": "^0.44.2",
|
|
50
50
|
"better-sqlite3": "^11.9.1",
|
|
51
51
|
"hono": "^4.7.10",
|