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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-cli",
3
- "version": "0.1.188",
3
+ "version": "0.1.190",
4
4
  "description": "CLI for trickle runtime type observability",
5
5
  "bin": {
6
6
  "trickle": "dist/index.js"
@@ -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")