@agentlensai/server 0.5.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/health-snapshot-store.d.ts +33 -0
- package/dist/db/health-snapshot-store.d.ts.map +1 -0
- package/dist/db/health-snapshot-store.js +112 -0
- package/dist/db/health-snapshot-store.js.map +1 -0
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +67 -0
- package/dist/db/migrate.js.map +1 -1
- package/dist/db/sqlite-store.d.ts +5 -0
- package/dist/db/sqlite-store.d.ts.map +1 -1
- package/dist/db/sqlite-store.js +15 -0
- package/dist/db/sqlite-store.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/alert-engine.d.ts.map +1 -1
- package/dist/lib/alert-engine.js +5 -2
- package/dist/lib/alert-engine.js.map +1 -1
- package/dist/lib/analysis/cost-analysis.d.ts.map +1 -1
- package/dist/lib/analysis/cost-analysis.js +5 -2
- package/dist/lib/analysis/cost-analysis.js.map +1 -1
- package/dist/lib/analysis/error-patterns.d.ts.map +1 -1
- package/dist/lib/analysis/error-patterns.js +5 -2
- package/dist/lib/analysis/error-patterns.js.map +1 -1
- package/dist/lib/analysis/performance-trends.d.ts.map +1 -1
- package/dist/lib/analysis/performance-trends.js +4 -1
- package/dist/lib/analysis/performance-trends.js.map +1 -1
- package/dist/lib/analysis/tool-sequences.d.ts.map +1 -1
- package/dist/lib/analysis/tool-sequences.js +5 -2
- package/dist/lib/analysis/tool-sequences.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/context/retrieval.d.ts +4 -0
- package/dist/lib/context/retrieval.d.ts.map +1 -1
- package/dist/lib/context/retrieval.js +4 -0
- package/dist/lib/context/retrieval.js.map +1 -1
- package/dist/lib/embeddings/local.js +2 -2
- package/dist/lib/embeddings/local.js.map +1 -1
- package/dist/lib/health/computer.d.ts +28 -0
- package/dist/lib/health/computer.d.ts.map +1 -0
- package/dist/lib/health/computer.js +270 -0
- package/dist/lib/health/computer.js.map +1 -0
- package/dist/lib/optimization/classifier.d.ts +34 -0
- package/dist/lib/optimization/classifier.d.ts.map +1 -0
- package/dist/lib/optimization/classifier.js +108 -0
- package/dist/lib/optimization/classifier.js.map +1 -0
- package/dist/lib/optimization/engine.d.ts +24 -0
- package/dist/lib/optimization/engine.d.ts.map +1 -0
- package/dist/lib/optimization/engine.js +202 -0
- package/dist/lib/optimization/engine.js.map +1 -0
- package/dist/lib/optimization/index.d.ts +10 -0
- package/dist/lib/optimization/index.d.ts.map +1 -0
- package/dist/lib/optimization/index.js +9 -0
- package/dist/lib/optimization/index.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/health.d.ts +21 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +142 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/optimize.d.ts +15 -0
- package/dist/routes/optimize.d.ts.map +1 -0
- package/dist/routes/optimize.js +55 -0
- package/dist/routes/optimize.js.map +1 -0
- package/dist/routes/recall.d.ts +2 -0
- package/dist/routes/recall.d.ts.map +1 -1
- package/dist/routes/recall.js +21 -1
- package/dist/routes/recall.js.map +1 -1
- package/dist/routes/reflect.d.ts.map +1 -1
- package/dist/routes/reflect.js +0 -1
- package/dist/routes/reflect.js.map +1 -1
- 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/dist/routes/tenant-helper.d.ts.map +1 -1
- package/dist/routes/tenant-helper.js +12 -0
- package/dist/routes/tenant-helper.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Score Computer (Story 1.2)
|
|
3
|
+
*
|
|
4
|
+
* Computes health scores for agents based on five dimensions:
|
|
5
|
+
* error rate, cost efficiency, tool success, latency, and completion rate.
|
|
6
|
+
* Each dimension is normalized to 0-100 and combined with configurable weights.
|
|
7
|
+
*/
|
|
8
|
+
/** Clamp a value between min and max */
|
|
9
|
+
function clamp(value, min, max) {
|
|
10
|
+
return Math.max(min, Math.min(max, value));
|
|
11
|
+
}
|
|
12
|
+
/** Get an ISO date string N days ago from a reference date */
|
|
13
|
+
function daysAgo(days, from = new Date()) {
|
|
14
|
+
const d = new Date(from);
|
|
15
|
+
d.setDate(d.getDate() - days);
|
|
16
|
+
return d.toISOString();
|
|
17
|
+
}
|
|
18
|
+
export class HealthComputer {
|
|
19
|
+
weights;
|
|
20
|
+
constructor(weights) {
|
|
21
|
+
this.weights = weights;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compute the health score for a single agent within a time window.
|
|
25
|
+
* Returns null if no sessions exist in the window.
|
|
26
|
+
*/
|
|
27
|
+
async compute(store, agentId, windowDays) {
|
|
28
|
+
const now = new Date();
|
|
29
|
+
const windowFrom = daysAgo(windowDays, now);
|
|
30
|
+
const windowTo = now.toISOString();
|
|
31
|
+
// Query sessions in the current window
|
|
32
|
+
const { sessions } = await store.querySessions({
|
|
33
|
+
agentId,
|
|
34
|
+
from: windowFrom,
|
|
35
|
+
to: windowTo,
|
|
36
|
+
limit: 10000,
|
|
37
|
+
});
|
|
38
|
+
if (sessions.length === 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
// Query sessions for the 30-day baseline (excluding current window)
|
|
42
|
+
const baselineFrom = daysAgo(30, now);
|
|
43
|
+
const baselineTo = daysAgo(windowDays, now);
|
|
44
|
+
const { sessions: baselineSessions } = await store.querySessions({
|
|
45
|
+
agentId,
|
|
46
|
+
from: baselineFrom,
|
|
47
|
+
to: baselineTo,
|
|
48
|
+
limit: 10000,
|
|
49
|
+
});
|
|
50
|
+
// Query tool events in the current window
|
|
51
|
+
const { events: toolEvents } = await store.queryEvents({
|
|
52
|
+
agentId,
|
|
53
|
+
from: windowFrom,
|
|
54
|
+
to: windowTo,
|
|
55
|
+
eventType: ['tool_call', 'tool_response', 'tool_error'],
|
|
56
|
+
limit: 10000,
|
|
57
|
+
});
|
|
58
|
+
// Compute each dimension
|
|
59
|
+
const errorRateDim = this.computeErrorRate(sessions);
|
|
60
|
+
const costEfficiencyDim = this.computeCostEfficiency(sessions, baselineSessions);
|
|
61
|
+
const toolSuccessDim = this.computeToolSuccess(toolEvents);
|
|
62
|
+
const latencyDim = this.computeLatency(sessions, baselineSessions);
|
|
63
|
+
const completionRateDim = this.computeCompletionRate(sessions);
|
|
64
|
+
const dimensions = [
|
|
65
|
+
errorRateDim,
|
|
66
|
+
costEfficiencyDim,
|
|
67
|
+
toolSuccessDim,
|
|
68
|
+
latencyDim,
|
|
69
|
+
completionRateDim,
|
|
70
|
+
];
|
|
71
|
+
const rawOverallScore = errorRateDim.score * this.weights.errorRate +
|
|
72
|
+
costEfficiencyDim.score * this.weights.costEfficiency +
|
|
73
|
+
toolSuccessDim.score * this.weights.toolSuccess +
|
|
74
|
+
latencyDim.score * this.weights.latency +
|
|
75
|
+
completionRateDim.score * this.weights.completionRate;
|
|
76
|
+
const overallScore = clamp(rawOverallScore, 0, 100);
|
|
77
|
+
// Compute trend by comparing against previous window
|
|
78
|
+
const trend = await this.computeTrend(store, agentId, windowDays, overallScore, now);
|
|
79
|
+
return {
|
|
80
|
+
agentId,
|
|
81
|
+
overallScore: Math.round(overallScore * 100) / 100,
|
|
82
|
+
trend: trend.direction,
|
|
83
|
+
trendDelta: trend.delta,
|
|
84
|
+
dimensions,
|
|
85
|
+
window: { from: windowFrom, to: windowTo },
|
|
86
|
+
sessionCount: sessions.length,
|
|
87
|
+
computedAt: now.toISOString(),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Compute health scores for all agents in the store.
|
|
92
|
+
*/
|
|
93
|
+
async computeOverview(store, windowDays) {
|
|
94
|
+
const agents = await store.listAgents();
|
|
95
|
+
const results = [];
|
|
96
|
+
for (const agent of agents) {
|
|
97
|
+
const score = await this.compute(store, agent.id, windowDays);
|
|
98
|
+
if (score !== null) {
|
|
99
|
+
results.push(score);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return results;
|
|
103
|
+
}
|
|
104
|
+
// ─── Dimension Calculations ─────────────────────────────
|
|
105
|
+
computeErrorRate(sessions) {
|
|
106
|
+
const total = sessions.length;
|
|
107
|
+
const withErrors = sessions.filter((s) => s.errorCount > 0).length;
|
|
108
|
+
const errorRate = total > 0 ? withErrors / total : 0;
|
|
109
|
+
const score = (1 - errorRate) * 100;
|
|
110
|
+
return {
|
|
111
|
+
name: 'error_rate',
|
|
112
|
+
score: Math.round(score * 100) / 100,
|
|
113
|
+
weight: this.weights.errorRate,
|
|
114
|
+
rawValue: Math.round(errorRate * 10000) / 10000,
|
|
115
|
+
description: `${withErrors}/${total} sessions had errors`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
computeCostEfficiency(windowSessions, baselineSessions) {
|
|
119
|
+
const windowTotal = windowSessions.reduce((sum, s) => sum + s.totalCostUsd, 0);
|
|
120
|
+
const windowAvg = windowSessions.length > 0 ? windowTotal / windowSessions.length : 0;
|
|
121
|
+
const baselineTotal = baselineSessions.reduce((sum, s) => sum + s.totalCostUsd, 0);
|
|
122
|
+
const baselineAvg = baselineSessions.length > 0 ? baselineTotal / baselineSessions.length : 0;
|
|
123
|
+
let score;
|
|
124
|
+
if (baselineAvg === 0 || windowAvg === 0) {
|
|
125
|
+
// No cost data or no baseline — neutral score
|
|
126
|
+
score = 100;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const ratio = windowAvg / baselineAvg;
|
|
130
|
+
score = clamp(100 - (ratio - 1) * 100, 0, 100);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
name: 'cost_efficiency',
|
|
134
|
+
score: Math.round(score * 100) / 100,
|
|
135
|
+
weight: this.weights.costEfficiency,
|
|
136
|
+
rawValue: Math.round(windowAvg * 1000000) / 1000000,
|
|
137
|
+
description: `Avg cost per session: $${windowAvg.toFixed(4)}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
computeToolSuccess(toolEvents) {
|
|
141
|
+
// Separate tool_call, tool_response, and tool_error events
|
|
142
|
+
const toolCalls = toolEvents.filter((e) => e.eventType === 'tool_call');
|
|
143
|
+
const toolResponses = toolEvents.filter((e) => e.eventType === 'tool_response');
|
|
144
|
+
const toolErrors = toolEvents.filter((e) => e.eventType === 'tool_error');
|
|
145
|
+
const totalCalls = toolCalls.length;
|
|
146
|
+
if (totalCalls === 0) {
|
|
147
|
+
return {
|
|
148
|
+
name: 'tool_success',
|
|
149
|
+
score: 100,
|
|
150
|
+
weight: this.weights.toolSuccess,
|
|
151
|
+
rawValue: 1,
|
|
152
|
+
description: 'No tool calls in window',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// Count failed tool responses (isError=true in payload) + tool_error events
|
|
156
|
+
const failedResponses = toolResponses.filter((e) => {
|
|
157
|
+
const payload = e.payload;
|
|
158
|
+
return payload.isError === true;
|
|
159
|
+
}).length;
|
|
160
|
+
const failedCalls = failedResponses + toolErrors.length;
|
|
161
|
+
const successRate = (totalCalls - failedCalls) / totalCalls;
|
|
162
|
+
const score = successRate * 100;
|
|
163
|
+
return {
|
|
164
|
+
name: 'tool_success',
|
|
165
|
+
score: Math.round(score * 100) / 100,
|
|
166
|
+
weight: this.weights.toolSuccess,
|
|
167
|
+
rawValue: Math.round(successRate * 10000) / 10000,
|
|
168
|
+
description: `${totalCalls - failedCalls}/${totalCalls} tool calls succeeded`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
computeLatency(windowSessions, baselineSessions) {
|
|
172
|
+
// Compute average duration for sessions with endedAt
|
|
173
|
+
const windowDurations = windowSessions
|
|
174
|
+
.filter((s) => s.endedAt)
|
|
175
|
+
.map((s) => new Date(s.endedAt).getTime() - new Date(s.startedAt).getTime());
|
|
176
|
+
const avgDuration = windowDurations.length > 0
|
|
177
|
+
? windowDurations.reduce((a, b) => a + b, 0) / windowDurations.length
|
|
178
|
+
: 0;
|
|
179
|
+
const baselineDurations = baselineSessions
|
|
180
|
+
.filter((s) => s.endedAt)
|
|
181
|
+
.map((s) => new Date(s.endedAt).getTime() - new Date(s.startedAt).getTime());
|
|
182
|
+
const baselineDuration = baselineDurations.length > 0
|
|
183
|
+
? baselineDurations.reduce((a, b) => a + b, 0) / baselineDurations.length
|
|
184
|
+
: 0;
|
|
185
|
+
let score;
|
|
186
|
+
if (baselineDuration === 0 || avgDuration === 0) {
|
|
187
|
+
score = 100;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
const ratio = avgDuration / baselineDuration;
|
|
191
|
+
score = clamp(100 - (ratio - 1) * 50, 0, 100);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
name: 'latency',
|
|
195
|
+
score: Math.round(score * 100) / 100,
|
|
196
|
+
weight: this.weights.latency,
|
|
197
|
+
rawValue: Math.round(avgDuration),
|
|
198
|
+
description: `Avg session duration: ${Math.round(avgDuration)}ms`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
computeCompletionRate(sessions) {
|
|
202
|
+
const total = sessions.length;
|
|
203
|
+
const completed = sessions.filter((s) => s.status === 'completed').length;
|
|
204
|
+
const rate = total > 0 ? completed / total : 0;
|
|
205
|
+
const score = rate * 100;
|
|
206
|
+
return {
|
|
207
|
+
name: 'completion_rate',
|
|
208
|
+
score: Math.round(score * 100) / 100,
|
|
209
|
+
weight: this.weights.completionRate,
|
|
210
|
+
rawValue: Math.round(rate * 10000) / 10000,
|
|
211
|
+
description: `${completed}/${total} sessions completed`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// ─── Trend ──────────────────────────────────────────────
|
|
215
|
+
async computeTrend(store, agentId, windowDays, currentScore, now) {
|
|
216
|
+
// Previous window: windowDays to 2*windowDays ago
|
|
217
|
+
const prevFrom = daysAgo(windowDays * 2, now);
|
|
218
|
+
const prevTo = daysAgo(windowDays, now);
|
|
219
|
+
const { sessions: prevSessions } = await store.querySessions({
|
|
220
|
+
agentId,
|
|
221
|
+
from: prevFrom,
|
|
222
|
+
to: prevTo,
|
|
223
|
+
limit: 10000,
|
|
224
|
+
});
|
|
225
|
+
if (prevSessions.length === 0) {
|
|
226
|
+
// No previous data — stable
|
|
227
|
+
return { direction: 'stable', delta: 0 };
|
|
228
|
+
}
|
|
229
|
+
// Query 30-day baseline for previous window
|
|
230
|
+
const baselineFrom = daysAgo(30, now);
|
|
231
|
+
const { sessions: baselineSessions } = await store.querySessions({
|
|
232
|
+
agentId,
|
|
233
|
+
from: baselineFrom,
|
|
234
|
+
to: prevTo,
|
|
235
|
+
limit: 10000,
|
|
236
|
+
});
|
|
237
|
+
// Query tool events for previous window
|
|
238
|
+
const { events: prevToolEvents } = await store.queryEvents({
|
|
239
|
+
agentId,
|
|
240
|
+
from: prevFrom,
|
|
241
|
+
to: prevTo,
|
|
242
|
+
eventType: ['tool_call', 'tool_response', 'tool_error'],
|
|
243
|
+
limit: 10000,
|
|
244
|
+
});
|
|
245
|
+
// Compute previous window dimensions
|
|
246
|
+
const prevErrorRate = this.computeErrorRate(prevSessions);
|
|
247
|
+
const prevCostEff = this.computeCostEfficiency(prevSessions, baselineSessions);
|
|
248
|
+
const prevToolSuccess = this.computeToolSuccess(prevToolEvents);
|
|
249
|
+
const prevLatency = this.computeLatency(prevSessions, baselineSessions);
|
|
250
|
+
const prevCompletion = this.computeCompletionRate(prevSessions);
|
|
251
|
+
const prevOverall = prevErrorRate.score * this.weights.errorRate +
|
|
252
|
+
prevCostEff.score * this.weights.costEfficiency +
|
|
253
|
+
prevToolSuccess.score * this.weights.toolSuccess +
|
|
254
|
+
prevLatency.score * this.weights.latency +
|
|
255
|
+
prevCompletion.score * this.weights.completionRate;
|
|
256
|
+
const delta = Math.round((currentScore - prevOverall) * 100) / 100;
|
|
257
|
+
let direction;
|
|
258
|
+
if (delta > 5) {
|
|
259
|
+
direction = 'improving';
|
|
260
|
+
}
|
|
261
|
+
else if (delta < -5) {
|
|
262
|
+
direction = 'degrading';
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
direction = 'stable';
|
|
266
|
+
}
|
|
267
|
+
return { direction, delta };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
//# sourceMappingURL=computer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"computer.js","sourceRoot":"","sources":["../../../src/lib/health/computer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complexity Classifier (Story 2.2)
|
|
3
|
+
*
|
|
4
|
+
* Classifies LLM calls into complexity tiers based on token usage
|
|
5
|
+
* and tool call count. Used by the optimization engine to group
|
|
6
|
+
* calls for model-downgrade recommendations.
|
|
7
|
+
*/
|
|
8
|
+
import type { AgentLensEvent, ComplexityTier } from '@agentlensai/core';
|
|
9
|
+
export interface ClassificationSignals {
|
|
10
|
+
inputTokens: number;
|
|
11
|
+
outputTokens: number;
|
|
12
|
+
toolCallCount: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ClassificationResult {
|
|
15
|
+
tier: ComplexityTier;
|
|
16
|
+
signals: ClassificationSignals;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Classify an LLM call's complexity based on token usage and tool calls.
|
|
20
|
+
*
|
|
21
|
+
* Thresholds:
|
|
22
|
+
* Simple: <500 input tokens AND 0 tool calls
|
|
23
|
+
* Moderate: 500-2000 input tokens OR 1-3 tool calls
|
|
24
|
+
* Complex: >2000 input tokens OR 4+ tool calls
|
|
25
|
+
*
|
|
26
|
+
* When both input tokens and tool call count are unknown (null/undefined),
|
|
27
|
+
* the function defaults to 'moderate' as the safest assumption.
|
|
28
|
+
*
|
|
29
|
+
* @param callEvent - The llm_call event
|
|
30
|
+
* @param responseEvent - The paired llm_response event (optional)
|
|
31
|
+
* @returns Classification result with tier and signals used
|
|
32
|
+
*/
|
|
33
|
+
export declare function classifyCallComplexity(callEvent: AgentLensEvent, responseEvent?: AgentLensEvent | null): ClassificationResult;
|
|
34
|
+
//# sourceMappingURL=classifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../../../src/lib/optimization/classifier.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAsC,MAAM,mBAAmB,CAAC;AAE5G,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,cAAc,EACzB,aAAa,CAAC,EAAE,cAAc,GAAG,IAAI,GACpC,oBAAoB,CAkBtB"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complexity Classifier (Story 2.2)
|
|
3
|
+
*
|
|
4
|
+
* Classifies LLM calls into complexity tiers based on token usage
|
|
5
|
+
* and tool call count. Used by the optimization engine to group
|
|
6
|
+
* calls for model-downgrade recommendations.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Classify an LLM call's complexity based on token usage and tool calls.
|
|
10
|
+
*
|
|
11
|
+
* Thresholds:
|
|
12
|
+
* Simple: <500 input tokens AND 0 tool calls
|
|
13
|
+
* Moderate: 500-2000 input tokens OR 1-3 tool calls
|
|
14
|
+
* Complex: >2000 input tokens OR 4+ tool calls
|
|
15
|
+
*
|
|
16
|
+
* When both input tokens and tool call count are unknown (null/undefined),
|
|
17
|
+
* the function defaults to 'moderate' as the safest assumption.
|
|
18
|
+
*
|
|
19
|
+
* @param callEvent - The llm_call event
|
|
20
|
+
* @param responseEvent - The paired llm_response event (optional)
|
|
21
|
+
* @returns Classification result with tier and signals used
|
|
22
|
+
*/
|
|
23
|
+
export function classifyCallComplexity(callEvent, responseEvent) {
|
|
24
|
+
const callPayload = callEvent.payload;
|
|
25
|
+
const responsePayload = responseEvent?.payload;
|
|
26
|
+
// Extract signals from whichever source is available
|
|
27
|
+
const inputTokens = extractInputTokens(callPayload, responsePayload);
|
|
28
|
+
const outputTokens = extractOutputTokens(responsePayload);
|
|
29
|
+
const toolCallCount = extractToolCallCount(callPayload, responsePayload);
|
|
30
|
+
const signals = {
|
|
31
|
+
inputTokens: inputTokens ?? 0,
|
|
32
|
+
outputTokens: outputTokens ?? 0,
|
|
33
|
+
toolCallCount: toolCallCount ?? 0,
|
|
34
|
+
};
|
|
35
|
+
const tier = determineTier(inputTokens, toolCallCount);
|
|
36
|
+
return { tier, signals };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Extract input token count from call or response payload.
|
|
40
|
+
* Returns null if unavailable from either source.
|
|
41
|
+
*/
|
|
42
|
+
function extractInputTokens(callPayload, responsePayload) {
|
|
43
|
+
// Prefer response usage (actual) over call usage (estimated)
|
|
44
|
+
const fromResponse = responsePayload?.usage?.inputTokens;
|
|
45
|
+
if (fromResponse != null && fromResponse >= 0)
|
|
46
|
+
return fromResponse;
|
|
47
|
+
// Some implementations attach usage directly on the call payload
|
|
48
|
+
const callWithUsage = callPayload;
|
|
49
|
+
const usage = callWithUsage.usage;
|
|
50
|
+
if (usage?.inputTokens != null && usage.inputTokens >= 0)
|
|
51
|
+
return usage.inputTokens;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extract output token count from response payload.
|
|
56
|
+
*/
|
|
57
|
+
function extractOutputTokens(responsePayload) {
|
|
58
|
+
const fromResponse = responsePayload?.usage?.outputTokens;
|
|
59
|
+
if (fromResponse != null && fromResponse >= 0)
|
|
60
|
+
return fromResponse;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Extract tool call count.
|
|
65
|
+
* Checks: call payload's tools (definitions provided), then response payload's
|
|
66
|
+
* toolCalls (actual invocations). Uses whichever is available.
|
|
67
|
+
*/
|
|
68
|
+
function extractToolCallCount(callPayload, responsePayload) {
|
|
69
|
+
// Response toolCalls = actual tool invocations (preferred)
|
|
70
|
+
if (responsePayload?.toolCalls != null) {
|
|
71
|
+
return responsePayload.toolCalls.length;
|
|
72
|
+
}
|
|
73
|
+
// No actual tool calls in response — default to 0
|
|
74
|
+
// (Don't fall back to tools definitions count, which is available tools, not invocations)
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Determine tier from extracted signals.
|
|
79
|
+
* If both inputs are null, defaults to 'moderate' (safe fallback).
|
|
80
|
+
*/
|
|
81
|
+
function determineTier(inputTokens, toolCallCount) {
|
|
82
|
+
const tokensKnown = inputTokens != null;
|
|
83
|
+
const toolsKnown = toolCallCount != null;
|
|
84
|
+
// If we have no data at all, default to moderate
|
|
85
|
+
if (!tokensKnown && !toolsKnown) {
|
|
86
|
+
return 'moderate';
|
|
87
|
+
}
|
|
88
|
+
// Check complex thresholds first (most restrictive)
|
|
89
|
+
if (tokensKnown && inputTokens > 2000)
|
|
90
|
+
return 'complex';
|
|
91
|
+
if (toolsKnown && toolCallCount >= 4)
|
|
92
|
+
return 'complex';
|
|
93
|
+
// Check simple thresholds (requires BOTH conditions)
|
|
94
|
+
if (tokensKnown && inputTokens < 500 && toolsKnown && toolCallCount === 0) {
|
|
95
|
+
return 'simple';
|
|
96
|
+
}
|
|
97
|
+
// If only tokens known and <500 with no tool info → can't confirm simple
|
|
98
|
+
if (tokensKnown && inputTokens < 500 && !toolsKnown) {
|
|
99
|
+
return 'moderate';
|
|
100
|
+
}
|
|
101
|
+
// If only tools known and 0 with no token info → can't confirm simple
|
|
102
|
+
if (!tokensKnown && toolsKnown && toolCallCount === 0) {
|
|
103
|
+
return 'moderate';
|
|
104
|
+
}
|
|
105
|
+
// Everything else is moderate
|
|
106
|
+
return 'moderate';
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=classifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classifier.js","sourceRoot":"","sources":["../../../src/lib/optimization/classifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recommendation Engine (Story 2.3)
|
|
3
|
+
*
|
|
4
|
+
* Analyzes LLM call patterns and recommends cheaper models for
|
|
5
|
+
* each complexity tier where a cheaper alternative has proven
|
|
6
|
+
* reliable (≥95% success rate).
|
|
7
|
+
*/
|
|
8
|
+
import type { IEventStore, OptimizationResult, ModelCosts } from '@agentlensai/core';
|
|
9
|
+
export declare class OptimizationEngine {
|
|
10
|
+
private readonly modelCosts;
|
|
11
|
+
constructor(modelCosts?: ModelCosts);
|
|
12
|
+
getRecommendations(store: IEventStore, options: {
|
|
13
|
+
agentId?: string;
|
|
14
|
+
period: number;
|
|
15
|
+
limit: number;
|
|
16
|
+
}): Promise<OptimizationResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Get the weighted cost rate for a model (per 1M tokens).
|
|
19
|
+
* Uses known model costs table, or falls back to actual cost data from events.
|
|
20
|
+
*/
|
|
21
|
+
private getModelCostRate;
|
|
22
|
+
private determineConfidence;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/lib/optimization/engine.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,WAAW,EAGX,kBAAkB,EAClB,UAAU,EAKX,MAAM,mBAAmB,CAAC;AAgB3B,qBAAa,kBAAkB;IAE3B,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,GAAE,UAAgC;IAGzD,kBAAkB,CACtB,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;KACf,GACA,OAAO,CAAC,kBAAkB,CAAC;IAgL9B;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAwBxB,OAAO,CAAC,mBAAmB;CAK5B"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recommendation Engine (Story 2.3)
|
|
3
|
+
*
|
|
4
|
+
* Analyzes LLM call patterns and recommends cheaper models for
|
|
5
|
+
* each complexity tier where a cheaper alternative has proven
|
|
6
|
+
* reliable (≥95% success rate).
|
|
7
|
+
*/
|
|
8
|
+
import { DEFAULT_MODEL_COSTS } from '@agentlensai/core';
|
|
9
|
+
import { classifyCallComplexity } from './classifier.js';
|
|
10
|
+
export class OptimizationEngine {
|
|
11
|
+
modelCosts;
|
|
12
|
+
constructor(modelCosts = DEFAULT_MODEL_COSTS) {
|
|
13
|
+
this.modelCosts = modelCosts;
|
|
14
|
+
}
|
|
15
|
+
async getRecommendations(store, options) {
|
|
16
|
+
const { agentId, period, limit } = options;
|
|
17
|
+
// Compute date range
|
|
18
|
+
const now = new Date();
|
|
19
|
+
const from = new Date(now.getTime() - period * 24 * 60 * 60 * 1000).toISOString();
|
|
20
|
+
const to = now.toISOString();
|
|
21
|
+
// 1. Query llm_call events for the period
|
|
22
|
+
const callResult = await store.queryEvents({
|
|
23
|
+
eventType: 'llm_call',
|
|
24
|
+
agentId,
|
|
25
|
+
from,
|
|
26
|
+
to,
|
|
27
|
+
limit: 10_000,
|
|
28
|
+
order: 'asc',
|
|
29
|
+
});
|
|
30
|
+
const callEvents = callResult.events;
|
|
31
|
+
if (callEvents.length === 0) {
|
|
32
|
+
return {
|
|
33
|
+
recommendations: [],
|
|
34
|
+
totalPotentialSavings: 0,
|
|
35
|
+
period,
|
|
36
|
+
analyzedCalls: 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// 2. Query llm_response events for pairing
|
|
40
|
+
const responseResult = await store.queryEvents({
|
|
41
|
+
eventType: 'llm_response',
|
|
42
|
+
agentId,
|
|
43
|
+
from,
|
|
44
|
+
to,
|
|
45
|
+
limit: 10_000,
|
|
46
|
+
order: 'asc',
|
|
47
|
+
});
|
|
48
|
+
// Build response lookup by callId
|
|
49
|
+
const responseMap = new Map();
|
|
50
|
+
for (const evt of responseResult.events) {
|
|
51
|
+
const payload = evt.payload;
|
|
52
|
+
if (payload.callId) {
|
|
53
|
+
responseMap.set(payload.callId, evt);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// 3. Classify each call and group by (model, tier)
|
|
57
|
+
const groups = new Map();
|
|
58
|
+
for (const callEvent of callEvents) {
|
|
59
|
+
const callPayload = callEvent.payload;
|
|
60
|
+
const callId = callPayload.callId;
|
|
61
|
+
const model = callPayload.model;
|
|
62
|
+
if (!model)
|
|
63
|
+
continue;
|
|
64
|
+
const responseEvent = callId ? responseMap.get(callId) ?? null : null;
|
|
65
|
+
const responsePayload = responseEvent?.payload;
|
|
66
|
+
// Skip unmatched calls (no response yet) — don't count them as failures
|
|
67
|
+
if (!responseEvent)
|
|
68
|
+
continue;
|
|
69
|
+
const { tier } = classifyCallComplexity(callEvent, responseEvent);
|
|
70
|
+
const groupKey = `${model}::${tier}`;
|
|
71
|
+
let group = groups.get(groupKey);
|
|
72
|
+
if (!group) {
|
|
73
|
+
group = {
|
|
74
|
+
model,
|
|
75
|
+
tier,
|
|
76
|
+
callCount: 0,
|
|
77
|
+
successCount: 0,
|
|
78
|
+
totalCost: 0,
|
|
79
|
+
totalInputTokens: 0,
|
|
80
|
+
totalOutputTokens: 0,
|
|
81
|
+
agentIds: new Set(),
|
|
82
|
+
};
|
|
83
|
+
groups.set(groupKey, group);
|
|
84
|
+
}
|
|
85
|
+
group.callCount++;
|
|
86
|
+
group.agentIds.add(callEvent.agentId);
|
|
87
|
+
// Determine success: finishReason is not 'error'
|
|
88
|
+
const isError = responsePayload?.finishReason === 'error';
|
|
89
|
+
if (!isError) {
|
|
90
|
+
group.successCount++;
|
|
91
|
+
}
|
|
92
|
+
// Accumulate cost from response payload
|
|
93
|
+
const cost = responsePayload?.costUsd ?? 0;
|
|
94
|
+
group.totalCost += cost;
|
|
95
|
+
// Accumulate tokens
|
|
96
|
+
const inputTokens = responsePayload?.usage?.inputTokens ?? 0;
|
|
97
|
+
const outputTokens = responsePayload?.usage?.outputTokens ?? 0;
|
|
98
|
+
group.totalInputTokens += inputTokens;
|
|
99
|
+
group.totalOutputTokens += outputTokens;
|
|
100
|
+
}
|
|
101
|
+
// 4. For each group, look for cheaper alternatives
|
|
102
|
+
const recommendations = [];
|
|
103
|
+
for (const [, group] of groups) {
|
|
104
|
+
const currentCostPerCall = group.callCount > 0 ? group.totalCost / group.callCount : 0;
|
|
105
|
+
const currentSuccessRate = group.callCount > 0 ? group.successCount / group.callCount : 0;
|
|
106
|
+
// Get effective cost rate for this model
|
|
107
|
+
const currentModelCost = this.getModelCostRate(group);
|
|
108
|
+
if (currentModelCost === null)
|
|
109
|
+
continue;
|
|
110
|
+
// Find cheaper alternatives that have data at this tier
|
|
111
|
+
for (const [, candidateGroup] of groups) {
|
|
112
|
+
if (candidateGroup.model === group.model)
|
|
113
|
+
continue;
|
|
114
|
+
if (candidateGroup.tier !== group.tier)
|
|
115
|
+
continue;
|
|
116
|
+
const candidateCostRate = this.getModelCostRate(candidateGroup);
|
|
117
|
+
if (candidateCostRate === null)
|
|
118
|
+
continue;
|
|
119
|
+
// Must be cheaper
|
|
120
|
+
if (candidateCostRate >= currentModelCost)
|
|
121
|
+
continue;
|
|
122
|
+
// Must have ≥95% success rate
|
|
123
|
+
const candidateSuccessRate = candidateGroup.callCount > 0
|
|
124
|
+
? candidateGroup.successCount / candidateGroup.callCount
|
|
125
|
+
: 0;
|
|
126
|
+
if (candidateSuccessRate < 0.95)
|
|
127
|
+
continue;
|
|
128
|
+
const recommendedCostPerCall = candidateGroup.callCount > 0
|
|
129
|
+
? candidateGroup.totalCost / candidateGroup.callCount
|
|
130
|
+
: 0;
|
|
131
|
+
const monthlySavings = (currentCostPerCall - recommendedCostPerCall) * group.callCount * (30 / period);
|
|
132
|
+
if (monthlySavings <= 0)
|
|
133
|
+
continue;
|
|
134
|
+
const confidence = this.determineConfidence(group.callCount);
|
|
135
|
+
// Pick a representative agentId (first in set)
|
|
136
|
+
const agentId = group.agentIds.values().next().value ?? '';
|
|
137
|
+
recommendations.push({
|
|
138
|
+
currentModel: group.model,
|
|
139
|
+
recommendedModel: candidateGroup.model,
|
|
140
|
+
complexityTier: group.tier,
|
|
141
|
+
currentCostPerCall: roundCost(currentCostPerCall),
|
|
142
|
+
recommendedCostPerCall: roundCost(recommendedCostPerCall),
|
|
143
|
+
monthlySavings: roundCost(monthlySavings),
|
|
144
|
+
callVolume: group.callCount,
|
|
145
|
+
currentSuccessRate: roundRate(currentSuccessRate),
|
|
146
|
+
recommendedSuccessRate: roundRate(candidateSuccessRate),
|
|
147
|
+
confidence,
|
|
148
|
+
agentId,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 5. Sort by monthlySavings DESC, take top N
|
|
153
|
+
recommendations.sort((a, b) => b.monthlySavings - a.monthlySavings);
|
|
154
|
+
const limited = recommendations.slice(0, limit);
|
|
155
|
+
const totalPotentialSavings = limited.reduce((sum, r) => sum + r.monthlySavings, 0);
|
|
156
|
+
return {
|
|
157
|
+
recommendations: limited,
|
|
158
|
+
totalPotentialSavings: roundCost(totalPotentialSavings),
|
|
159
|
+
period,
|
|
160
|
+
analyzedCalls: callEvents.length,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get the weighted cost rate for a model (per 1M tokens).
|
|
165
|
+
* Uses known model costs table, or falls back to actual cost data from events.
|
|
166
|
+
*/
|
|
167
|
+
getModelCostRate(group) {
|
|
168
|
+
const knownCost = this.modelCosts[group.model];
|
|
169
|
+
if (knownCost) {
|
|
170
|
+
// Weighted average using actual input/output ratio when available
|
|
171
|
+
const totalTokens = group.totalInputTokens + group.totalOutputTokens;
|
|
172
|
+
let inputWeight = 0.75; // default fallback ~3:1 input:output
|
|
173
|
+
let outputWeight = 0.25;
|
|
174
|
+
if (totalTokens > 0) {
|
|
175
|
+
inputWeight = group.totalInputTokens / totalTokens;
|
|
176
|
+
outputWeight = group.totalOutputTokens / totalTokens;
|
|
177
|
+
}
|
|
178
|
+
return knownCost.input * inputWeight + knownCost.output * outputWeight;
|
|
179
|
+
}
|
|
180
|
+
// Fall back to actual cost from event data
|
|
181
|
+
const totalTokens = group.totalInputTokens + group.totalOutputTokens;
|
|
182
|
+
if (totalTokens > 0 && group.totalCost > 0) {
|
|
183
|
+
// Effective cost per 1M tokens from actual data
|
|
184
|
+
return (group.totalCost / totalTokens) * 1_000_000;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
determineConfidence(callVolume) {
|
|
189
|
+
if (callVolume > 200)
|
|
190
|
+
return 'high';
|
|
191
|
+
if (callVolume >= 50)
|
|
192
|
+
return 'medium';
|
|
193
|
+
return 'low';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function roundCost(value) {
|
|
197
|
+
return Math.round(value * 1_000_000) / 1_000_000;
|
|
198
|
+
}
|
|
199
|
+
function roundRate(value) {
|
|
200
|
+
return Math.round(value * 10_000) / 10_000;
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=engine.js.map
|