@agentlensai/server 0.5.0 → 0.6.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.
Files changed (71) hide show
  1. package/dist/db/health-snapshot-store.d.ts +33 -0
  2. package/dist/db/health-snapshot-store.d.ts.map +1 -0
  3. package/dist/db/health-snapshot-store.js +112 -0
  4. package/dist/db/health-snapshot-store.js.map +1 -0
  5. package/dist/db/migrate.d.ts.map +1 -1
  6. package/dist/db/migrate.js +19 -0
  7. package/dist/db/migrate.js.map +1 -1
  8. package/dist/db/sqlite-store.d.ts +5 -0
  9. package/dist/db/sqlite-store.d.ts.map +1 -1
  10. package/dist/db/sqlite-store.js +15 -0
  11. package/dist/db/sqlite-store.js.map +1 -1
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +13 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/lib/alert-engine.d.ts.map +1 -1
  17. package/dist/lib/alert-engine.js +5 -2
  18. package/dist/lib/alert-engine.js.map +1 -1
  19. package/dist/lib/analysis/cost-analysis.d.ts.map +1 -1
  20. package/dist/lib/analysis/cost-analysis.js +5 -2
  21. package/dist/lib/analysis/cost-analysis.js.map +1 -1
  22. package/dist/lib/analysis/error-patterns.d.ts.map +1 -1
  23. package/dist/lib/analysis/error-patterns.js +5 -2
  24. package/dist/lib/analysis/error-patterns.js.map +1 -1
  25. package/dist/lib/analysis/performance-trends.d.ts.map +1 -1
  26. package/dist/lib/analysis/performance-trends.js +4 -1
  27. package/dist/lib/analysis/performance-trends.js.map +1 -1
  28. package/dist/lib/analysis/tool-sequences.d.ts.map +1 -1
  29. package/dist/lib/analysis/tool-sequences.js +5 -2
  30. package/dist/lib/analysis/tool-sequences.js.map +1 -1
  31. package/dist/lib/context/retrieval.d.ts +4 -0
  32. package/dist/lib/context/retrieval.d.ts.map +1 -1
  33. package/dist/lib/context/retrieval.js +4 -0
  34. package/dist/lib/context/retrieval.js.map +1 -1
  35. package/dist/lib/embeddings/local.js +2 -2
  36. package/dist/lib/embeddings/local.js.map +1 -1
  37. package/dist/lib/health/computer.d.ts +28 -0
  38. package/dist/lib/health/computer.d.ts.map +1 -0
  39. package/dist/lib/health/computer.js +270 -0
  40. package/dist/lib/health/computer.js.map +1 -0
  41. package/dist/lib/optimization/classifier.d.ts +34 -0
  42. package/dist/lib/optimization/classifier.d.ts.map +1 -0
  43. package/dist/lib/optimization/classifier.js +108 -0
  44. package/dist/lib/optimization/classifier.js.map +1 -0
  45. package/dist/lib/optimization/engine.d.ts +24 -0
  46. package/dist/lib/optimization/engine.d.ts.map +1 -0
  47. package/dist/lib/optimization/engine.js +202 -0
  48. package/dist/lib/optimization/engine.js.map +1 -0
  49. package/dist/lib/optimization/index.d.ts +10 -0
  50. package/dist/lib/optimization/index.d.ts.map +1 -0
  51. package/dist/lib/optimization/index.js +9 -0
  52. package/dist/lib/optimization/index.js.map +1 -0
  53. package/dist/routes/health.d.ts +21 -0
  54. package/dist/routes/health.d.ts.map +1 -0
  55. package/dist/routes/health.js +142 -0
  56. package/dist/routes/health.js.map +1 -0
  57. package/dist/routes/optimize.d.ts +15 -0
  58. package/dist/routes/optimize.d.ts.map +1 -0
  59. package/dist/routes/optimize.js +55 -0
  60. package/dist/routes/optimize.js.map +1 -0
  61. package/dist/routes/recall.d.ts +2 -0
  62. package/dist/routes/recall.d.ts.map +1 -1
  63. package/dist/routes/recall.js +21 -1
  64. package/dist/routes/recall.js.map +1 -1
  65. package/dist/routes/reflect.d.ts.map +1 -1
  66. package/dist/routes/reflect.js +0 -1
  67. package/dist/routes/reflect.js.map +1 -1
  68. package/dist/routes/tenant-helper.d.ts.map +1 -1
  69. package/dist/routes/tenant-helper.js +12 -0
  70. package/dist/routes/tenant-helper.js.map +1 -1
  71. package/package.json +1 -1
@@ -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