trickle-cli 0.1.197 → 0.1.199
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/run.js +16 -10
- package/dist/commands/summarize.d.ts +9 -0
- package/dist/commands/summarize.js +224 -0
- package/dist/index.js +9 -0
- package/package.json +1 -1
- package/src/commands/run.ts +15 -18
- package/src/commands/summarize.ts +208 -0
- package/src/index.ts +10 -0
package/dist/commands/run.js
CHANGED
|
@@ -279,8 +279,7 @@ async function runCommand(command, opts) {
|
|
|
279
279
|
if (!backendProc) {
|
|
280
280
|
// Fall back to local/offline mode instead of exiting
|
|
281
281
|
localMode = true;
|
|
282
|
-
|
|
283
|
-
console.log(chalk_1.default.gray(" Observations will be saved to .trickle/observations.jsonl"));
|
|
282
|
+
// Silent for first-time users — local mode is the default experience
|
|
284
283
|
}
|
|
285
284
|
}
|
|
286
285
|
// Detect language and inject instrumentation
|
|
@@ -400,13 +399,9 @@ async function executeSingleRun(instrumentedCommand, env, opts, singleFile, loca
|
|
|
400
399
|
await autoCloudPush();
|
|
401
400
|
// Generate post-run summary for AI agents
|
|
402
401
|
(0, summary_1.writeRunSummary)({ exitCode, command: instrumentedCommand });
|
|
403
|
-
// Next steps hint
|
|
404
402
|
console.log("");
|
|
405
|
-
console.log(chalk_1.default.
|
|
406
|
-
console.log(chalk_1.default.gray("
|
|
407
|
-
console.log(chalk_1.default.gray(" trickle explain <file> ") + "understand a file (functions, call graph, data flow)");
|
|
408
|
-
console.log(chalk_1.default.gray(" trickle flamegraph ") + "performance hotspots");
|
|
409
|
-
console.log(chalk_1.default.gray(" trickle test ") + "run tests with observability");
|
|
403
|
+
console.log(chalk_1.default.gray(" trickle summary ") + "full analysis");
|
|
404
|
+
console.log(chalk_1.default.gray(" trickle why ") + "trace any error to root cause");
|
|
410
405
|
console.log("");
|
|
411
406
|
return exitCode;
|
|
412
407
|
}
|
|
@@ -562,8 +557,19 @@ async function autoCloudPush() {
|
|
|
562
557
|
try {
|
|
563
558
|
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
564
559
|
if (config.url && config.token) {
|
|
565
|
-
|
|
566
|
-
|
|
560
|
+
// Suppress console output for auto-push to avoid noise
|
|
561
|
+
const origLog = console.log;
|
|
562
|
+
const origErr = console.error;
|
|
563
|
+
console.log = () => { };
|
|
564
|
+
console.error = () => { };
|
|
565
|
+
try {
|
|
566
|
+
const { cloudPush } = await Promise.resolve().then(() => __importStar(require("./cloud")));
|
|
567
|
+
await cloudPush();
|
|
568
|
+
}
|
|
569
|
+
finally {
|
|
570
|
+
console.log = origLog;
|
|
571
|
+
console.error = origErr;
|
|
572
|
+
}
|
|
567
573
|
}
|
|
568
574
|
}
|
|
569
575
|
catch { }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trickle summarize — Compress verbose agent traces into key decision points.
|
|
3
|
+
*
|
|
4
|
+
* Reads agents.jsonl, llm.jsonl, mcp.jsonl and produces a concise
|
|
5
|
+
* narrative of what the agent did, why, and at what cost.
|
|
6
|
+
*/
|
|
7
|
+
export declare function summarizeCommand(opts: {
|
|
8
|
+
json?: boolean;
|
|
9
|
+
}): void;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* trickle summarize — Compress verbose agent traces into key decision points.
|
|
4
|
+
*
|
|
5
|
+
* Reads agents.jsonl, llm.jsonl, mcp.jsonl and produces a concise
|
|
6
|
+
* narrative of what the agent did, why, and at what cost.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.summarizeCommand = summarizeCommand;
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
49
|
+
function readJsonl(fp) {
|
|
50
|
+
if (!fs.existsSync(fp))
|
|
51
|
+
return [];
|
|
52
|
+
return fs.readFileSync(fp, 'utf-8').split('\n').filter(Boolean)
|
|
53
|
+
.map(l => { try {
|
|
54
|
+
return JSON.parse(l);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
} }).filter(Boolean);
|
|
59
|
+
}
|
|
60
|
+
function summarizeCommand(opts) {
|
|
61
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
62
|
+
const agentEvents = readJsonl(path.join(dir, 'agents.jsonl'));
|
|
63
|
+
const llmCalls = readJsonl(path.join(dir, 'llm.jsonl'));
|
|
64
|
+
const mcpCalls = readJsonl(path.join(dir, 'mcp.jsonl'));
|
|
65
|
+
const errors = readJsonl(path.join(dir, 'errors.jsonl'));
|
|
66
|
+
if (agentEvents.length === 0 && llmCalls.length === 0 && mcpCalls.length === 0) {
|
|
67
|
+
console.log(chalk_1.default.yellow(' No agent, LLM, or MCP data to summarize.'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const summary = buildSummary(agentEvents, llmCalls, mcpCalls, errors);
|
|
71
|
+
if (opts.json) {
|
|
72
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
console.log('');
|
|
76
|
+
console.log(chalk_1.default.bold(' trickle summarize'));
|
|
77
|
+
console.log(chalk_1.default.gray(' ' + '─'.repeat(60)));
|
|
78
|
+
// One-line overview
|
|
79
|
+
console.log(` ${summary.overview}`);
|
|
80
|
+
console.log('');
|
|
81
|
+
// Key decisions
|
|
82
|
+
if (summary.decisions.length > 0) {
|
|
83
|
+
console.log(chalk_1.default.bold(' Key Decisions'));
|
|
84
|
+
for (const d of summary.decisions) {
|
|
85
|
+
const icon = d.type === 'error' ? chalk_1.default.red('✗') : d.type === 'tool' ? chalk_1.default.green('⚙') : d.type === 'llm' ? chalk_1.default.magenta('✦') : chalk_1.default.blue('→');
|
|
86
|
+
console.log(` ${icon} ${d.description}`);
|
|
87
|
+
}
|
|
88
|
+
console.log('');
|
|
89
|
+
}
|
|
90
|
+
// Cost breakdown
|
|
91
|
+
if (summary.cost.total > 0) {
|
|
92
|
+
console.log(chalk_1.default.bold(' Cost'));
|
|
93
|
+
console.log(` ${chalk_1.default.green('$' + summary.cost.total.toFixed(4))} total across ${summary.cost.llmCalls} LLM calls (${formatTokens(summary.cost.tokens)} tokens)`);
|
|
94
|
+
if (summary.cost.mostExpensive) {
|
|
95
|
+
console.log(chalk_1.default.gray(` Most expensive: $${summary.cost.mostExpensive.cost.toFixed(4)} — ${summary.cost.mostExpensive.description}`));
|
|
96
|
+
}
|
|
97
|
+
console.log('');
|
|
98
|
+
}
|
|
99
|
+
// Issues
|
|
100
|
+
if (summary.issues.length > 0) {
|
|
101
|
+
console.log(chalk_1.default.bold(' Issues'));
|
|
102
|
+
for (const issue of summary.issues) {
|
|
103
|
+
console.log(` ${chalk_1.default.red('!')} ${issue}`);
|
|
104
|
+
}
|
|
105
|
+
console.log('');
|
|
106
|
+
}
|
|
107
|
+
console.log(chalk_1.default.gray(' ' + '─'.repeat(60)));
|
|
108
|
+
console.log('');
|
|
109
|
+
}
|
|
110
|
+
function buildSummary(agentEvents, llmCalls, mcpCalls, errors) {
|
|
111
|
+
const decisions = [];
|
|
112
|
+
const issues = [];
|
|
113
|
+
// Extract key agent decisions
|
|
114
|
+
const crewStarts = agentEvents.filter(e => e.event === 'crew_start' || e.event === 'chain_start');
|
|
115
|
+
const crewEnds = agentEvents.filter(e => e.event === 'crew_end' || e.event === 'chain_end');
|
|
116
|
+
const toolStarts = agentEvents.filter(e => e.event === 'tool_start');
|
|
117
|
+
const toolEnds = agentEvents.filter(e => e.event === 'tool_end');
|
|
118
|
+
const toolErrors = agentEvents.filter(e => e.event === 'tool_error');
|
|
119
|
+
const agentActions = agentEvents.filter(e => e.event === 'action');
|
|
120
|
+
// Tools used
|
|
121
|
+
const toolNames = [...new Set(toolStarts.map(e => e.tool).filter(Boolean))];
|
|
122
|
+
if (toolNames.length > 0) {
|
|
123
|
+
decisions.push({ type: 'tool', description: `Used ${toolNames.length} tools: ${toolNames.join(', ')}` });
|
|
124
|
+
}
|
|
125
|
+
// Agent reasoning (from action events with thoughts)
|
|
126
|
+
for (const a of agentActions.slice(0, 3)) {
|
|
127
|
+
if (a.thought) {
|
|
128
|
+
decisions.push({ type: 'reasoning', description: `Thought: "${truncate(a.thought, 80)}"` });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Handoffs
|
|
132
|
+
const handoffs = agentEvents.filter(e => e.tool === 'handoff');
|
|
133
|
+
for (const h of handoffs) {
|
|
134
|
+
decisions.push({ type: 'handoff', description: `Handoff: ${truncate(h.toolInput || '', 80)}` });
|
|
135
|
+
}
|
|
136
|
+
// LLM calls summary
|
|
137
|
+
const models = [...new Set(llmCalls.map(c => `${c.provider}/${c.model}`))];
|
|
138
|
+
if (models.length > 0) {
|
|
139
|
+
decisions.push({ type: 'llm', description: `${llmCalls.length} LLM calls using ${models.join(', ')}` });
|
|
140
|
+
}
|
|
141
|
+
// MCP tool calls
|
|
142
|
+
const mcpTools = [...new Set(mcpCalls.filter(c => c.tool !== '__list_tools').map(c => c.tool))];
|
|
143
|
+
if (mcpTools.length > 0) {
|
|
144
|
+
decisions.push({ type: 'tool', description: `${mcpCalls.length} MCP tool calls: ${mcpTools.join(', ')}` });
|
|
145
|
+
}
|
|
146
|
+
// Errors
|
|
147
|
+
for (const te of toolErrors.slice(0, 2)) {
|
|
148
|
+
decisions.push({ type: 'error', description: `Tool "${te.tool}" failed: ${truncate(te.error || '', 60)}` });
|
|
149
|
+
}
|
|
150
|
+
for (const err of errors.slice(0, 2)) {
|
|
151
|
+
issues.push(`${err.type || 'Error'}: ${truncate(err.message || '', 80)}`);
|
|
152
|
+
}
|
|
153
|
+
// Detect patterns
|
|
154
|
+
const toolNameList = toolStarts.map(e => e.tool || '');
|
|
155
|
+
for (let i = 0; i < toolNameList.length - 2; i++) {
|
|
156
|
+
if (toolNameList[i] === toolNameList[i + 1] && toolNameList[i] === toolNameList[i + 2]) {
|
|
157
|
+
issues.push(`Tool "${toolNameList[i]}" called 3+ times — possible retry loop`);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const llmErrors = llmCalls.filter(c => c.error);
|
|
162
|
+
if (llmErrors.length > 0) {
|
|
163
|
+
issues.push(`${llmErrors.length}/${llmCalls.length} LLM calls failed`);
|
|
164
|
+
}
|
|
165
|
+
// Cost
|
|
166
|
+
const totalCost = llmCalls.reduce((s, c) => s + (c.estimatedCostUsd || 0), 0);
|
|
167
|
+
const totalTokens = llmCalls.reduce((s, c) => s + (c.totalTokens || 0), 0);
|
|
168
|
+
const mostExpensiveCall = llmCalls.length > 0
|
|
169
|
+
? llmCalls.reduce((max, c) => (c.estimatedCostUsd || 0) > (max.estimatedCostUsd || 0) ? c : max, llmCalls[0])
|
|
170
|
+
: null;
|
|
171
|
+
// Duration
|
|
172
|
+
const allTimestamps = [
|
|
173
|
+
...agentEvents.map(e => e.timestamp),
|
|
174
|
+
...llmCalls.map(c => c.timestamp),
|
|
175
|
+
...mcpCalls.map(c => c.timestamp),
|
|
176
|
+
].filter(Boolean).sort();
|
|
177
|
+
const duration = allTimestamps.length >= 2 ? allTimestamps[allTimestamps.length - 1] - allTimestamps[0] : 0;
|
|
178
|
+
// Build overview
|
|
179
|
+
const parts = [];
|
|
180
|
+
if (crewStarts.length > 0) {
|
|
181
|
+
const frameworks = [...new Set(agentEvents.map(e => e.framework).filter(Boolean))];
|
|
182
|
+
parts.push(`${crewStarts.length} agent run(s)${frameworks.length > 0 ? ` (${frameworks.join(', ')})` : ''}`);
|
|
183
|
+
}
|
|
184
|
+
if (llmCalls.length > 0)
|
|
185
|
+
parts.push(`${llmCalls.length} LLM calls ($${totalCost.toFixed(4)})`);
|
|
186
|
+
if (toolNames.length > 0)
|
|
187
|
+
parts.push(`${toolStarts.length} tool calls`);
|
|
188
|
+
if (mcpTools.length > 0)
|
|
189
|
+
parts.push(`${mcpCalls.length} MCP calls`);
|
|
190
|
+
if (toolErrors.length > 0)
|
|
191
|
+
parts.push(`${toolErrors.length} tool errors`);
|
|
192
|
+
if (duration > 0)
|
|
193
|
+
parts.push(formatDuration(duration));
|
|
194
|
+
const completed = crewEnds.length > 0 ? 'completed' : crewStarts.length > 0 ? 'started' : '';
|
|
195
|
+
const overview = parts.length > 0
|
|
196
|
+
? `${completed ? completed.charAt(0).toUpperCase() + completed.slice(1) + ': ' : ''}${parts.join(', ')}`
|
|
197
|
+
: 'No significant events captured';
|
|
198
|
+
return {
|
|
199
|
+
overview, decisions, issues, duration,
|
|
200
|
+
cost: {
|
|
201
|
+
total: totalCost, llmCalls: llmCalls.length, tokens: totalTokens,
|
|
202
|
+
mostExpensive: mostExpensiveCall && mostExpensiveCall.estimatedCostUsd > 0
|
|
203
|
+
? { cost: mostExpensiveCall.estimatedCostUsd, description: `${mostExpensiveCall.model}: "${truncate(mostExpensiveCall.inputPreview || '', 50)}"` }
|
|
204
|
+
: null,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function truncate(s, len) {
|
|
209
|
+
return s.length > len ? s.substring(0, len) + '...' : s;
|
|
210
|
+
}
|
|
211
|
+
function formatTokens(n) {
|
|
212
|
+
if (n >= 1_000_000)
|
|
213
|
+
return (n / 1_000_000).toFixed(1) + 'M';
|
|
214
|
+
if (n >= 1_000)
|
|
215
|
+
return (n / 1_000).toFixed(1) + 'K';
|
|
216
|
+
return String(n);
|
|
217
|
+
}
|
|
218
|
+
function formatDuration(ms) {
|
|
219
|
+
if (ms >= 60_000)
|
|
220
|
+
return (ms / 60_000).toFixed(1) + 'min';
|
|
221
|
+
if (ms >= 1_000)
|
|
222
|
+
return (ms / 1_000).toFixed(1) + 's';
|
|
223
|
+
return ms + 'ms';
|
|
224
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -920,6 +920,15 @@ program
|
|
|
920
920
|
const { whyCommand } = await Promise.resolve().then(() => __importStar(require("./commands/why")));
|
|
921
921
|
whyCommand(query, opts);
|
|
922
922
|
});
|
|
923
|
+
// trickle summarize
|
|
924
|
+
program
|
|
925
|
+
.command("summarize")
|
|
926
|
+
.description("Compress agent traces into key decisions — what happened, why, at what cost")
|
|
927
|
+
.option("--json", "Output structured JSON")
|
|
928
|
+
.action(async (opts) => {
|
|
929
|
+
const { summarizeCommand } = await Promise.resolve().then(() => __importStar(require("./commands/summarize")));
|
|
930
|
+
summarizeCommand(opts);
|
|
931
|
+
});
|
|
923
932
|
// trickle cleanup
|
|
924
933
|
program
|
|
925
934
|
.command("cleanup")
|
package/package.json
CHANGED
package/src/commands/run.ts
CHANGED
|
@@ -290,16 +290,7 @@ export async function runCommand(
|
|
|
290
290
|
if (!backendProc) {
|
|
291
291
|
// Fall back to local/offline mode instead of exiting
|
|
292
292
|
localMode = true;
|
|
293
|
-
|
|
294
|
-
chalk.yellow(
|
|
295
|
-
`\n Backend not available — using local mode (offline)`,
|
|
296
|
-
),
|
|
297
|
-
);
|
|
298
|
-
console.log(
|
|
299
|
-
chalk.gray(
|
|
300
|
-
" Observations will be saved to .trickle/observations.jsonl",
|
|
301
|
-
),
|
|
302
|
-
);
|
|
293
|
+
// Silent for first-time users — local mode is the default experience
|
|
303
294
|
}
|
|
304
295
|
}
|
|
305
296
|
|
|
@@ -453,13 +444,9 @@ async function executeSingleRun(
|
|
|
453
444
|
// Generate post-run summary for AI agents
|
|
454
445
|
writeRunSummary({ exitCode, command: instrumentedCommand });
|
|
455
446
|
|
|
456
|
-
// Next steps hint
|
|
457
447
|
console.log("");
|
|
458
|
-
console.log(chalk.
|
|
459
|
-
console.log(chalk.gray("
|
|
460
|
-
console.log(chalk.gray(" trickle explain <file> ") + "understand a file (functions, call graph, data flow)");
|
|
461
|
-
console.log(chalk.gray(" trickle flamegraph ") + "performance hotspots");
|
|
462
|
-
console.log(chalk.gray(" trickle test ") + "run tests with observability");
|
|
448
|
+
console.log(chalk.gray(" trickle summary ") + "full analysis");
|
|
449
|
+
console.log(chalk.gray(" trickle why ") + "trace any error to root cause");
|
|
463
450
|
console.log("");
|
|
464
451
|
|
|
465
452
|
return exitCode;
|
|
@@ -632,8 +619,18 @@ async function autoCloudPush(): Promise<void> {
|
|
|
632
619
|
try {
|
|
633
620
|
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
634
621
|
if (config.url && config.token) {
|
|
635
|
-
|
|
636
|
-
|
|
622
|
+
// Suppress console output for auto-push to avoid noise
|
|
623
|
+
const origLog = console.log;
|
|
624
|
+
const origErr = console.error;
|
|
625
|
+
console.log = () => {};
|
|
626
|
+
console.error = () => {};
|
|
627
|
+
try {
|
|
628
|
+
const { cloudPush } = await import("./cloud");
|
|
629
|
+
await cloudPush();
|
|
630
|
+
} finally {
|
|
631
|
+
console.log = origLog;
|
|
632
|
+
console.error = origErr;
|
|
633
|
+
}
|
|
637
634
|
}
|
|
638
635
|
} catch {}
|
|
639
636
|
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trickle summarize — Compress verbose agent traces into key decision points.
|
|
3
|
+
*
|
|
4
|
+
* Reads agents.jsonl, llm.jsonl, mcp.jsonl and produces a concise
|
|
5
|
+
* narrative of what the agent did, why, and at what cost.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
|
|
12
|
+
function readJsonl(fp: string): any[] {
|
|
13
|
+
if (!fs.existsSync(fp)) return [];
|
|
14
|
+
return fs.readFileSync(fp, 'utf-8').split('\n').filter(Boolean)
|
|
15
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function summarizeCommand(opts: { json?: boolean }): void {
|
|
19
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
20
|
+
const agentEvents = readJsonl(path.join(dir, 'agents.jsonl'));
|
|
21
|
+
const llmCalls = readJsonl(path.join(dir, 'llm.jsonl'));
|
|
22
|
+
const mcpCalls = readJsonl(path.join(dir, 'mcp.jsonl'));
|
|
23
|
+
const errors = readJsonl(path.join(dir, 'errors.jsonl'));
|
|
24
|
+
|
|
25
|
+
if (agentEvents.length === 0 && llmCalls.length === 0 && mcpCalls.length === 0) {
|
|
26
|
+
console.log(chalk.yellow(' No agent, LLM, or MCP data to summarize.'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const summary = buildSummary(agentEvents, llmCalls, mcpCalls, errors);
|
|
31
|
+
|
|
32
|
+
if (opts.json) {
|
|
33
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(chalk.bold(' trickle summarize'));
|
|
39
|
+
console.log(chalk.gray(' ' + '─'.repeat(60)));
|
|
40
|
+
|
|
41
|
+
// One-line overview
|
|
42
|
+
console.log(` ${summary.overview}`);
|
|
43
|
+
console.log('');
|
|
44
|
+
|
|
45
|
+
// Key decisions
|
|
46
|
+
if (summary.decisions.length > 0) {
|
|
47
|
+
console.log(chalk.bold(' Key Decisions'));
|
|
48
|
+
for (const d of summary.decisions) {
|
|
49
|
+
const icon = d.type === 'error' ? chalk.red('✗') : d.type === 'tool' ? chalk.green('⚙') : d.type === 'llm' ? chalk.magenta('✦') : chalk.blue('→');
|
|
50
|
+
console.log(` ${icon} ${d.description}`);
|
|
51
|
+
}
|
|
52
|
+
console.log('');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Cost breakdown
|
|
56
|
+
if (summary.cost.total > 0) {
|
|
57
|
+
console.log(chalk.bold(' Cost'));
|
|
58
|
+
console.log(` ${chalk.green('$' + summary.cost.total.toFixed(4))} total across ${summary.cost.llmCalls} LLM calls (${formatTokens(summary.cost.tokens)} tokens)`);
|
|
59
|
+
if (summary.cost.mostExpensive) {
|
|
60
|
+
console.log(chalk.gray(` Most expensive: $${summary.cost.mostExpensive.cost.toFixed(4)} — ${summary.cost.mostExpensive.description}`));
|
|
61
|
+
}
|
|
62
|
+
console.log('');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Issues
|
|
66
|
+
if (summary.issues.length > 0) {
|
|
67
|
+
console.log(chalk.bold(' Issues'));
|
|
68
|
+
for (const issue of summary.issues) {
|
|
69
|
+
console.log(` ${chalk.red('!')} ${issue}`);
|
|
70
|
+
}
|
|
71
|
+
console.log('');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(chalk.gray(' ' + '─'.repeat(60)));
|
|
75
|
+
console.log('');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface TraceSummary {
|
|
79
|
+
overview: string;
|
|
80
|
+
decisions: Array<{ type: string; description: string }>;
|
|
81
|
+
cost: { total: number; llmCalls: number; tokens: number; mostExpensive: { cost: number; description: string } | null };
|
|
82
|
+
issues: string[];
|
|
83
|
+
duration: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildSummary(agentEvents: any[], llmCalls: any[], mcpCalls: any[], errors: any[]): TraceSummary {
|
|
87
|
+
const decisions: TraceSummary['decisions'] = [];
|
|
88
|
+
const issues: string[] = [];
|
|
89
|
+
|
|
90
|
+
// Extract key agent decisions
|
|
91
|
+
const crewStarts = agentEvents.filter(e => e.event === 'crew_start' || e.event === 'chain_start');
|
|
92
|
+
const crewEnds = agentEvents.filter(e => e.event === 'crew_end' || e.event === 'chain_end');
|
|
93
|
+
const toolStarts = agentEvents.filter(e => e.event === 'tool_start');
|
|
94
|
+
const toolEnds = agentEvents.filter(e => e.event === 'tool_end');
|
|
95
|
+
const toolErrors = agentEvents.filter(e => e.event === 'tool_error');
|
|
96
|
+
const agentActions = agentEvents.filter(e => e.event === 'action');
|
|
97
|
+
|
|
98
|
+
// Tools used
|
|
99
|
+
const toolNames = [...new Set(toolStarts.map(e => e.tool).filter(Boolean))];
|
|
100
|
+
if (toolNames.length > 0) {
|
|
101
|
+
decisions.push({ type: 'tool', description: `Used ${toolNames.length} tools: ${toolNames.join(', ')}` });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Agent reasoning (from action events with thoughts)
|
|
105
|
+
for (const a of agentActions.slice(0, 3)) {
|
|
106
|
+
if (a.thought) {
|
|
107
|
+
decisions.push({ type: 'reasoning', description: `Thought: "${truncate(a.thought, 80)}"` });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handoffs
|
|
112
|
+
const handoffs = agentEvents.filter(e => e.tool === 'handoff');
|
|
113
|
+
for (const h of handoffs) {
|
|
114
|
+
decisions.push({ type: 'handoff', description: `Handoff: ${truncate(h.toolInput || '', 80)}` });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// LLM calls summary
|
|
118
|
+
const models = [...new Set(llmCalls.map(c => `${c.provider}/${c.model}`))];
|
|
119
|
+
if (models.length > 0) {
|
|
120
|
+
decisions.push({ type: 'llm', description: `${llmCalls.length} LLM calls using ${models.join(', ')}` });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// MCP tool calls
|
|
124
|
+
const mcpTools = [...new Set(mcpCalls.filter(c => c.tool !== '__list_tools').map(c => c.tool))];
|
|
125
|
+
if (mcpTools.length > 0) {
|
|
126
|
+
decisions.push({ type: 'tool', description: `${mcpCalls.length} MCP tool calls: ${mcpTools.join(', ')}` });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Errors
|
|
130
|
+
for (const te of toolErrors.slice(0, 2)) {
|
|
131
|
+
decisions.push({ type: 'error', description: `Tool "${te.tool}" failed: ${truncate(te.error || '', 60)}` });
|
|
132
|
+
}
|
|
133
|
+
for (const err of errors.slice(0, 2)) {
|
|
134
|
+
issues.push(`${err.type || 'Error'}: ${truncate(err.message || '', 80)}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Detect patterns
|
|
138
|
+
const toolNameList = toolStarts.map(e => e.tool || '');
|
|
139
|
+
for (let i = 0; i < toolNameList.length - 2; i++) {
|
|
140
|
+
if (toolNameList[i] === toolNameList[i + 1] && toolNameList[i] === toolNameList[i + 2]) {
|
|
141
|
+
issues.push(`Tool "${toolNameList[i]}" called 3+ times — possible retry loop`);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const llmErrors = llmCalls.filter(c => c.error);
|
|
147
|
+
if (llmErrors.length > 0) {
|
|
148
|
+
issues.push(`${llmErrors.length}/${llmCalls.length} LLM calls failed`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Cost
|
|
152
|
+
const totalCost = llmCalls.reduce((s: number, c: any) => s + (c.estimatedCostUsd || 0), 0);
|
|
153
|
+
const totalTokens = llmCalls.reduce((s: number, c: any) => s + (c.totalTokens || 0), 0);
|
|
154
|
+
const mostExpensiveCall = llmCalls.length > 0
|
|
155
|
+
? llmCalls.reduce((max: any, c: any) => (c.estimatedCostUsd || 0) > (max.estimatedCostUsd || 0) ? c : max, llmCalls[0])
|
|
156
|
+
: null;
|
|
157
|
+
|
|
158
|
+
// Duration
|
|
159
|
+
const allTimestamps = [
|
|
160
|
+
...agentEvents.map(e => e.timestamp),
|
|
161
|
+
...llmCalls.map(c => c.timestamp),
|
|
162
|
+
...mcpCalls.map(c => c.timestamp),
|
|
163
|
+
].filter(Boolean).sort();
|
|
164
|
+
const duration = allTimestamps.length >= 2 ? allTimestamps[allTimestamps.length - 1] - allTimestamps[0] : 0;
|
|
165
|
+
|
|
166
|
+
// Build overview
|
|
167
|
+
const parts: string[] = [];
|
|
168
|
+
if (crewStarts.length > 0) {
|
|
169
|
+
const frameworks = [...new Set(agentEvents.map(e => e.framework).filter(Boolean))];
|
|
170
|
+
parts.push(`${crewStarts.length} agent run(s)${frameworks.length > 0 ? ` (${frameworks.join(', ')})` : ''}`);
|
|
171
|
+
}
|
|
172
|
+
if (llmCalls.length > 0) parts.push(`${llmCalls.length} LLM calls ($${totalCost.toFixed(4)})`);
|
|
173
|
+
if (toolNames.length > 0) parts.push(`${toolStarts.length} tool calls`);
|
|
174
|
+
if (mcpTools.length > 0) parts.push(`${mcpCalls.length} MCP calls`);
|
|
175
|
+
if (toolErrors.length > 0) parts.push(`${toolErrors.length} tool errors`);
|
|
176
|
+
if (duration > 0) parts.push(formatDuration(duration));
|
|
177
|
+
|
|
178
|
+
const completed = crewEnds.length > 0 ? 'completed' : crewStarts.length > 0 ? 'started' : '';
|
|
179
|
+
const overview = parts.length > 0
|
|
180
|
+
? `${completed ? completed.charAt(0).toUpperCase() + completed.slice(1) + ': ' : ''}${parts.join(', ')}`
|
|
181
|
+
: 'No significant events captured';
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
overview, decisions, issues, duration,
|
|
185
|
+
cost: {
|
|
186
|
+
total: totalCost, llmCalls: llmCalls.length, tokens: totalTokens,
|
|
187
|
+
mostExpensive: mostExpensiveCall && mostExpensiveCall.estimatedCostUsd > 0
|
|
188
|
+
? { cost: mostExpensiveCall.estimatedCostUsd, description: `${mostExpensiveCall.model}: "${truncate(mostExpensiveCall.inputPreview || '', 50)}"` }
|
|
189
|
+
: null,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function truncate(s: string, len: number): string {
|
|
195
|
+
return s.length > len ? s.substring(0, len) + '...' : s;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatTokens(n: number): string {
|
|
199
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
|
|
200
|
+
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
|
|
201
|
+
return String(n);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function formatDuration(ms: number): string {
|
|
205
|
+
if (ms >= 60_000) return (ms / 60_000).toFixed(1) + 'min';
|
|
206
|
+
if (ms >= 1_000) return (ms / 1_000).toFixed(1) + 's';
|
|
207
|
+
return ms + 'ms';
|
|
208
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -953,6 +953,16 @@ program
|
|
|
953
953
|
whyCommand(query, opts);
|
|
954
954
|
});
|
|
955
955
|
|
|
956
|
+
// trickle summarize
|
|
957
|
+
program
|
|
958
|
+
.command("summarize")
|
|
959
|
+
.description("Compress agent traces into key decisions — what happened, why, at what cost")
|
|
960
|
+
.option("--json", "Output structured JSON")
|
|
961
|
+
.action(async (opts) => {
|
|
962
|
+
const { summarizeCommand } = await import("./commands/summarize");
|
|
963
|
+
summarizeCommand(opts);
|
|
964
|
+
});
|
|
965
|
+
|
|
956
966
|
// trickle cleanup
|
|
957
967
|
program
|
|
958
968
|
.command("cleanup")
|