trickle-cli 0.1.188 → 0.1.190
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/commands/eval.d.ts +17 -0
- package/dist/commands/eval.js +266 -0
- package/dist/index.js +10 -0
- package/package.json +1 -1
- package/src/commands/eval.ts +248 -0
- package/src/index.ts +11 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trickle eval — Score agent runs using traces already captured.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes agents.jsonl, llm.jsonl, errors.jsonl to produce reliability
|
|
5
|
+
* scores without needing an LLM-as-judge. Zero cost, zero API keys.
|
|
6
|
+
*
|
|
7
|
+
* Scoring dimensions:
|
|
8
|
+
* - Completion: Did the agent finish successfully?
|
|
9
|
+
* - Error rate: How many errors during execution?
|
|
10
|
+
* - Cost efficiency: Tokens per meaningful output
|
|
11
|
+
* - Tool reliability: Success rate of tool calls
|
|
12
|
+
* - Latency: Was execution time reasonable?
|
|
13
|
+
*/
|
|
14
|
+
export declare function evalCommand(opts: {
|
|
15
|
+
json?: boolean;
|
|
16
|
+
failUnder?: string;
|
|
17
|
+
}): void;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* trickle eval — Score agent runs using traces already captured.
|
|
4
|
+
*
|
|
5
|
+
* Analyzes agents.jsonl, llm.jsonl, errors.jsonl to produce reliability
|
|
6
|
+
* scores without needing an LLM-as-judge. Zero cost, zero API keys.
|
|
7
|
+
*
|
|
8
|
+
* Scoring dimensions:
|
|
9
|
+
* - Completion: Did the agent finish successfully?
|
|
10
|
+
* - Error rate: How many errors during execution?
|
|
11
|
+
* - Cost efficiency: Tokens per meaningful output
|
|
12
|
+
* - Tool reliability: Success rate of tool calls
|
|
13
|
+
* - Latency: Was execution time reasonable?
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
49
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
50
|
+
};
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.evalCommand = evalCommand;
|
|
53
|
+
const fs = __importStar(require("fs"));
|
|
54
|
+
const path = __importStar(require("path"));
|
|
55
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
56
|
+
function readJsonl(fp) {
|
|
57
|
+
if (!fs.existsSync(fp))
|
|
58
|
+
return [];
|
|
59
|
+
return fs.readFileSync(fp, 'utf-8').split('\n').filter(Boolean)
|
|
60
|
+
.map(l => { try {
|
|
61
|
+
return JSON.parse(l);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
} }).filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
function evalCommand(opts) {
|
|
68
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
69
|
+
const agentEvents = readJsonl(path.join(dir, 'agents.jsonl'));
|
|
70
|
+
const llmCalls = readJsonl(path.join(dir, 'llm.jsonl'));
|
|
71
|
+
const errors = readJsonl(path.join(dir, 'errors.jsonl'));
|
|
72
|
+
const mcpCalls = readJsonl(path.join(dir, 'mcp.jsonl'));
|
|
73
|
+
if (agentEvents.length === 0 && llmCalls.length === 0) {
|
|
74
|
+
console.log(chalk_1.default.yellow(' No agent or LLM data to evaluate. Run an agent with trickle first.'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const result = scoreRun(agentEvents, llmCalls, errors, mcpCalls);
|
|
78
|
+
if (opts.json) {
|
|
79
|
+
const threshold = opts.failUnder ? parseInt(opts.failUnder, 10) : undefined;
|
|
80
|
+
const output = {
|
|
81
|
+
...result,
|
|
82
|
+
...(threshold !== undefined ? { threshold, passed: result.overallScore >= threshold } : {}),
|
|
83
|
+
};
|
|
84
|
+
console.log(JSON.stringify(output, null, 2));
|
|
85
|
+
if (threshold !== undefined && result.overallScore < threshold) {
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Pretty print
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(chalk_1.default.bold(' trickle eval'));
|
|
93
|
+
console.log(chalk_1.default.gray(' ' + '─'.repeat(60)));
|
|
94
|
+
const gradeColor = result.overallScore >= 80 ? chalk_1.default.green :
|
|
95
|
+
result.overallScore >= 60 ? chalk_1.default.yellow : chalk_1.default.red;
|
|
96
|
+
console.log(` Overall: ${gradeColor(result.grade + ' (' + result.overallScore + '/100)')}`);
|
|
97
|
+
console.log('');
|
|
98
|
+
// Dimension scores
|
|
99
|
+
const dims = result.dimensions;
|
|
100
|
+
printDimension('Completion', dims.completion);
|
|
101
|
+
printDimension('Errors', dims.errors);
|
|
102
|
+
printDimension('Cost Efficiency', dims.costEfficiency);
|
|
103
|
+
printDimension('Tool Reliability', dims.toolReliability);
|
|
104
|
+
printDimension('Latency', dims.latency);
|
|
105
|
+
console.log(chalk_1.default.gray('\n ' + '─'.repeat(60)));
|
|
106
|
+
console.log(chalk_1.default.bold(' Summary'));
|
|
107
|
+
console.log(` ${result.summary}`);
|
|
108
|
+
if (result.recommendations.length > 0) {
|
|
109
|
+
console.log(chalk_1.default.bold('\n Recommendations'));
|
|
110
|
+
for (const rec of result.recommendations) {
|
|
111
|
+
console.log(` ${chalk_1.default.yellow('→')} ${rec}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
console.log('');
|
|
115
|
+
// CI mode: exit with non-zero if score below threshold
|
|
116
|
+
if (opts.failUnder) {
|
|
117
|
+
const threshold = parseInt(opts.failUnder, 10);
|
|
118
|
+
if (!isNaN(threshold) && result.overallScore < threshold) {
|
|
119
|
+
console.log(chalk_1.default.red(` FAIL: Score ${result.overallScore} is below threshold ${threshold}`));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function printDimension(name, dim) {
|
|
125
|
+
const bar = renderBar(dim.score);
|
|
126
|
+
const color = dim.score >= 80 ? chalk_1.default.green : dim.score >= 60 ? chalk_1.default.yellow : chalk_1.default.red;
|
|
127
|
+
console.log(` ${name.padEnd(18)} ${bar} ${color(String(dim.score).padStart(3))}/100 ${chalk_1.default.gray(dim.detail)}`);
|
|
128
|
+
}
|
|
129
|
+
function renderBar(score) {
|
|
130
|
+
const filled = Math.round(score / 5);
|
|
131
|
+
const empty = 20 - filled;
|
|
132
|
+
const color = score >= 80 ? chalk_1.default.green : score >= 60 ? chalk_1.default.yellow : chalk_1.default.red;
|
|
133
|
+
return color('█'.repeat(filled)) + chalk_1.default.gray('░'.repeat(empty));
|
|
134
|
+
}
|
|
135
|
+
function scoreRun(agentEvents, llmCalls, errors, mcpCalls) {
|
|
136
|
+
const recommendations = [];
|
|
137
|
+
// 1. Completion score (0-100)
|
|
138
|
+
const crewStarts = agentEvents.filter(e => e.event === 'crew_start' || e.event === 'chain_start');
|
|
139
|
+
const crewEnds = agentEvents.filter(e => e.event === 'crew_end' || e.event === 'chain_end');
|
|
140
|
+
const crewErrors = agentEvents.filter(e => e.event === 'crew_error' || e.event === 'chain_error');
|
|
141
|
+
const completionRate = crewStarts.length > 0
|
|
142
|
+
? Math.min(1, crewEnds.length / crewStarts.length)
|
|
143
|
+
: (llmCalls.length > 0 ? (llmCalls.filter(c => !c.error).length / llmCalls.length) : 1);
|
|
144
|
+
const completionScore = Math.round(completionRate * 100);
|
|
145
|
+
let completionDetail = '';
|
|
146
|
+
if (crewStarts.length > 0) {
|
|
147
|
+
completionDetail = `${crewEnds.length}/${crewStarts.length} workflows completed`;
|
|
148
|
+
if (crewErrors.length > 0)
|
|
149
|
+
completionDetail += `, ${crewErrors.length} failed`;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
completionDetail = `${llmCalls.filter(c => !c.error).length}/${llmCalls.length} LLM calls succeeded`;
|
|
153
|
+
}
|
|
154
|
+
if (completionScore < 80)
|
|
155
|
+
recommendations.push('Improve completion rate — check agent error handling and tool reliability');
|
|
156
|
+
// 2. Error score (0-100, inverse of error rate)
|
|
157
|
+
const totalSteps = agentEvents.length + llmCalls.length + mcpCalls.length;
|
|
158
|
+
const errorEvents = [
|
|
159
|
+
...agentEvents.filter(e => e.event?.includes('error')),
|
|
160
|
+
...llmCalls.filter(c => c.error),
|
|
161
|
+
...mcpCalls.filter(c => c.isError),
|
|
162
|
+
...errors,
|
|
163
|
+
];
|
|
164
|
+
const errorRate = totalSteps > 0 ? errorEvents.length / totalSteps : 0;
|
|
165
|
+
const errorScore = Math.round(Math.max(0, (1 - errorRate * 5)) * 100); // 20% errors = 0 score
|
|
166
|
+
const errorDetail = `${errorEvents.length} errors in ${totalSteps} steps (${(errorRate * 100).toFixed(1)}%)`;
|
|
167
|
+
if (errorScore < 80)
|
|
168
|
+
recommendations.push(`Reduce error rate — ${errorEvents.length} errors detected. Use \`trickle why\` to investigate`);
|
|
169
|
+
// 3. Cost efficiency (0-100)
|
|
170
|
+
const totalCost = llmCalls.reduce((s, c) => s + (c.estimatedCostUsd || 0), 0);
|
|
171
|
+
const totalTokens = llmCalls.reduce((s, c) => s + (c.totalTokens || 0), 0);
|
|
172
|
+
const outputTokens = llmCalls.reduce((s, c) => s + (c.outputTokens || 0), 0);
|
|
173
|
+
const inputTokens = llmCalls.reduce((s, c) => s + (c.inputTokens || 0), 0);
|
|
174
|
+
// Efficiency: ratio of output tokens to input tokens (higher = more efficient)
|
|
175
|
+
const ioRatio = inputTokens > 0 ? outputTokens / inputTokens : 1;
|
|
176
|
+
// Score: 1:1 ratio = 100, 1:10 ratio = 50, 1:100 = 10
|
|
177
|
+
const costScore = llmCalls.length === 0 ? 100 : Math.round(Math.min(100, Math.max(10, ioRatio * 100)));
|
|
178
|
+
const costDetail = llmCalls.length > 0
|
|
179
|
+
? `$${totalCost.toFixed(4)} total, ${formatTokens(inputTokens)} in → ${formatTokens(outputTokens)} out (${ioRatio.toFixed(2)} ratio)`
|
|
180
|
+
: 'No LLM calls';
|
|
181
|
+
if (costScore < 60 && llmCalls.length > 0)
|
|
182
|
+
recommendations.push('Reduce prompt size — input tokens far exceed output. Consider summarizing context before sending');
|
|
183
|
+
// 4. Tool reliability (0-100)
|
|
184
|
+
const toolStarts = agentEvents.filter(e => e.event === 'tool_start');
|
|
185
|
+
const toolEnds = agentEvents.filter(e => e.event === 'tool_end');
|
|
186
|
+
const toolErrors = agentEvents.filter(e => e.event === 'tool_error');
|
|
187
|
+
const mcpErrors = mcpCalls.filter(c => c.isError);
|
|
188
|
+
const totalToolCalls = toolStarts.length + mcpCalls.filter(c => c.tool !== '__list_tools').length;
|
|
189
|
+
const totalToolErrors = toolErrors.length + mcpErrors.length;
|
|
190
|
+
const toolSuccessRate = totalToolCalls > 0 ? 1 - (totalToolErrors / totalToolCalls) : 1;
|
|
191
|
+
const toolScore = Math.round(toolSuccessRate * 100);
|
|
192
|
+
const toolDetail = totalToolCalls > 0
|
|
193
|
+
? `${totalToolCalls - totalToolErrors}/${totalToolCalls} tool calls succeeded`
|
|
194
|
+
: 'No tool calls';
|
|
195
|
+
if (toolScore < 80)
|
|
196
|
+
recommendations.push(`Fix failing tools — ${totalToolErrors} tool errors detected. Check tool implementations`);
|
|
197
|
+
// Check for retry loops
|
|
198
|
+
const toolNames = toolStarts.map(e => e.tool || '');
|
|
199
|
+
let maxConsecutive = 1;
|
|
200
|
+
let current = 1;
|
|
201
|
+
for (let i = 1; i < toolNames.length; i++) {
|
|
202
|
+
if (toolNames[i] === toolNames[i - 1] && toolNames[i]) {
|
|
203
|
+
current++;
|
|
204
|
+
maxConsecutive = Math.max(maxConsecutive, current);
|
|
205
|
+
}
|
|
206
|
+
else
|
|
207
|
+
current = 1;
|
|
208
|
+
}
|
|
209
|
+
if (maxConsecutive >= 3)
|
|
210
|
+
recommendations.push(`Tool retry loop detected (${maxConsecutive} consecutive calls). Agent may be stuck`);
|
|
211
|
+
// 5. Latency score (0-100)
|
|
212
|
+
const durations = [
|
|
213
|
+
...agentEvents.filter(e => e.durationMs).map(e => e.durationMs),
|
|
214
|
+
...llmCalls.filter(c => c.durationMs).map(c => c.durationMs),
|
|
215
|
+
];
|
|
216
|
+
const avgLatency = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
|
|
217
|
+
const maxLatency = durations.length > 0 ? Math.max(...durations) : 0;
|
|
218
|
+
// Score: < 500ms avg = 100, 500-2000 = linear, > 5000ms = 20
|
|
219
|
+
const latencyScore = durations.length === 0 ? 100 :
|
|
220
|
+
Math.round(Math.min(100, Math.max(20, 100 - (avgLatency - 500) / 50)));
|
|
221
|
+
const latencyDetail = durations.length > 0
|
|
222
|
+
? `avg ${avgLatency.toFixed(0)}ms, max ${maxLatency.toFixed(0)}ms across ${durations.length} steps`
|
|
223
|
+
: 'No timing data';
|
|
224
|
+
if (latencyScore < 60)
|
|
225
|
+
recommendations.push(`High latency — avg ${avgLatency.toFixed(0)}ms. Consider faster models or reducing prompt size`);
|
|
226
|
+
// Overall score (weighted average)
|
|
227
|
+
const weights = { completion: 0.3, errors: 0.25, costEfficiency: 0.15, toolReliability: 0.2, latency: 0.1 };
|
|
228
|
+
const overallScore = Math.round(completionScore * weights.completion +
|
|
229
|
+
errorScore * weights.errors +
|
|
230
|
+
costScore * weights.costEfficiency +
|
|
231
|
+
toolScore * weights.toolReliability +
|
|
232
|
+
latencyScore * weights.latency);
|
|
233
|
+
const grade = overallScore >= 90 ? 'A' : overallScore >= 80 ? 'B' : overallScore >= 70 ? 'C' :
|
|
234
|
+
overallScore >= 60 ? 'D' : 'F';
|
|
235
|
+
// Summary
|
|
236
|
+
const parts = [];
|
|
237
|
+
if (crewStarts.length > 0)
|
|
238
|
+
parts.push(`${crewStarts.length} workflow(s)`);
|
|
239
|
+
if (llmCalls.length > 0)
|
|
240
|
+
parts.push(`${llmCalls.length} LLM calls ($${totalCost.toFixed(4)})`);
|
|
241
|
+
if (totalToolCalls > 0)
|
|
242
|
+
parts.push(`${totalToolCalls} tool calls`);
|
|
243
|
+
if (errorEvents.length > 0)
|
|
244
|
+
parts.push(`${errorEvents.length} errors`);
|
|
245
|
+
const summary = parts.join(', ') || 'No agent activity detected';
|
|
246
|
+
return {
|
|
247
|
+
overallScore,
|
|
248
|
+
grade,
|
|
249
|
+
dimensions: {
|
|
250
|
+
completion: { score: completionScore, detail: completionDetail },
|
|
251
|
+
errors: { score: errorScore, detail: errorDetail },
|
|
252
|
+
costEfficiency: { score: costScore, detail: costDetail },
|
|
253
|
+
toolReliability: { score: toolScore, detail: toolDetail },
|
|
254
|
+
latency: { score: latencyScore, detail: latencyDetail },
|
|
255
|
+
},
|
|
256
|
+
summary,
|
|
257
|
+
recommendations,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function formatTokens(n) {
|
|
261
|
+
if (n >= 1_000_000)
|
|
262
|
+
return (n / 1_000_000).toFixed(1) + 'M';
|
|
263
|
+
if (n >= 1_000)
|
|
264
|
+
return (n / 1_000).toFixed(1) + 'K';
|
|
265
|
+
return String(n);
|
|
266
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -913,6 +913,16 @@ program
|
|
|
913
913
|
const { whyCommand } = await Promise.resolve().then(() => __importStar(require("./commands/why")));
|
|
914
914
|
whyCommand(query, opts);
|
|
915
915
|
});
|
|
916
|
+
// trickle eval
|
|
917
|
+
program
|
|
918
|
+
.command("eval")
|
|
919
|
+
.description("Score agent runs on reliability — completion, errors, cost efficiency, tool reliability, latency")
|
|
920
|
+
.option("--json", "Output raw JSON for CI integration")
|
|
921
|
+
.option("--fail-under <score>", "Exit with code 1 if overall score is below this threshold (0-100, for CI)")
|
|
922
|
+
.action(async (opts) => {
|
|
923
|
+
const { evalCommand } = await Promise.resolve().then(() => __importStar(require("./commands/eval")));
|
|
924
|
+
evalCommand(opts);
|
|
925
|
+
});
|
|
916
926
|
// trickle cost-report
|
|
917
927
|
program
|
|
918
928
|
.command("cost-report")
|
package/package.json
CHANGED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trickle eval — Score agent runs using traces already captured.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes agents.jsonl, llm.jsonl, errors.jsonl to produce reliability
|
|
5
|
+
* scores without needing an LLM-as-judge. Zero cost, zero API keys.
|
|
6
|
+
*
|
|
7
|
+
* Scoring dimensions:
|
|
8
|
+
* - Completion: Did the agent finish successfully?
|
|
9
|
+
* - Error rate: How many errors during execution?
|
|
10
|
+
* - Cost efficiency: Tokens per meaningful output
|
|
11
|
+
* - Tool reliability: Success rate of tool calls
|
|
12
|
+
* - Latency: Was execution time reasonable?
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
|
|
19
|
+
interface EvalResult {
|
|
20
|
+
overallScore: number;
|
|
21
|
+
grade: string;
|
|
22
|
+
dimensions: {
|
|
23
|
+
completion: { score: number; detail: string };
|
|
24
|
+
errors: { score: number; detail: string };
|
|
25
|
+
costEfficiency: { score: number; detail: string };
|
|
26
|
+
toolReliability: { score: number; detail: string };
|
|
27
|
+
latency: { score: number; detail: string };
|
|
28
|
+
};
|
|
29
|
+
summary: string;
|
|
30
|
+
recommendations: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readJsonl(fp: string): any[] {
|
|
34
|
+
if (!fs.existsSync(fp)) return [];
|
|
35
|
+
return fs.readFileSync(fp, 'utf-8').split('\n').filter(Boolean)
|
|
36
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function evalCommand(opts: { json?: boolean; failUnder?: string }): void {
|
|
40
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
41
|
+
const agentEvents = readJsonl(path.join(dir, 'agents.jsonl'));
|
|
42
|
+
const llmCalls = readJsonl(path.join(dir, 'llm.jsonl'));
|
|
43
|
+
const errors = readJsonl(path.join(dir, 'errors.jsonl'));
|
|
44
|
+
const mcpCalls = readJsonl(path.join(dir, 'mcp.jsonl'));
|
|
45
|
+
|
|
46
|
+
if (agentEvents.length === 0 && llmCalls.length === 0) {
|
|
47
|
+
console.log(chalk.yellow(' No agent or LLM data to evaluate. Run an agent with trickle first.'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = scoreRun(agentEvents, llmCalls, errors, mcpCalls);
|
|
52
|
+
|
|
53
|
+
if (opts.json) {
|
|
54
|
+
const threshold = opts.failUnder ? parseInt(opts.failUnder, 10) : undefined;
|
|
55
|
+
const output = {
|
|
56
|
+
...result,
|
|
57
|
+
...(threshold !== undefined ? { threshold, passed: result.overallScore >= threshold } : {}),
|
|
58
|
+
};
|
|
59
|
+
console.log(JSON.stringify(output, null, 2));
|
|
60
|
+
if (threshold !== undefined && result.overallScore < threshold) {
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Pretty print
|
|
67
|
+
console.log('');
|
|
68
|
+
console.log(chalk.bold(' trickle eval'));
|
|
69
|
+
console.log(chalk.gray(' ' + '─'.repeat(60)));
|
|
70
|
+
|
|
71
|
+
const gradeColor = result.overallScore >= 80 ? chalk.green :
|
|
72
|
+
result.overallScore >= 60 ? chalk.yellow : chalk.red;
|
|
73
|
+
console.log(` Overall: ${gradeColor(result.grade + ' (' + result.overallScore + '/100)')}`);
|
|
74
|
+
console.log('');
|
|
75
|
+
|
|
76
|
+
// Dimension scores
|
|
77
|
+
const dims = result.dimensions;
|
|
78
|
+
printDimension('Completion', dims.completion);
|
|
79
|
+
printDimension('Errors', dims.errors);
|
|
80
|
+
printDimension('Cost Efficiency', dims.costEfficiency);
|
|
81
|
+
printDimension('Tool Reliability', dims.toolReliability);
|
|
82
|
+
printDimension('Latency', dims.latency);
|
|
83
|
+
|
|
84
|
+
console.log(chalk.gray('\n ' + '─'.repeat(60)));
|
|
85
|
+
console.log(chalk.bold(' Summary'));
|
|
86
|
+
console.log(` ${result.summary}`);
|
|
87
|
+
|
|
88
|
+
if (result.recommendations.length > 0) {
|
|
89
|
+
console.log(chalk.bold('\n Recommendations'));
|
|
90
|
+
for (const rec of result.recommendations) {
|
|
91
|
+
console.log(` ${chalk.yellow('→')} ${rec}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('');
|
|
96
|
+
|
|
97
|
+
// CI mode: exit with non-zero if score below threshold
|
|
98
|
+
if (opts.failUnder) {
|
|
99
|
+
const threshold = parseInt(opts.failUnder, 10);
|
|
100
|
+
if (!isNaN(threshold) && result.overallScore < threshold) {
|
|
101
|
+
console.log(chalk.red(` FAIL: Score ${result.overallScore} is below threshold ${threshold}`));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function printDimension(name: string, dim: { score: number; detail: string }): void {
|
|
108
|
+
const bar = renderBar(dim.score);
|
|
109
|
+
const color = dim.score >= 80 ? chalk.green : dim.score >= 60 ? chalk.yellow : chalk.red;
|
|
110
|
+
console.log(` ${name.padEnd(18)} ${bar} ${color(String(dim.score).padStart(3))}/100 ${chalk.gray(dim.detail)}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderBar(score: number): string {
|
|
114
|
+
const filled = Math.round(score / 5);
|
|
115
|
+
const empty = 20 - filled;
|
|
116
|
+
const color = score >= 80 ? chalk.green : score >= 60 ? chalk.yellow : chalk.red;
|
|
117
|
+
return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function scoreRun(
|
|
121
|
+
agentEvents: any[], llmCalls: any[], errors: any[], mcpCalls: any[],
|
|
122
|
+
): EvalResult {
|
|
123
|
+
const recommendations: string[] = [];
|
|
124
|
+
|
|
125
|
+
// 1. Completion score (0-100)
|
|
126
|
+
const crewStarts = agentEvents.filter(e => e.event === 'crew_start' || e.event === 'chain_start');
|
|
127
|
+
const crewEnds = agentEvents.filter(e => e.event === 'crew_end' || e.event === 'chain_end');
|
|
128
|
+
const crewErrors = agentEvents.filter(e => e.event === 'crew_error' || e.event === 'chain_error');
|
|
129
|
+
const completionRate = crewStarts.length > 0
|
|
130
|
+
? Math.min(1, crewEnds.length / crewStarts.length)
|
|
131
|
+
: (llmCalls.length > 0 ? (llmCalls.filter(c => !c.error).length / llmCalls.length) : 1);
|
|
132
|
+
const completionScore = Math.round(completionRate * 100);
|
|
133
|
+
let completionDetail = '';
|
|
134
|
+
if (crewStarts.length > 0) {
|
|
135
|
+
completionDetail = `${crewEnds.length}/${crewStarts.length} workflows completed`;
|
|
136
|
+
if (crewErrors.length > 0) completionDetail += `, ${crewErrors.length} failed`;
|
|
137
|
+
} else {
|
|
138
|
+
completionDetail = `${llmCalls.filter(c => !c.error).length}/${llmCalls.length} LLM calls succeeded`;
|
|
139
|
+
}
|
|
140
|
+
if (completionScore < 80) recommendations.push('Improve completion rate — check agent error handling and tool reliability');
|
|
141
|
+
|
|
142
|
+
// 2. Error score (0-100, inverse of error rate)
|
|
143
|
+
const totalSteps = agentEvents.length + llmCalls.length + mcpCalls.length;
|
|
144
|
+
const errorEvents = [
|
|
145
|
+
...agentEvents.filter(e => e.event?.includes('error')),
|
|
146
|
+
...llmCalls.filter(c => c.error),
|
|
147
|
+
...mcpCalls.filter(c => c.isError),
|
|
148
|
+
...errors,
|
|
149
|
+
];
|
|
150
|
+
const errorRate = totalSteps > 0 ? errorEvents.length / totalSteps : 0;
|
|
151
|
+
const errorScore = Math.round(Math.max(0, (1 - errorRate * 5)) * 100); // 20% errors = 0 score
|
|
152
|
+
const errorDetail = `${errorEvents.length} errors in ${totalSteps} steps (${(errorRate * 100).toFixed(1)}%)`;
|
|
153
|
+
if (errorScore < 80) recommendations.push(`Reduce error rate — ${errorEvents.length} errors detected. Use \`trickle why\` to investigate`);
|
|
154
|
+
|
|
155
|
+
// 3. Cost efficiency (0-100)
|
|
156
|
+
const totalCost = llmCalls.reduce((s: number, c: any) => s + (c.estimatedCostUsd || 0), 0);
|
|
157
|
+
const totalTokens = llmCalls.reduce((s: number, c: any) => s + (c.totalTokens || 0), 0);
|
|
158
|
+
const outputTokens = llmCalls.reduce((s: number, c: any) => s + (c.outputTokens || 0), 0);
|
|
159
|
+
const inputTokens = llmCalls.reduce((s: number, c: any) => s + (c.inputTokens || 0), 0);
|
|
160
|
+
// Efficiency: ratio of output tokens to input tokens (higher = more efficient)
|
|
161
|
+
const ioRatio = inputTokens > 0 ? outputTokens / inputTokens : 1;
|
|
162
|
+
// Score: 1:1 ratio = 100, 1:10 ratio = 50, 1:100 = 10
|
|
163
|
+
const costScore = llmCalls.length === 0 ? 100 : Math.round(Math.min(100, Math.max(10, ioRatio * 100)));
|
|
164
|
+
const costDetail = llmCalls.length > 0
|
|
165
|
+
? `$${totalCost.toFixed(4)} total, ${formatTokens(inputTokens)} in → ${formatTokens(outputTokens)} out (${ioRatio.toFixed(2)} ratio)`
|
|
166
|
+
: 'No LLM calls';
|
|
167
|
+
if (costScore < 60 && llmCalls.length > 0) recommendations.push('Reduce prompt size — input tokens far exceed output. Consider summarizing context before sending');
|
|
168
|
+
|
|
169
|
+
// 4. Tool reliability (0-100)
|
|
170
|
+
const toolStarts = agentEvents.filter(e => e.event === 'tool_start');
|
|
171
|
+
const toolEnds = agentEvents.filter(e => e.event === 'tool_end');
|
|
172
|
+
const toolErrors = agentEvents.filter(e => e.event === 'tool_error');
|
|
173
|
+
const mcpErrors = mcpCalls.filter(c => c.isError);
|
|
174
|
+
const totalToolCalls = toolStarts.length + mcpCalls.filter(c => c.tool !== '__list_tools').length;
|
|
175
|
+
const totalToolErrors = toolErrors.length + mcpErrors.length;
|
|
176
|
+
const toolSuccessRate = totalToolCalls > 0 ? 1 - (totalToolErrors / totalToolCalls) : 1;
|
|
177
|
+
const toolScore = Math.round(toolSuccessRate * 100);
|
|
178
|
+
const toolDetail = totalToolCalls > 0
|
|
179
|
+
? `${totalToolCalls - totalToolErrors}/${totalToolCalls} tool calls succeeded`
|
|
180
|
+
: 'No tool calls';
|
|
181
|
+
if (toolScore < 80) recommendations.push(`Fix failing tools — ${totalToolErrors} tool errors detected. Check tool implementations`);
|
|
182
|
+
|
|
183
|
+
// Check for retry loops
|
|
184
|
+
const toolNames = toolStarts.map(e => e.tool || '');
|
|
185
|
+
let maxConsecutive = 1;
|
|
186
|
+
let current = 1;
|
|
187
|
+
for (let i = 1; i < toolNames.length; i++) {
|
|
188
|
+
if (toolNames[i] === toolNames[i - 1] && toolNames[i]) { current++; maxConsecutive = Math.max(maxConsecutive, current); }
|
|
189
|
+
else current = 1;
|
|
190
|
+
}
|
|
191
|
+
if (maxConsecutive >= 3) recommendations.push(`Tool retry loop detected (${maxConsecutive} consecutive calls). Agent may be stuck`);
|
|
192
|
+
|
|
193
|
+
// 5. Latency score (0-100)
|
|
194
|
+
const durations = [
|
|
195
|
+
...agentEvents.filter(e => e.durationMs).map(e => e.durationMs),
|
|
196
|
+
...llmCalls.filter(c => c.durationMs).map(c => c.durationMs),
|
|
197
|
+
];
|
|
198
|
+
const avgLatency = durations.length > 0 ? durations.reduce((s: number, d: number) => s + d, 0) / durations.length : 0;
|
|
199
|
+
const maxLatency = durations.length > 0 ? Math.max(...durations) : 0;
|
|
200
|
+
// Score: < 500ms avg = 100, 500-2000 = linear, > 5000ms = 20
|
|
201
|
+
const latencyScore = durations.length === 0 ? 100 :
|
|
202
|
+
Math.round(Math.min(100, Math.max(20, 100 - (avgLatency - 500) / 50)));
|
|
203
|
+
const latencyDetail = durations.length > 0
|
|
204
|
+
? `avg ${avgLatency.toFixed(0)}ms, max ${maxLatency.toFixed(0)}ms across ${durations.length} steps`
|
|
205
|
+
: 'No timing data';
|
|
206
|
+
if (latencyScore < 60) recommendations.push(`High latency — avg ${avgLatency.toFixed(0)}ms. Consider faster models or reducing prompt size`);
|
|
207
|
+
|
|
208
|
+
// Overall score (weighted average)
|
|
209
|
+
const weights = { completion: 0.3, errors: 0.25, costEfficiency: 0.15, toolReliability: 0.2, latency: 0.1 };
|
|
210
|
+
const overallScore = Math.round(
|
|
211
|
+
completionScore * weights.completion +
|
|
212
|
+
errorScore * weights.errors +
|
|
213
|
+
costScore * weights.costEfficiency +
|
|
214
|
+
toolScore * weights.toolReliability +
|
|
215
|
+
latencyScore * weights.latency
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const grade = overallScore >= 90 ? 'A' : overallScore >= 80 ? 'B' : overallScore >= 70 ? 'C' :
|
|
219
|
+
overallScore >= 60 ? 'D' : 'F';
|
|
220
|
+
|
|
221
|
+
// Summary
|
|
222
|
+
const parts: string[] = [];
|
|
223
|
+
if (crewStarts.length > 0) parts.push(`${crewStarts.length} workflow(s)`);
|
|
224
|
+
if (llmCalls.length > 0) parts.push(`${llmCalls.length} LLM calls ($${totalCost.toFixed(4)})`);
|
|
225
|
+
if (totalToolCalls > 0) parts.push(`${totalToolCalls} tool calls`);
|
|
226
|
+
if (errorEvents.length > 0) parts.push(`${errorEvents.length} errors`);
|
|
227
|
+
const summary = parts.join(', ') || 'No agent activity detected';
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
overallScore,
|
|
231
|
+
grade,
|
|
232
|
+
dimensions: {
|
|
233
|
+
completion: { score: completionScore, detail: completionDetail },
|
|
234
|
+
errors: { score: errorScore, detail: errorDetail },
|
|
235
|
+
costEfficiency: { score: costScore, detail: costDetail },
|
|
236
|
+
toolReliability: { score: toolScore, detail: toolDetail },
|
|
237
|
+
latency: { score: latencyScore, detail: latencyDetail },
|
|
238
|
+
},
|
|
239
|
+
summary,
|
|
240
|
+
recommendations,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function formatTokens(n: number): string {
|
|
245
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
|
|
246
|
+
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
|
|
247
|
+
return String(n);
|
|
248
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -946,6 +946,17 @@ program
|
|
|
946
946
|
whyCommand(query, opts);
|
|
947
947
|
});
|
|
948
948
|
|
|
949
|
+
// trickle eval
|
|
950
|
+
program
|
|
951
|
+
.command("eval")
|
|
952
|
+
.description("Score agent runs on reliability — completion, errors, cost efficiency, tool reliability, latency")
|
|
953
|
+
.option("--json", "Output raw JSON for CI integration")
|
|
954
|
+
.option("--fail-under <score>", "Exit with code 1 if overall score is below this threshold (0-100, for CI)")
|
|
955
|
+
.action(async (opts) => {
|
|
956
|
+
const { evalCommand } = await import("./commands/eval");
|
|
957
|
+
evalCommand(opts);
|
|
958
|
+
});
|
|
959
|
+
|
|
949
960
|
// trickle cost-report
|
|
950
961
|
program
|
|
951
962
|
.command("cost-report")
|