Repository navigation
Expand file tree
/
Copy pathbundle.ts
More file actions
93 lines (88 loc) · 3.45 KB
/
Copy pathbundle.ts
File metadata and controls
93 lines (88 loc) · 3.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
* HTTP handlers for the Compare-runs UI's bundle endpoints. A bundle is
* a set of RunRecords sharing a `bundleId`, produced by one Quickstart
* submission. The dashboard's CompareModal fetches:
*
* - GET /api/v1/bundles/:id -> bundle metadata + member runs
* - GET /api/v1/bundles/:id/aggregate -> server-side rollup (counts, cost)
*
* Lazy-loading: members in the listBundle response carry only the
* RunRecord summary (cost, duration, actorName, summaryTrajectory).
* Full RunArtifact JSON is fetched per-cell via the existing
* /api/v1/runs/:id endpoint when a cell is pinned or opened.
*
* @module paracosm/cli/bundle-routes
*/
import type { ServerResponse } from 'node:http';
import type { RunHistoryStore } from '../stores/run-history.js';
import type { RunRecord } from '../services/run-record.js';
export interface BundleRoutesDeps {
runHistoryStore: RunHistoryStore;
}
export async function handleListBundle(
bundleId: string,
res: ServerResponse,
deps: BundleRoutesDeps,
): Promise<void> {
if (!deps.runHistoryStore.listRunsByBundleId) {
res.writeHead(501, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'bundle queries not supported by this store' }));
return;
}
const members = await deps.runHistoryStore.listRunsByBundleId(bundleId);
if (members.length === 0) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: `Bundle ${bundleId} has no members` }));
return;
}
const scenarioId = members[0].scenarioId;
const createdAt = members[0].createdAt;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
bundleId,
scenarioId,
createdAt,
memberCount: members.length,
members,
}));
}
export interface BundleAggregate {
bundleId: string;
count: number;
costTotalUSD: number;
meanDurationMs: number;
outcomeBuckets: Record<string, number>;
}
export async function handleBundleAggregate(
bundleId: string,
res: ServerResponse,
deps: BundleRoutesDeps,
): Promise<void> {
if (!deps.runHistoryStore.listRunsByBundleId) {
res.writeHead(501, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'bundle queries not supported' }));
return;
}
const members = await deps.runHistoryStore.listRunsByBundleId(bundleId);
if (members.length === 0) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: `Bundle ${bundleId} has no members` }));
return;
}
const aggregate = computeAggregate(bundleId, members);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(aggregate));
}
export function computeAggregate(bundleId: string, members: RunRecord[]): BundleAggregate {
const costTotalUSD = members.reduce((sum, m) => sum + (m.costUSD ?? 0), 0);
const durations = members.map(m => m.durationMs ?? 0).filter(d => d > 0);
const meanDurationMs = durations.length > 0
? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length)
: 0;
// Outcome buckets are populated when the artifact's fingerprint or a
// dedicated column carries the outcome class. RunRecord alone does
// not, so v1 returns an empty bucket map -- the AggregateStrip's
// outcome chart treats empty buckets as "data unavailable" gracefully.
const outcomeBuckets: Record<string, number> = {};
return { bundleId, count: members.length, costTotalUSD, meanDurationMs, outcomeBuckets };
}