agent-profiler 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/dist/cli.js +5 -4
- package/dist/commands/init.js +4 -0
- package/dist/commands/status.js +36 -9
- package/dist/core/db.js +164 -28
- package/dist/core/gitWorkspace.js +46 -5
- package/dist/core/packageMeta.js +20 -0
- package/dist/core/schema.sql +8 -0
- package/package.json +57 -3
- package/agent-profiler-0.1.0.tgz +0 -0
- package/docs/agent-profiler-mvp-handoff.md +0 -980
- package/google-home.png +0 -0
- package/src/adapters/codex.ts +0 -131
- package/src/adapters/cursor.ts +0 -115
- package/src/cli.ts +0 -109
- package/src/commands/auditContext.ts +0 -62
- package/src/commands/hook.ts +0 -104
- package/src/commands/init.ts +0 -324
- package/src/commands/last.ts +0 -326
- package/src/commands/status.ts +0 -345
- package/src/core/contextAudit.ts +0 -102
- package/src/core/db.ts +0 -491
- package/src/core/eventMetadata.ts +0 -184
- package/src/core/gitWorkspace.ts +0 -92
- package/src/core/normalize.ts +0 -29
- package/src/core/profile.ts +0 -35
- package/src/core/schema.sql +0 -56
- package/src/core/tokens.ts +0 -4
- package/src/types/better-sqlite3.d.ts +0 -26
- package/tsconfig.json +0 -15
package/src/commands/init.ts
DELETED
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { openDb, resolveUsableDbPath } from "../core/db.js";
|
|
5
|
-
import { getHomeProfileDir, getLocalProfileDir } from "../core/profile.js";
|
|
6
|
-
|
|
7
|
-
export type InitSource = "cursor" | "codex";
|
|
8
|
-
|
|
9
|
-
type CursorAdapterConfig = {
|
|
10
|
-
enabled: boolean;
|
|
11
|
-
hookFile: string;
|
|
12
|
-
mode: "dev" | "prod";
|
|
13
|
-
initializedAt: string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type AgentProfilerConfig = {
|
|
17
|
-
adapters: Partial<{
|
|
18
|
-
cursor: CursorAdapterConfig;
|
|
19
|
-
codex: CursorAdapterConfig;
|
|
20
|
-
}>;
|
|
21
|
-
databasePath: string;
|
|
22
|
-
updatedAt: string;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
type CursorHookCommand = { command: string };
|
|
26
|
-
|
|
27
|
-
type CursorIdeHooksConfig = {
|
|
28
|
-
version?: number;
|
|
29
|
-
hooks?: Record<string, string | CursorHookCommand[]>;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
type CodexHookHandler = { type?: string; command?: string; timeout?: number };
|
|
33
|
-
type CodexHookGroup = { matcher?: string; hooks?: CodexHookHandler[] };
|
|
34
|
-
type CodexHooksFile = { hooks?: Record<string, CodexHookGroup[]> };
|
|
35
|
-
|
|
36
|
-
const CURSOR_EVENTS = [
|
|
37
|
-
"beforeSubmitPrompt",
|
|
38
|
-
"afterAgentResponse",
|
|
39
|
-
"afterAgentThought",
|
|
40
|
-
"preToolUse",
|
|
41
|
-
"postToolUse",
|
|
42
|
-
"postToolUseFailure",
|
|
43
|
-
"beforeMCPExecution",
|
|
44
|
-
"afterMCPExecution",
|
|
45
|
-
"beforeShellExecution",
|
|
46
|
-
"afterShellExecution",
|
|
47
|
-
"beforeReadFile",
|
|
48
|
-
"afterFileEdit",
|
|
49
|
-
"stop",
|
|
50
|
-
] as const;
|
|
51
|
-
|
|
52
|
-
const CODEX_EVENTS = [
|
|
53
|
-
"SessionStart",
|
|
54
|
-
"UserPromptSubmit",
|
|
55
|
-
"PreToolUse",
|
|
56
|
-
"PostToolUse",
|
|
57
|
-
"Stop",
|
|
58
|
-
] as const;
|
|
59
|
-
|
|
60
|
-
function ensureDir(dirPath: string): void {
|
|
61
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function getProfilerDir(): string {
|
|
65
|
-
const localDir = getLocalProfileDir(process.cwd());
|
|
66
|
-
try {
|
|
67
|
-
ensureDir(localDir);
|
|
68
|
-
return localDir;
|
|
69
|
-
} catch {
|
|
70
|
-
const homeDir = getHomeProfileDir();
|
|
71
|
-
ensureDir(homeDir);
|
|
72
|
-
return homeDir;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function getProfilerDirByMode(mode: "dev" | "prod"): string {
|
|
77
|
-
if (mode === "prod") {
|
|
78
|
-
const homeDir = getHomeProfileDir();
|
|
79
|
-
ensureDir(homeDir);
|
|
80
|
-
return homeDir;
|
|
81
|
-
}
|
|
82
|
-
return getProfilerDir();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function readJsonFile<T>(filePath: string): T | null {
|
|
86
|
-
try {
|
|
87
|
-
const raw = fs.readFileSync(filePath, "utf8");
|
|
88
|
-
return JSON.parse(raw) as T;
|
|
89
|
-
} catch {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function writeJsonFile(filePath: string, value: unknown): void {
|
|
95
|
-
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function resolveCursorHookFile(): string {
|
|
99
|
-
const localCursorDir = path.join(process.cwd(), ".cursor");
|
|
100
|
-
try {
|
|
101
|
-
ensureDir(localCursorDir);
|
|
102
|
-
return path.join(localCursorDir, "hooks.json");
|
|
103
|
-
} catch {
|
|
104
|
-
const homeBase = process.env.HOME ?? process.cwd();
|
|
105
|
-
const homeCursorDir = path.join(homeBase, ".cursor");
|
|
106
|
-
ensureDir(homeCursorDir);
|
|
107
|
-
return path.join(homeCursorDir, "hooks.json");
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function resolveCodexDir(): string {
|
|
112
|
-
const local = path.join(process.cwd(), ".codex");
|
|
113
|
-
ensureDir(local);
|
|
114
|
-
return local;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function packagedCliJsPath(): string {
|
|
118
|
-
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
119
|
-
return path.join(packageRoot, "dist", "cli.js");
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function hookCommand(mode: "dev" | "prod", source: InitSource, eventName: string): string {
|
|
123
|
-
const cliPath = packagedCliJsPath();
|
|
124
|
-
return mode === "dev"
|
|
125
|
-
? `node ${cliPath} hook ${source} ${eventName}`
|
|
126
|
-
: `agent-profiler hook ${source} ${eventName}`;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function updateCursorIdeHooks(hooksFile: string, mode: "dev" | "prod"): void {
|
|
130
|
-
const existing = readJsonFile<CursorIdeHooksConfig>(hooksFile) ?? {};
|
|
131
|
-
const hooks: Record<string, string | CursorHookCommand[]> = {
|
|
132
|
-
...(existing.hooks ?? {}),
|
|
133
|
-
};
|
|
134
|
-
for (const eventName of CURSOR_EVENTS) {
|
|
135
|
-
hooks[eventName] = [{ command: hookCommand(mode, "cursor", eventName) }];
|
|
136
|
-
}
|
|
137
|
-
const updated: CursorIdeHooksConfig = { ...existing, version: 1, hooks };
|
|
138
|
-
writeJsonFile(hooksFile, updated);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function profilerMarkerSubcommand(source: InitSource, eventName: string): string {
|
|
142
|
-
return `hook ${source} ${eventName}`;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function codexGroupHasProfilerCommand(
|
|
146
|
-
groups: CodexHookGroup[] | undefined,
|
|
147
|
-
marker: string,
|
|
148
|
-
): boolean {
|
|
149
|
-
if (!groups) return false;
|
|
150
|
-
for (const g of groups) {
|
|
151
|
-
for (const h of g.hooks ?? []) {
|
|
152
|
-
if (typeof h.command === "string" && h.command.includes(marker)) return true;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function mergeCodexProfilerHooks(hooksFile: string, mode: "dev" | "prod"): void {
|
|
159
|
-
const existingRaw = readJsonFile<unknown>(hooksFile);
|
|
160
|
-
const file: CodexHooksFile =
|
|
161
|
-
existingRaw &&
|
|
162
|
-
typeof existingRaw === "object" &&
|
|
163
|
-
!Array.isArray(existingRaw) &&
|
|
164
|
-
"hooks" in (existingRaw as object)
|
|
165
|
-
? (existingRaw as CodexHooksFile)
|
|
166
|
-
: { hooks: {} };
|
|
167
|
-
|
|
168
|
-
file.hooks = { ...(file.hooks ?? {}) };
|
|
169
|
-
|
|
170
|
-
for (const eventName of CODEX_EVENTS) {
|
|
171
|
-
const marker = profilerMarkerSubcommand("codex", eventName);
|
|
172
|
-
const groups = [...(file.hooks[eventName] ?? [])];
|
|
173
|
-
if (codexGroupHasProfilerCommand(groups, marker)) {
|
|
174
|
-
file.hooks[eventName] = groups;
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
const command = hookCommand(mode, "codex", eventName);
|
|
178
|
-
const needsMatcher =
|
|
179
|
-
eventName === "SessionStart" ||
|
|
180
|
-
eventName === "PreToolUse" ||
|
|
181
|
-
eventName === "PostToolUse";
|
|
182
|
-
const group: CodexHookGroup = {
|
|
183
|
-
...(needsMatcher ? { matcher: "*" } : {}),
|
|
184
|
-
hooks: [{ type: "command", command }],
|
|
185
|
-
};
|
|
186
|
-
groups.push(group);
|
|
187
|
-
file.hooks[eventName] = groups;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
writeJsonFile(hooksFile, file);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function ensureCodexHooksFeatureFlag(codexDir: string): void {
|
|
194
|
-
const configPath = path.join(codexDir, "config.toml");
|
|
195
|
-
let raw = "";
|
|
196
|
-
try {
|
|
197
|
-
raw = fs.readFileSync(configPath, "utf8");
|
|
198
|
-
} catch {
|
|
199
|
-
raw = "";
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (!raw.trim()) {
|
|
203
|
-
fs.writeFileSync(configPath, `[features]\ncodex_hooks = true\n`, "utf8");
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (/\bcodex_hooks\s*=\s*false\b/.test(raw)) {
|
|
208
|
-
raw = raw.replace(/\bcodex_hooks\s*=\s*false\b/, "codex_hooks = true");
|
|
209
|
-
fs.writeFileSync(configPath, raw, "utf8");
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (/\bcodex_hooks\s*=\s*true\b/.test(raw)) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
fs.writeFileSync(
|
|
218
|
-
configPath,
|
|
219
|
-
`${raw.trimEnd()}\n\n[features]\ncodex_hooks = true\n`,
|
|
220
|
-
"utf8",
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function writeProfilerConfigMerged(
|
|
225
|
-
configPath: string,
|
|
226
|
-
adapterKey: keyof NonNullable<AgentProfilerConfig["adapters"]>,
|
|
227
|
-
adapterConfig: CursorAdapterConfig,
|
|
228
|
-
dbPath: string,
|
|
229
|
-
): void {
|
|
230
|
-
const previous = readJsonFile<AgentProfilerConfig>(configPath);
|
|
231
|
-
const now = new Date().toISOString();
|
|
232
|
-
const priorInit = previous?.adapters?.[adapterKey]?.initializedAt;
|
|
233
|
-
const merged: CursorAdapterConfig = {
|
|
234
|
-
...adapterConfig,
|
|
235
|
-
initializedAt: priorInit && priorInit.length > 0 ? priorInit : now,
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
const updated: AgentProfilerConfig = {
|
|
239
|
-
adapters: {
|
|
240
|
-
...previous?.adapters,
|
|
241
|
-
[adapterKey]: merged,
|
|
242
|
-
},
|
|
243
|
-
databasePath: dbPath,
|
|
244
|
-
updatedAt: now,
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
writeJsonFile(configPath, updated);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function runInit(source: InitSource, mode: "dev" | "prod" = "dev"): void {
|
|
251
|
-
const profilerDir = getProfilerDirByMode(mode);
|
|
252
|
-
ensureDir(profilerDir);
|
|
253
|
-
|
|
254
|
-
const configuredDbPath = path.join(profilerDir, "events.sqlite");
|
|
255
|
-
const resolvedDbPath = resolveUsableDbPath(configuredDbPath);
|
|
256
|
-
|
|
257
|
-
const db = openDb(resolvedDbPath);
|
|
258
|
-
db.close();
|
|
259
|
-
|
|
260
|
-
let hooksPath = "";
|
|
261
|
-
let configPath = path.join(profilerDir, "config.json");
|
|
262
|
-
let usedFallbackConfigPath = false;
|
|
263
|
-
|
|
264
|
-
if (source === "cursor") {
|
|
265
|
-
hooksPath = resolveCursorHookFile();
|
|
266
|
-
updateCursorIdeHooks(hooksPath, mode);
|
|
267
|
-
try {
|
|
268
|
-
writeProfilerConfigMerged(configPath, "cursor", {
|
|
269
|
-
enabled: true,
|
|
270
|
-
hookFile: hooksPath,
|
|
271
|
-
mode,
|
|
272
|
-
initializedAt: "",
|
|
273
|
-
}, resolvedDbPath);
|
|
274
|
-
} catch {
|
|
275
|
-
const localConfigDir = getLocalProfileDir(process.cwd());
|
|
276
|
-
ensureDir(localConfigDir);
|
|
277
|
-
configPath = path.join(localConfigDir, "config.json");
|
|
278
|
-
writeProfilerConfigMerged(configPath, "cursor", {
|
|
279
|
-
enabled: true,
|
|
280
|
-
hookFile: hooksPath,
|
|
281
|
-
mode,
|
|
282
|
-
initializedAt: "",
|
|
283
|
-
}, resolvedDbPath);
|
|
284
|
-
usedFallbackConfigPath = true;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
console.log("Agent Profiler initialized for Cursor.");
|
|
288
|
-
} else {
|
|
289
|
-
const codexDir = resolveCodexDir();
|
|
290
|
-
ensureCodexHooksFeatureFlag(codexDir);
|
|
291
|
-
hooksPath = path.join(codexDir, "hooks.json");
|
|
292
|
-
mergeCodexProfilerHooks(hooksPath, mode);
|
|
293
|
-
try {
|
|
294
|
-
writeProfilerConfigMerged(configPath, "codex", {
|
|
295
|
-
enabled: true,
|
|
296
|
-
hookFile: hooksPath,
|
|
297
|
-
mode,
|
|
298
|
-
initializedAt: "",
|
|
299
|
-
}, resolvedDbPath);
|
|
300
|
-
} catch {
|
|
301
|
-
const localConfigDir = getLocalProfileDir(process.cwd());
|
|
302
|
-
ensureDir(localConfigDir);
|
|
303
|
-
configPath = path.join(localConfigDir, "config.json");
|
|
304
|
-
writeProfilerConfigMerged(configPath, "codex", {
|
|
305
|
-
enabled: true,
|
|
306
|
-
hookFile: hooksPath,
|
|
307
|
-
mode,
|
|
308
|
-
initializedAt: "",
|
|
309
|
-
}, resolvedDbPath);
|
|
310
|
-
usedFallbackConfigPath = true;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
console.log("Agent Profiler initialized for Codex.");
|
|
314
|
-
console.log("Note: Enable hooks in Codex if prompted; project .codex/ must be trusted.");
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
console.log(`Mode: ${mode}`);
|
|
318
|
-
console.log(`Config: ${configPath}`);
|
|
319
|
-
console.log(`Database: ${resolvedDbPath}`);
|
|
320
|
-
console.log(`Hooks: ${hooksPath}`);
|
|
321
|
-
if (usedFallbackConfigPath) {
|
|
322
|
-
console.log("Note: Could not write home config in this environment; wrote local config instead.");
|
|
323
|
-
}
|
|
324
|
-
}
|
package/src/commands/last.ts
DELETED
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import { getDefaultDbPath, getEventsForLatestSession, openDb } from "../core/db.js";
|
|
2
|
-
import { runContextAudit } from "../core/contextAudit.js";
|
|
3
|
-
|
|
4
|
-
type RedFlag = {
|
|
5
|
-
severity: "HIGH" | "MEDIUM";
|
|
6
|
-
title: string;
|
|
7
|
-
detail: string;
|
|
8
|
-
recommendation: string;
|
|
9
|
-
penalty: number;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
function parsePayload(rawPayload: string): Record<string, unknown> {
|
|
13
|
-
try {
|
|
14
|
-
const parsed = JSON.parse(rawPayload);
|
|
15
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
16
|
-
return parsed as Record<string, unknown>;
|
|
17
|
-
}
|
|
18
|
-
} catch {
|
|
19
|
-
// ignore malformed payloads
|
|
20
|
-
}
|
|
21
|
-
return {};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function formatTokens(value: number): string {
|
|
25
|
-
return `~${new Intl.NumberFormat("en-US").format(value)} tokens`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function formatDuration(mins: number): string {
|
|
29
|
-
return mins < 1 ? "<1 minute" : `${mins} minute${mins === 1 ? "" : "s"}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function normalizeSnippet(value: string): string {
|
|
33
|
-
return value.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 160);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const LARGEST_EVENTS_LIMIT = 10;
|
|
37
|
-
|
|
38
|
-
export type LastReport = {
|
|
39
|
-
source: string;
|
|
40
|
-
repo: string;
|
|
41
|
-
durationMinutes: number;
|
|
42
|
-
usage: {
|
|
43
|
-
input: number;
|
|
44
|
-
output: number;
|
|
45
|
-
toolResults: number;
|
|
46
|
-
shellOutput: number;
|
|
47
|
-
total: number;
|
|
48
|
-
};
|
|
49
|
-
sessionShape: {
|
|
50
|
-
turns: number;
|
|
51
|
-
fileEdits: number;
|
|
52
|
-
shellCalls: number;
|
|
53
|
-
toolCalls: number;
|
|
54
|
-
};
|
|
55
|
-
largestEvents: Array<{
|
|
56
|
-
role: string;
|
|
57
|
-
sourceEvent: string;
|
|
58
|
-
estimatedTotalTokens: number;
|
|
59
|
-
}>;
|
|
60
|
-
efficiencyScore: number;
|
|
61
|
-
redFlags: Array<{ severity: "HIGH" | "MEDIUM"; title: string; detail: string }>;
|
|
62
|
-
recommendations: string[];
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export function getLastReport(): LastReport | null {
|
|
66
|
-
const db = openDb(getDefaultDbPath());
|
|
67
|
-
const events = getEventsForLatestSession(db);
|
|
68
|
-
db.close();
|
|
69
|
-
|
|
70
|
-
if (events.length === 0) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const first = events[0];
|
|
75
|
-
const last = events[events.length - 1];
|
|
76
|
-
const start = new Date(first.createdAt).getTime();
|
|
77
|
-
const end = new Date(last.createdAt).getTime();
|
|
78
|
-
const durationMinutes = Math.max(0, Math.round((end - start) / 60000));
|
|
79
|
-
|
|
80
|
-
let input = 0;
|
|
81
|
-
let output = 0;
|
|
82
|
-
let total = 0;
|
|
83
|
-
let shellOutput = 0;
|
|
84
|
-
let toolResults = 0;
|
|
85
|
-
|
|
86
|
-
const turnIds = new Set<string>();
|
|
87
|
-
let fileEdits = 0;
|
|
88
|
-
let shellCalls = 0;
|
|
89
|
-
let toolCalls = 0;
|
|
90
|
-
const fileEditCounts = new Map<string, number>();
|
|
91
|
-
|
|
92
|
-
const redFlags: RedFlag[] = [];
|
|
93
|
-
let recommendations: string[] = [];
|
|
94
|
-
const shellFailureBuckets = new Map<string, { runs: number; tokenTotal: number }>();
|
|
95
|
-
|
|
96
|
-
for (const event of events) {
|
|
97
|
-
input += event.estimatedInputTokens;
|
|
98
|
-
output += event.estimatedOutputTokens;
|
|
99
|
-
total += event.estimatedTotalTokens;
|
|
100
|
-
if (event.turnId) turnIds.add(event.turnId);
|
|
101
|
-
|
|
102
|
-
if (event.role === "file_edit") {
|
|
103
|
-
fileEdits += 1;
|
|
104
|
-
const payload = parsePayload(event.rawPayload);
|
|
105
|
-
const file = (payload.filePath ?? payload.path ?? payload.relativePath) as
|
|
106
|
-
| string
|
|
107
|
-
| undefined;
|
|
108
|
-
if (file) {
|
|
109
|
-
fileEditCounts.set(file, (fileEditCounts.get(file) ?? 0) + 1);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (event.role === "shell_command") shellCalls += 1;
|
|
114
|
-
if (event.role === "shell_output") {
|
|
115
|
-
shellCalls += 1;
|
|
116
|
-
shellOutput += event.estimatedTotalTokens;
|
|
117
|
-
|
|
118
|
-
const payload = parsePayload(event.rawPayload);
|
|
119
|
-
const command = typeof payload.command === "string" ? payload.command : null;
|
|
120
|
-
const stderr = typeof payload.stderr === "string" ? payload.stderr : "";
|
|
121
|
-
const stdout = typeof payload.stdout === "string" ? payload.stdout : "";
|
|
122
|
-
const outputSnippet = normalizeSnippet(stderr || stdout);
|
|
123
|
-
const looksFailed =
|
|
124
|
-
outputSnippet.includes("error") ||
|
|
125
|
-
outputSnippet.includes("failed") ||
|
|
126
|
-
outputSnippet.includes("exception") ||
|
|
127
|
-
outputSnippet.includes("traceback");
|
|
128
|
-
|
|
129
|
-
if (command && looksFailed) {
|
|
130
|
-
const key = `${normalizeSnippet(command)}|${outputSnippet}`;
|
|
131
|
-
const prev = shellFailureBuckets.get(key) ?? { runs: 0, tokenTotal: 0 };
|
|
132
|
-
shellFailureBuckets.set(key, {
|
|
133
|
-
runs: prev.runs + 1,
|
|
134
|
-
tokenTotal: prev.tokenTotal + event.estimatedTotalTokens,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (event.role === "tool_call") toolCalls += 1;
|
|
139
|
-
if (event.role === "tool_result" || event.role === "tool_failure")
|
|
140
|
-
toolResults += event.estimatedTotalTokens;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
let score = 100;
|
|
144
|
-
|
|
145
|
-
const topChurn = [...fileEditCounts.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
146
|
-
if (topChurn && topChurn[1] >= 5) {
|
|
147
|
-
redFlags.push({
|
|
148
|
-
severity: "HIGH",
|
|
149
|
-
title: "same-file churn",
|
|
150
|
-
detail: `${topChurn[0]} was edited ${topChurn[1]} times.`,
|
|
151
|
-
recommendation:
|
|
152
|
-
"Add a focused repo rule or skill note for this file's recurring failure pattern.",
|
|
153
|
-
penalty: 15,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (shellOutput > 4000) {
|
|
158
|
-
redFlags.push({
|
|
159
|
-
severity: "HIGH",
|
|
160
|
-
title: "shell output noise",
|
|
161
|
-
detail: `Shell output produced ${formatTokens(shellOutput)} in this session.`,
|
|
162
|
-
recommendation: "Capture key test/build failures once, then summarize repeated output.",
|
|
163
|
-
penalty: 12,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (toolResults > 4000) {
|
|
168
|
-
redFlags.push({
|
|
169
|
-
severity: "MEDIUM",
|
|
170
|
-
title: "large tool result",
|
|
171
|
-
detail: `Tool results produced ${formatTokens(toolResults)} in this session.`,
|
|
172
|
-
recommendation: "Request narrower tool queries and summarize oversized tool responses.",
|
|
173
|
-
penalty: 10,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const worstFailureLoop = [...shellFailureBuckets.entries()].sort(
|
|
178
|
-
(a, b) => b[1].runs - a[1].runs || b[1].tokenTotal - a[1].tokenTotal,
|
|
179
|
-
)[0];
|
|
180
|
-
if (worstFailureLoop && worstFailureLoop[1].runs >= 3) {
|
|
181
|
-
redFlags.push({
|
|
182
|
-
severity: "HIGH",
|
|
183
|
-
title: "thrashing loop",
|
|
184
|
-
detail: `A similar failing shell command looped ${worstFailureLoop[1].runs} times (${formatTokens(worstFailureLoop[1].tokenTotal)}).`,
|
|
185
|
-
recommendation: "Pause after repeated failures, capture one root error, then adjust strategy.",
|
|
186
|
-
penalty: 14,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const largestPrompt = events
|
|
191
|
-
.filter((e) => e.role === "user_prompt")
|
|
192
|
-
.reduce((max, cur) => Math.max(max, cur.estimatedTotalTokens), 0);
|
|
193
|
-
if (largestPrompt > 8000) {
|
|
194
|
-
redFlags.push({
|
|
195
|
-
severity: "MEDIUM",
|
|
196
|
-
title: "oversized prompt",
|
|
197
|
-
detail: `Largest prompt was ${formatTokens(largestPrompt)}.`,
|
|
198
|
-
recommendation: "Split goals into smaller requests and load reference docs on demand.",
|
|
199
|
-
penalty: 8,
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (total > 12000 && fileEdits === 0) {
|
|
204
|
-
redFlags.push({
|
|
205
|
-
severity: "MEDIUM",
|
|
206
|
-
title: "low-signal session",
|
|
207
|
-
detail: `High observable usage (${formatTokens(total)}) with no file edits.`,
|
|
208
|
-
recommendation: "Push for earlier implementation checkpoints instead of extended analysis.",
|
|
209
|
-
penalty: 10,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const contextAudit = runContextAudit(process.cwd());
|
|
214
|
-
if (contextAudit.totalEstimatedTokens > 6000) {
|
|
215
|
-
redFlags.push({
|
|
216
|
-
severity: "MEDIUM",
|
|
217
|
-
title: "context bloat",
|
|
218
|
-
detail: `Always-on instruction files estimate ${formatTokens(contextAudit.totalEstimatedTokens)}.`,
|
|
219
|
-
recommendation: "Move large static references into on-demand skills or targeted commands.",
|
|
220
|
-
penalty: 10,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
for (const flag of redFlags) score -= flag.penalty;
|
|
225
|
-
score = Math.max(0, score);
|
|
226
|
-
recommendations = [...new Set(redFlags.map((r) => r.recommendation))];
|
|
227
|
-
if (recommendations.length === 0) {
|
|
228
|
-
recommendations = [
|
|
229
|
-
"No major waste pattern detected. Keep prompts scoped and continue tracking trends.",
|
|
230
|
-
];
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const largestEvents = [...events]
|
|
234
|
-
.sort((a, b) => b.estimatedTotalTokens - a.estimatedTotalTokens)
|
|
235
|
-
.slice(0, LARGEST_EVENTS_LIMIT)
|
|
236
|
-
.map((e) => ({
|
|
237
|
-
role: e.role,
|
|
238
|
-
sourceEvent: e.sourceEvent,
|
|
239
|
-
estimatedTotalTokens: e.estimatedTotalTokens,
|
|
240
|
-
}));
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
source: first.source,
|
|
244
|
-
repo: first.repoPath ?? process.cwd(),
|
|
245
|
-
durationMinutes,
|
|
246
|
-
usage: { input, output, toolResults, shellOutput, total },
|
|
247
|
-
sessionShape: {
|
|
248
|
-
turns: turnIds.size,
|
|
249
|
-
fileEdits,
|
|
250
|
-
shellCalls,
|
|
251
|
-
toolCalls,
|
|
252
|
-
},
|
|
253
|
-
largestEvents,
|
|
254
|
-
efficiencyScore: score,
|
|
255
|
-
redFlags: redFlags.map((flag) => ({
|
|
256
|
-
severity: flag.severity,
|
|
257
|
-
title: flag.title,
|
|
258
|
-
detail: flag.detail,
|
|
259
|
-
})),
|
|
260
|
-
recommendations,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export function runLast(): void {
|
|
265
|
-
const report = getLastReport();
|
|
266
|
-
if (!report) {
|
|
267
|
-
console.log("Agent Profiler: Last Session\n\nNo events captured yet.");
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const lines: string[] = [];
|
|
272
|
-
lines.push("Agent Profiler: Last Session");
|
|
273
|
-
lines.push("");
|
|
274
|
-
lines.push("Source:");
|
|
275
|
-
lines.push(` ${report.source}`);
|
|
276
|
-
lines.push("");
|
|
277
|
-
lines.push("Repo:");
|
|
278
|
-
lines.push(` ${report.repo}`);
|
|
279
|
-
lines.push("");
|
|
280
|
-
lines.push("Duration:");
|
|
281
|
-
lines.push(` ${formatDuration(report.durationMinutes)}`);
|
|
282
|
-
lines.push("");
|
|
283
|
-
lines.push("Observable usage:");
|
|
284
|
-
lines.push(` Input: ${formatTokens(report.usage.input)}`);
|
|
285
|
-
lines.push(` Output: ${formatTokens(report.usage.output)}`);
|
|
286
|
-
lines.push(` Tool results: ${formatTokens(report.usage.toolResults)}`);
|
|
287
|
-
lines.push(` Shell output: ${formatTokens(report.usage.shellOutput)}`);
|
|
288
|
-
lines.push(` Total: ${formatTokens(report.usage.total)}`);
|
|
289
|
-
lines.push("");
|
|
290
|
-
lines.push("Session shape:");
|
|
291
|
-
lines.push(` Turns: ${report.sessionShape.turns}`);
|
|
292
|
-
lines.push(` File edits: ${report.sessionShape.fileEdits}`);
|
|
293
|
-
lines.push(` Shell calls: ${report.sessionShape.shellCalls}`);
|
|
294
|
-
lines.push(` Tool calls: ${report.sessionShape.toolCalls}`);
|
|
295
|
-
lines.push("");
|
|
296
|
-
lines.push(`Largest events (top ${LARGEST_EVENTS_LIMIT} by observable tokens):`);
|
|
297
|
-
if (report.largestEvents.length === 0) {
|
|
298
|
-
lines.push(" none");
|
|
299
|
-
} else {
|
|
300
|
-
for (const row of report.largestEvents) {
|
|
301
|
-
lines.push(
|
|
302
|
-
` ${row.role} / ${row.sourceEvent} ${formatTokens(row.estimatedTotalTokens)}`,
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
lines.push("");
|
|
307
|
-
lines.push("Efficiency score:");
|
|
308
|
-
lines.push(` ${report.efficiencyScore} / 100`);
|
|
309
|
-
lines.push("");
|
|
310
|
-
lines.push("Red flags:");
|
|
311
|
-
if (report.redFlags.length === 0) {
|
|
312
|
-
lines.push(" none");
|
|
313
|
-
} else {
|
|
314
|
-
for (const flag of report.redFlags) {
|
|
315
|
-
lines.push(` ${flag.severity} ${flag.title}`);
|
|
316
|
-
lines.push(` ${flag.detail}`);
|
|
317
|
-
lines.push("");
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
lines.push("Recommendations:");
|
|
321
|
-
report.recommendations.forEach((recommendation, index) => {
|
|
322
|
-
lines.push(` ${index + 1}. ${recommendation}`);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
console.log(lines.join("\n"));
|
|
326
|
-
}
|