@ericsanchezok/synergy-workbench 0.0.0-stage → 3.1.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/AGENTS.md +30 -0
- package/README.md +16 -2
- package/dist/agents.js +11 -0
- package/dist/cli-adapter.js +8 -0
- package/dist/component.js +65 -0
- package/dist/config-schema.js +265 -0
- package/dist/execution/activity.js +78 -0
- package/dist/execution/content.js +184 -0
- package/dist/execution/json-sections.js +231 -0
- package/dist/execution/presentation.js +79 -0
- package/dist/execution/preview.js +13 -0
- package/dist/execution/process.js +125 -0
- package/dist/execution/projection.js +327 -0
- package/dist/execution/query.js +18 -0
- package/dist/execution/routes.js +189 -0
- package/dist/execution/schema.js +322 -0
- package/dist/execution/service.js +1993 -0
- package/dist/features.js +22 -0
- package/dist/http.js +65 -0
- package/dist/performance/analysis.js +330 -0
- package/dist/performance/catalog.js +391 -0
- package/dist/performance/dashboard.js +483 -0
- package/dist/performance/error.js +15 -0
- package/dist/performance/index.js +6 -0
- package/dist/performance/inflight.js +43 -0
- package/dist/performance/projection.js +138 -0
- package/dist/performance/prompt/base.txt +38 -0
- package/dist/performance/prompt/builder.js +21 -0
- package/dist/performance/routes/performance-route.js +439 -0
- package/dist/performance/schema.js +628 -0
- package/dist/performance/timeline.js +98 -0
- package/dist/performance/trace-detail.js +91 -0
- package/dist/presentation/service.js +270 -0
- package/dist/presentation/tools/workbench-view.js +65 -0
- package/dist/presentation/tools.js +10 -0
- package/dist/project/directories.js +363 -0
- package/dist/project/git-health.js +498 -0
- package/dist/project/ide.js +66 -0
- package/dist/project/migration.js +22 -0
- package/dist/project/routes/directories.js +89 -0
- package/dist/project/routes/git.js +30 -0
- package/dist/project/routes/task-defaults.js +42 -0
- package/dist/project/session-health.js +9 -0
- package/dist/project/startup.js +28 -0
- package/dist/project/task-defaults.js +68 -0
- package/dist/project/tools/session-control.js +664 -0
- package/dist/project/tools/session-control.txt +60 -0
- package/dist/project/tools/worktree-archive.js +76 -0
- package/dist/project/tools/worktree-enter.js +175 -0
- package/dist/project/tools/worktree-leave.js +119 -0
- package/dist/project/tools/worktree-list.js +52 -0
- package/dist/project/tools.js +26 -0
- package/dist/project/vcs-branch-watcher.js +76 -0
- package/dist/project/vcs.js +114 -0
- package/dist/project/worktree-janitor.js +140 -0
- package/dist/project/worktrees.js +98 -0
- package/dist/push/bridge.js +171 -0
- package/dist/push/index.js +4 -0
- package/dist/push/routes/push.js +224 -0
- package/dist/push/service.js +93 -0
- package/dist/push/store.js +121 -0
- package/dist/push/types.js +58 -0
- package/dist/review/git.js +226 -0
- package/dist/review/routes.js +45 -0
- package/dist/review/schema.js +32 -0
- package/dist/review/state.js +79 -0
- package/dist/stats/aggregator.js +252 -0
- package/dist/stats/canonical.js +89 -0
- package/dist/stats/cli/stats.js +302 -0
- package/dist/stats/engine.js +176 -0
- package/dist/stats/index.js +5 -0
- package/dist/stats/rollup.js +460 -0
- package/dist/stats/routes/stats.js +94 -0
- package/dist/stats/routes/usage.js +83 -0
- package/dist/stats/storage.js +86 -0
- package/dist/stats/types.js +267 -0
- package/dist/worker.js +6 -0
- package/package.json +232 -4
- package/src/agents.ts +15 -0
- package/src/cli-adapter.ts +11 -0
- package/src/component.ts +73 -0
- package/src/config-schema.ts +289 -0
- package/src/execution/activity.ts +83 -0
- package/src/execution/content.ts +189 -0
- package/src/execution/json-sections.ts +230 -0
- package/src/execution/presentation.ts +91 -0
- package/src/execution/preview.ts +12 -0
- package/src/execution/process.ts +118 -0
- package/src/execution/projection.ts +330 -0
- package/src/execution/query.ts +19 -0
- package/src/execution/routes.ts +324 -0
- package/src/execution/schema.ts +336 -0
- package/src/execution/service.ts +2149 -0
- package/src/features.ts +22 -0
- package/src/http.ts +75 -0
- package/src/performance/analysis.ts +356 -0
- package/src/performance/catalog.ts +902 -0
- package/src/performance/dashboard.ts +521 -0
- package/src/performance/error.ts +32 -0
- package/src/performance/index.ts +6 -0
- package/src/performance/inflight.ts +46 -0
- package/src/performance/projection.ts +143 -0
- package/src/performance/prompt/base.txt +38 -0
- package/src/performance/prompt/builder.ts +24 -0
- package/src/performance/routes/performance-route.ts +541 -0
- package/src/performance/schema.ts +695 -0
- package/src/performance/timeline.ts +100 -0
- package/src/performance/trace-detail.ts +97 -0
- package/src/presentation/service.ts +303 -0
- package/src/presentation/tools/workbench-view.ts +75 -0
- package/src/presentation/tools.ts +11 -0
- package/src/project/directories.ts +391 -0
- package/src/project/git-health.ts +577 -0
- package/src/project/ide.ts +80 -0
- package/src/project/migration.ts +24 -0
- package/src/project/routes/directories.ts +124 -0
- package/src/project/routes/git.ts +42 -0
- package/src/project/routes/task-defaults.ts +50 -0
- package/src/project/session-health.ts +10 -0
- package/src/project/startup.ts +28 -0
- package/src/project/task-defaults.ts +70 -0
- package/src/project/tools/session-control.ts +725 -0
- package/src/project/tools/session-control.txt +60 -0
- package/src/project/tools/worktree-archive.ts +92 -0
- package/src/project/tools/worktree-enter.ts +227 -0
- package/src/project/tools/worktree-leave.ts +140 -0
- package/src/project/tools/worktree-list.ts +68 -0
- package/src/project/tools.ts +29 -0
- package/src/project/vcs-branch-watcher.ts +81 -0
- package/src/project/vcs.ts +124 -0
- package/src/project/worktree-janitor.ts +153 -0
- package/src/project/worktrees.ts +110 -0
- package/src/push/bridge.ts +182 -0
- package/src/push/index.ts +4 -0
- package/src/push/routes/push.ts +267 -0
- package/src/push/service.ts +108 -0
- package/src/push/store.ts +120 -0
- package/src/push/types.ts +67 -0
- package/src/review/git.ts +221 -0
- package/src/review/routes.ts +65 -0
- package/src/review/schema.ts +34 -0
- package/src/review/state.ts +77 -0
- package/src/stats/aggregator.ts +277 -0
- package/src/stats/canonical.ts +93 -0
- package/src/stats/cli/stats.ts +348 -0
- package/src/stats/engine.ts +197 -0
- package/src/stats/index.ts +5 -0
- package/src/stats/rollup.ts +522 -0
- package/src/stats/routes/stats.ts +107 -0
- package/src/stats/routes/usage.ts +103 -0
- package/src/stats/storage.ts +78 -0
- package/src/stats/types.ts +351 -0
- package/src/worker.ts +7 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Usage } from "@ericsanchezok/synergy-harness/usage";
|
|
2
|
+
import { RolloutAccounting } from "@ericsanchezok/synergy-harness/session/rollout/accounting";
|
|
3
|
+
import { ExecutionPresentation } from "../execution/presentation.js";
|
|
4
|
+
export var CanonicalStats;
|
|
5
|
+
(function (CanonicalStats) {
|
|
6
|
+
function projection(accounting) {
|
|
7
|
+
return {
|
|
8
|
+
...RolloutAccounting.project(accounting),
|
|
9
|
+
cost: RolloutAccounting.knownExpense(accounting),
|
|
10
|
+
costPresentation: ExecutionPresentation.cost(accounting),
|
|
11
|
+
accounting,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async function apply(snapshot) {
|
|
15
|
+
const usage = await Usage.summary();
|
|
16
|
+
const days = new Map(snapshot.timeSeries.days.map((day) => [
|
|
17
|
+
day.day,
|
|
18
|
+
{
|
|
19
|
+
...day,
|
|
20
|
+
...projection(RolloutAccounting.empty()),
|
|
21
|
+
toolCalls: 0,
|
|
22
|
+
},
|
|
23
|
+
]));
|
|
24
|
+
for (const day of usage.daily)
|
|
25
|
+
days.set(day.date, {
|
|
26
|
+
day: day.date,
|
|
27
|
+
sessions: 0,
|
|
28
|
+
turns: 0,
|
|
29
|
+
additions: 0,
|
|
30
|
+
deletions: 0,
|
|
31
|
+
files: 0,
|
|
32
|
+
errors: 0,
|
|
33
|
+
...days.get(day.date),
|
|
34
|
+
...projection(day.accounting),
|
|
35
|
+
toolCalls: day.toolCalls,
|
|
36
|
+
});
|
|
37
|
+
const total = projection(usage.accounting);
|
|
38
|
+
const turns = snapshot.overview.totalTurns;
|
|
39
|
+
const activeDays = usage.daily.filter((day) => day.accounting.calls || day.accounting.legacy.messages).length || 1;
|
|
40
|
+
return {
|
|
41
|
+
...snapshot,
|
|
42
|
+
tokenCost: {
|
|
43
|
+
...total,
|
|
44
|
+
cacheHitRate: usage.cache.ratio ?? usage.cache.observedRatio ?? 0,
|
|
45
|
+
avgCostPerTurn: turns ? total.cost / turns : 0,
|
|
46
|
+
avgTokensPerTurn: turns ? usage.accounting.tokens.total.known / turns : 0,
|
|
47
|
+
dailyCost: total.cost / activeDays,
|
|
48
|
+
dailyTokens: usage.accounting.tokens.total.known / activeDays,
|
|
49
|
+
},
|
|
50
|
+
models: {
|
|
51
|
+
models: usage.models.map((model) => {
|
|
52
|
+
const previous = snapshot.models.models.find((value) => value.providerID === model.providerID && value.modelID === model.modelID);
|
|
53
|
+
return {
|
|
54
|
+
providerID: model.providerID,
|
|
55
|
+
modelID: model.modelID,
|
|
56
|
+
messages: previous?.messages ?? 0,
|
|
57
|
+
turns: previous?.turns ?? 0,
|
|
58
|
+
avgResponseMs: previous?.avgResponseMs ?? 0,
|
|
59
|
+
...projection(model.accounting),
|
|
60
|
+
};
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
agents: {
|
|
64
|
+
...snapshot.agents,
|
|
65
|
+
agents: Object.entries(usage.agents).map(([agent, accounting]) => {
|
|
66
|
+
const previous = snapshot.agents.agents.find((value) => value.agent === agent);
|
|
67
|
+
return {
|
|
68
|
+
agent,
|
|
69
|
+
messages: previous?.messages ?? 0,
|
|
70
|
+
sessions: previous?.sessions ?? 0,
|
|
71
|
+
subagentInvocations: previous?.subagentInvocations ?? 0,
|
|
72
|
+
...projection(accounting),
|
|
73
|
+
};
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
tools: {
|
|
77
|
+
tools: usage.tools.map((tool) => ({
|
|
78
|
+
tool: tool.tool,
|
|
79
|
+
calls: tool.calls,
|
|
80
|
+
successes: tool.completed,
|
|
81
|
+
errors: tool.failed,
|
|
82
|
+
avgDurationMs: tool.averageMs ?? 0,
|
|
83
|
+
})),
|
|
84
|
+
},
|
|
85
|
+
timeSeries: { ...snapshot.timeSeries, days: [...days.values()].sort((a, b) => a.day.localeCompare(b.day)) },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
CanonicalStats.apply = apply;
|
|
89
|
+
})(CanonicalStats || (CanonicalStats = {}));
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { cmd } from "@ericsanchezok/synergy-util/cli-command";
|
|
2
|
+
import { UI } from "@ericsanchezok/synergy-util/terminal";
|
|
3
|
+
import { Engine } from "../index.js";
|
|
4
|
+
import { RolloutQuery } from "@ericsanchezok/synergy-harness/session/rollout/query";
|
|
5
|
+
export const StatsCommand = cmd({
|
|
6
|
+
command: "stats",
|
|
7
|
+
describe: "show token usage and cost statistics",
|
|
8
|
+
builder: (yargs) => {
|
|
9
|
+
return yargs
|
|
10
|
+
.option("run", { type: "string", describe: "Show one run and its descendant accounting" })
|
|
11
|
+
.option("compare", {
|
|
12
|
+
type: "string",
|
|
13
|
+
array: true,
|
|
14
|
+
nargs: 2,
|
|
15
|
+
describe: "Compare two runs without inferring task quality",
|
|
16
|
+
})
|
|
17
|
+
.option("days", {
|
|
18
|
+
describe: "show stats for the last N days (default: all time)",
|
|
19
|
+
type: "number",
|
|
20
|
+
})
|
|
21
|
+
.option("tools", {
|
|
22
|
+
describe: "number of tools to show (default: all)",
|
|
23
|
+
type: "number",
|
|
24
|
+
})
|
|
25
|
+
.option("models", {
|
|
26
|
+
describe: "show model statistics (default: hidden). Pass a number to show top N, otherwise shows all",
|
|
27
|
+
})
|
|
28
|
+
.option("project", {
|
|
29
|
+
describe: "filter by project (default: all projects, empty string: current project)",
|
|
30
|
+
type: "string",
|
|
31
|
+
})
|
|
32
|
+
.option("recompute", {
|
|
33
|
+
describe: "force full recompute from scratch",
|
|
34
|
+
type: "boolean",
|
|
35
|
+
default: false,
|
|
36
|
+
})
|
|
37
|
+
.option("json", {
|
|
38
|
+
describe: "output raw JSON instead of formatted display",
|
|
39
|
+
type: "boolean",
|
|
40
|
+
default: false,
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
handler: async (args) => {
|
|
44
|
+
if (args.run || args.compare) {
|
|
45
|
+
if (args.run && args.compare)
|
|
46
|
+
throw new Error("Use either --run or --compare");
|
|
47
|
+
const result = args.run
|
|
48
|
+
? await RolloutQuery.find(args.run)
|
|
49
|
+
: await Promise.all(args.compare.map((id) => RolloutQuery.find(id)));
|
|
50
|
+
console.log(JSON.stringify(Array.isArray(result) ? RolloutQuery.compare(result[0], result[1]) : result, null, 2));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const onProgress = (event) => {
|
|
54
|
+
if (args.json)
|
|
55
|
+
return;
|
|
56
|
+
const pct = event.total > 0 ? Math.round((event.current / event.total) * 100) : 0;
|
|
57
|
+
const bar = UI.progressBar({ ratio: event.current / Math.max(1, event.total), width: 30, brackets: true });
|
|
58
|
+
const msg = event.message ?? event.phase;
|
|
59
|
+
process.stdout.write(`\r ${bar} ${pct}% ${msg}`);
|
|
60
|
+
if (event.current === event.total && event.phase === "snapshot") {
|
|
61
|
+
process.stdout.write("\r\x1B[K");
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
let snapshot;
|
|
65
|
+
if (args.project !== undefined) {
|
|
66
|
+
console.log("Note: project-scoped filtering requires full recomputation.");
|
|
67
|
+
snapshot = await Engine.recompute(onProgress);
|
|
68
|
+
}
|
|
69
|
+
else if (args.recompute) {
|
|
70
|
+
snapshot = await Engine.recompute(onProgress);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
snapshot = await Engine.update(onProgress);
|
|
74
|
+
}
|
|
75
|
+
// Apply --days filter client-side on time series
|
|
76
|
+
if (args.days !== undefined) {
|
|
77
|
+
snapshot = filterByDays(snapshot, args.days);
|
|
78
|
+
}
|
|
79
|
+
if (args.json) {
|
|
80
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let modelLimit;
|
|
84
|
+
if (args.models === true) {
|
|
85
|
+
modelLimit = Infinity;
|
|
86
|
+
}
|
|
87
|
+
else if (typeof args.models === "number") {
|
|
88
|
+
modelLimit = args.models;
|
|
89
|
+
}
|
|
90
|
+
displayStats(snapshot, args.tools, modelLimit);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
function filterByDays(snapshot, days) {
|
|
94
|
+
const MS_IN_DAY = 24 * 60 * 60 * 1000;
|
|
95
|
+
const cutoff = days === 0
|
|
96
|
+
? (() => {
|
|
97
|
+
const d = new Date();
|
|
98
|
+
d.setHours(0, 0, 0, 0);
|
|
99
|
+
return d.getTime();
|
|
100
|
+
})()
|
|
101
|
+
: Date.now() - days * MS_IN_DAY;
|
|
102
|
+
const filteredDays = snapshot.timeSeries.days.filter((d) => new Date(d.day).getTime() >= cutoff);
|
|
103
|
+
return {
|
|
104
|
+
...snapshot,
|
|
105
|
+
timeSeries: {
|
|
106
|
+
...snapshot.timeSeries,
|
|
107
|
+
days: filteredDays,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function displayStats(snapshot, toolLimit, modelLimit) {
|
|
112
|
+
const width = 72;
|
|
113
|
+
function renderRow(label, value) {
|
|
114
|
+
const availableWidth = width - 1;
|
|
115
|
+
const paddingNeeded = availableWidth - label.length - value.length;
|
|
116
|
+
const padding = Math.max(0, paddingNeeded);
|
|
117
|
+
return `│${label}${" ".repeat(padding)}${value} │`;
|
|
118
|
+
}
|
|
119
|
+
function renderHeader(title) {
|
|
120
|
+
const innerWidth = width - 2;
|
|
121
|
+
const padTotal = innerWidth - title.length;
|
|
122
|
+
const padLeft = Math.floor(padTotal / 2);
|
|
123
|
+
const padRight = padTotal - padLeft;
|
|
124
|
+
console.log(`│${" ".repeat(padLeft)}${title}${" ".repeat(padRight)}│`);
|
|
125
|
+
}
|
|
126
|
+
function topBorder() {
|
|
127
|
+
console.log(`┌${"─".repeat(width - 2)}┐`);
|
|
128
|
+
}
|
|
129
|
+
function divider() {
|
|
130
|
+
console.log(`├${"─".repeat(width - 2)}┤`);
|
|
131
|
+
}
|
|
132
|
+
function bottomBorder() {
|
|
133
|
+
console.log(`└${"─".repeat(width - 2)}┘`);
|
|
134
|
+
}
|
|
135
|
+
const { overview, tokenCost, models, agents, tools, codeChanges, lifecycle, channels } = snapshot;
|
|
136
|
+
// ── Overview ──────────────────────────────────────────────
|
|
137
|
+
topBorder();
|
|
138
|
+
renderHeader("OVERVIEW");
|
|
139
|
+
divider();
|
|
140
|
+
console.log(renderRow("Sessions", `${overview.totalSessions.toLocaleString()} (active: ${overview.activeSessions}, archived: ${overview.archivedSessions})`));
|
|
141
|
+
console.log(renderRow("Messages", overview.totalMessages.toLocaleString()));
|
|
142
|
+
console.log(renderRow("Turns", overview.totalTurns.toLocaleString()));
|
|
143
|
+
console.log(renderRow("Days Active", overview.totalDays.toString()));
|
|
144
|
+
console.log(renderRow("Longest Streak", overview.longestStreak.toString()));
|
|
145
|
+
console.log(renderRow("Current Streak", overview.currentStreak.toString()));
|
|
146
|
+
console.log(renderRow("Projects", overview.projectCount.toString()));
|
|
147
|
+
bottomBorder();
|
|
148
|
+
console.log();
|
|
149
|
+
// ── Tokens & Cost ─────────────────────────────────────────
|
|
150
|
+
topBorder();
|
|
151
|
+
renderHeader("TOKENS & COST");
|
|
152
|
+
divider();
|
|
153
|
+
console.log(renderRow("Known API Estimate", `$${tokenCost.cost.toFixed(2)}`));
|
|
154
|
+
if (tokenCost.accounting) {
|
|
155
|
+
const accounting = tokenCost.accounting;
|
|
156
|
+
console.log(renderRow("Unpriced API Attempts", accounting.apiEstimate.unknown.toString()));
|
|
157
|
+
console.log(renderRow("Unobserved Calls", accounting.unobservedCalls.toString()));
|
|
158
|
+
console.log(renderRow("Subscription API Equivalent", `$${accounting.subscriptionEquivalent.known.toFixed(4)}`));
|
|
159
|
+
console.log(renderRow("Unknown Subscription Estimates", accounting.subscriptionEquivalent.unknown.toString()));
|
|
160
|
+
console.log(renderRow("Historical Calculation", `$${accounting.legacy.cost.toFixed(4)}`));
|
|
161
|
+
for (const [currency, amount] of Object.entries(accounting.reported.currencies))
|
|
162
|
+
console.log(renderRow(`Provider Reported (${currency})`, amount.toFixed(4)));
|
|
163
|
+
}
|
|
164
|
+
console.log(renderRow("Avg Cost/Turn", `$${tokenCost.avgCostPerTurn.toFixed(4)}`));
|
|
165
|
+
console.log(renderRow("Daily Cost", `$${tokenCost.dailyCost.toFixed(2)}`));
|
|
166
|
+
console.log(renderRow("Input Tokens", formatNumber(tokenCost.tokens.input)));
|
|
167
|
+
console.log(renderRow("Output Tokens", formatNumber(tokenCost.tokens.output)));
|
|
168
|
+
console.log(renderRow("Reasoning (included in output)", formatNumber(tokenCost.tokens.reasoning)));
|
|
169
|
+
console.log(renderRow("Cache Read", formatNumber(tokenCost.tokens.cache.read)));
|
|
170
|
+
console.log(renderRow("Cache Write", formatNumber(tokenCost.tokens.cache.write)));
|
|
171
|
+
console.log(renderRow("Cache Hit Rate", `${(tokenCost.cacheHitRate * 100).toFixed(1)}%`));
|
|
172
|
+
console.log(renderRow("Avg Tokens/Turn", formatNumber(Math.round(tokenCost.avgTokensPerTurn))));
|
|
173
|
+
console.log(renderRow("Daily Tokens", formatNumber(Math.round(tokenCost.dailyTokens))));
|
|
174
|
+
bottomBorder();
|
|
175
|
+
console.log();
|
|
176
|
+
// ── Models ────────────────────────────────────────────────
|
|
177
|
+
if (modelLimit !== undefined && models.models.length > 0) {
|
|
178
|
+
const sorted = [...models.models].sort((a, b) => b.messages - a.messages);
|
|
179
|
+
const toDisplay = modelLimit === Infinity ? sorted : sorted.slice(0, modelLimit);
|
|
180
|
+
topBorder();
|
|
181
|
+
renderHeader("MODEL USAGE");
|
|
182
|
+
divider();
|
|
183
|
+
for (const m of toDisplay) {
|
|
184
|
+
const modelKey = `${m.providerID}/${m.modelID}`;
|
|
185
|
+
console.log(renderRow(modelKey, ""));
|
|
186
|
+
console.log(renderRow(" Messages", m.messages.toLocaleString()));
|
|
187
|
+
console.log(renderRow(" Input Tokens", formatNumber(m.tokens.input)));
|
|
188
|
+
console.log(renderRow(" Output Tokens", formatNumber(m.tokens.output)));
|
|
189
|
+
console.log(renderRow(" Known API Estimate", `$${m.cost.toFixed(4)}`));
|
|
190
|
+
console.log(renderRow(" Avg Response", `${m.avgResponseMs.toFixed(0)}ms`));
|
|
191
|
+
divider();
|
|
192
|
+
}
|
|
193
|
+
// Replace last divider with bottom border
|
|
194
|
+
process.stdout.write("\x1B[1A");
|
|
195
|
+
bottomBorder();
|
|
196
|
+
console.log();
|
|
197
|
+
}
|
|
198
|
+
// ── Agents ────────────────────────────────────────────────
|
|
199
|
+
if (agents.agents.length > 0) {
|
|
200
|
+
const sorted = [...agents.agents].sort((a, b) => b.messages - a.messages);
|
|
201
|
+
topBorder();
|
|
202
|
+
renderHeader("AGENT USAGE");
|
|
203
|
+
divider();
|
|
204
|
+
for (const a of sorted) {
|
|
205
|
+
console.log(renderRow(a.agent, ""));
|
|
206
|
+
console.log(renderRow(" Messages", a.messages.toLocaleString()));
|
|
207
|
+
console.log(renderRow(" Sessions", a.sessions.toLocaleString()));
|
|
208
|
+
console.log(renderRow(" Known API Estimate", `$${a.cost.toFixed(4)}`));
|
|
209
|
+
console.log(renderRow(" Subagent Calls", a.subagentInvocations.toLocaleString()));
|
|
210
|
+
divider();
|
|
211
|
+
}
|
|
212
|
+
process.stdout.write("\x1B[1A");
|
|
213
|
+
bottomBorder();
|
|
214
|
+
console.log();
|
|
215
|
+
if (agents.totalSubagentCalls > 0) {
|
|
216
|
+
console.log(` Total subagent calls: ${agents.totalSubagentCalls.toLocaleString()}`);
|
|
217
|
+
console.log();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// ── Tools ─────────────────────────────────────────────────
|
|
221
|
+
if (tools.tools.length > 0) {
|
|
222
|
+
const sorted = [...tools.tools].sort((a, b) => b.calls - a.calls);
|
|
223
|
+
const toDisplay = toolLimit ? sorted.slice(0, toolLimit) : sorted;
|
|
224
|
+
const maxCalls = Math.max(...toDisplay.map((t) => t.calls));
|
|
225
|
+
const totalCalls = tools.tools.reduce((sum, t) => sum + t.calls, 0);
|
|
226
|
+
topBorder();
|
|
227
|
+
renderHeader("TOOL USAGE");
|
|
228
|
+
divider();
|
|
229
|
+
for (const t of toDisplay) {
|
|
230
|
+
const ratio = t.calls / maxCalls;
|
|
231
|
+
const bar = UI.progressBar({ ratio, width: 20, brackets: false });
|
|
232
|
+
const percentage = totalCalls > 0 ? ((t.calls / totalCalls) * 100).toFixed(1) : "0.0";
|
|
233
|
+
const successRate = t.calls > 0 ? ((t.successes / t.calls) * 100).toFixed(0) : "—";
|
|
234
|
+
const maxToolLength = 18;
|
|
235
|
+
const truncatedTool = t.tool.length > maxToolLength ? t.tool.substring(0, maxToolLength - 2) + ".." : t.tool;
|
|
236
|
+
const toolName = truncatedTool.padEnd(maxToolLength);
|
|
237
|
+
const content = ` ${toolName} ${bar.padEnd(20)} ${t.calls.toString().padStart(4)} (${percentage.padStart(4)}%) ${successRate.padStart(3)}%ok`;
|
|
238
|
+
const padding = Math.max(0, width - content.length - 1);
|
|
239
|
+
console.log(`│${content}${" ".repeat(padding)} │`);
|
|
240
|
+
}
|
|
241
|
+
divider();
|
|
242
|
+
const avgDurLine = ` Avg duration per tool (ms):`;
|
|
243
|
+
console.log(`│${avgDurLine}${" ".repeat(width - avgDurLine.length - 2)} │`);
|
|
244
|
+
for (const t of toDisplay) {
|
|
245
|
+
const durText = ` ${t.tool}: ${t.avgDurationMs.toFixed(0)}ms`;
|
|
246
|
+
const padding = Math.max(0, width - durText.length - 2);
|
|
247
|
+
console.log(`│${durText}${" ".repeat(padding)} │`);
|
|
248
|
+
}
|
|
249
|
+
bottomBorder();
|
|
250
|
+
console.log();
|
|
251
|
+
}
|
|
252
|
+
// ── Code Changes ──────────────────────────────────────────
|
|
253
|
+
if (codeChanges.totalAdditions > 0 || codeChanges.totalDeletions > 0) {
|
|
254
|
+
topBorder();
|
|
255
|
+
renderHeader("CODE CHANGES");
|
|
256
|
+
divider();
|
|
257
|
+
console.log(renderRow("Additions", formatNumber(codeChanges.totalAdditions)));
|
|
258
|
+
console.log(renderRow("Deletions", formatNumber(codeChanges.totalDeletions)));
|
|
259
|
+
console.log(renderRow("Net Lines", formatNumber(codeChanges.netLines)));
|
|
260
|
+
console.log(renderRow("Files Touched", formatNumber(codeChanges.totalFiles)));
|
|
261
|
+
console.log(renderRow("Daily Additions", formatNumber(Math.round(codeChanges.dailyAdditions))));
|
|
262
|
+
console.log(renderRow("Daily Deletions", formatNumber(Math.round(codeChanges.dailyDeletions))));
|
|
263
|
+
bottomBorder();
|
|
264
|
+
console.log();
|
|
265
|
+
}
|
|
266
|
+
// ── Lifecycle ─────────────────────────────────────────────
|
|
267
|
+
topBorder();
|
|
268
|
+
renderHeader("LIFECYCLE");
|
|
269
|
+
divider();
|
|
270
|
+
console.log(renderRow("Pinned Sessions", lifecycle.pinnedCount.toLocaleString()));
|
|
271
|
+
console.log(renderRow("Avg Turns/Session", lifecycle.avgTurnsPerSession.toFixed(1)));
|
|
272
|
+
console.log(renderRow("Median Turns/Session", lifecycle.medianTurnsPerSession.toFixed(1)));
|
|
273
|
+
console.log(renderRow("Compactions", lifecycle.compactionCount.toLocaleString()));
|
|
274
|
+
console.log(renderRow("Retries", lifecycle.retryCount.toLocaleString()));
|
|
275
|
+
console.log(renderRow("Error Rate", `${(lifecycle.errorRate * 100).toFixed(1)}%`));
|
|
276
|
+
console.log(renderRow("Duration (short/med/long)", `${lifecycle.durationBuckets.short}/${lifecycle.durationBuckets.medium}/${lifecycle.durationBuckets.long}`));
|
|
277
|
+
bottomBorder();
|
|
278
|
+
console.log();
|
|
279
|
+
// ── Channels ──────────────────────────────────────────────
|
|
280
|
+
if (channels.channels.length > 0) {
|
|
281
|
+
topBorder();
|
|
282
|
+
renderHeader("CHANNELS");
|
|
283
|
+
divider();
|
|
284
|
+
for (const c of channels.channels) {
|
|
285
|
+
console.log(renderRow(c.channel, `sessions: ${c.sessions}, messages: ${c.messages}`));
|
|
286
|
+
}
|
|
287
|
+
divider();
|
|
288
|
+
console.log(renderRow("Interactive", channels.interactiveSessions.toLocaleString()));
|
|
289
|
+
console.log(renderRow("Unattended", channels.unattendedSessions.toLocaleString()));
|
|
290
|
+
bottomBorder();
|
|
291
|
+
console.log();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function formatNumber(num) {
|
|
295
|
+
if (num >= 1000000) {
|
|
296
|
+
return (num / 1000000).toFixed(1) + "M";
|
|
297
|
+
}
|
|
298
|
+
else if (num >= 1000) {
|
|
299
|
+
return (num / 1000).toFixed(1) + "K";
|
|
300
|
+
}
|
|
301
|
+
return num.toString();
|
|
302
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { RolloutSnapshot } from "@ericsanchezok/synergy-harness/session/rollout/snapshot";
|
|
2
|
+
import { OperationDigest } from "./types.js";
|
|
3
|
+
import { Lock } from "@ericsanchezok/synergy-harness/util/lock";
|
|
4
|
+
import { readRolloutRevision } from "@ericsanchezok/synergy-harness/rollout";
|
|
5
|
+
import { Storage } from "@ericsanchezok/synergy-harness/storage/storage";
|
|
6
|
+
import { StoragePath } from "@ericsanchezok/synergy-harness/storage/path";
|
|
7
|
+
import { Aggregator } from "./aggregator.js";
|
|
8
|
+
import { StatsStorage } from "./storage.js";
|
|
9
|
+
import { Rollup } from "./rollup.js";
|
|
10
|
+
import { CanonicalStats } from "./canonical.js";
|
|
11
|
+
import { Usage } from "@ericsanchezok/synergy-harness/usage";
|
|
12
|
+
export var Engine;
|
|
13
|
+
(function (Engine) {
|
|
14
|
+
// -----------------------------------------------------------------------
|
|
15
|
+
// Public API
|
|
16
|
+
// -----------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Run incremental stats update: scan sessions changed since watermark,
|
|
19
|
+
* update digests + daily buckets, recompute snapshot.
|
|
20
|
+
*/
|
|
21
|
+
async function update(onProgress) {
|
|
22
|
+
return requestRefresh(false, onProgress);
|
|
23
|
+
}
|
|
24
|
+
Engine.update = update;
|
|
25
|
+
const jobs = Storage.state(() => new Map());
|
|
26
|
+
function requestRefresh(full, onProgress) {
|
|
27
|
+
const running = jobs();
|
|
28
|
+
const existing = running.get(full) ?? (!full ? running.get(true) : undefined);
|
|
29
|
+
if (existing) {
|
|
30
|
+
if (onProgress)
|
|
31
|
+
existing.listeners.add(onProgress);
|
|
32
|
+
return existing.promise.finally(() => {
|
|
33
|
+
if (onProgress)
|
|
34
|
+
existing.listeners.delete(onProgress);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const listeners = new Set(onProgress ? [onProgress] : []);
|
|
38
|
+
const promise = refresh(full, (event) => {
|
|
39
|
+
for (const listener of listeners)
|
|
40
|
+
void Promise.resolve()
|
|
41
|
+
.then(() => listener(event))
|
|
42
|
+
.catch(() => listeners.delete(listener));
|
|
43
|
+
}).finally(() => running.delete(full));
|
|
44
|
+
running.set(full, { promise, listeners });
|
|
45
|
+
return promise;
|
|
46
|
+
}
|
|
47
|
+
async function operationDigests() {
|
|
48
|
+
const result = [];
|
|
49
|
+
const retained = new Set();
|
|
50
|
+
for (const scopeID of await Storage.scan(["operations"])) {
|
|
51
|
+
for (const operationID of await Storage.scan(["operations", scopeID])) {
|
|
52
|
+
const owner = { kind: "operation", scopeID, operationID };
|
|
53
|
+
const revision = await readRolloutRevision(owner);
|
|
54
|
+
if (!revision)
|
|
55
|
+
continue;
|
|
56
|
+
const key = StoragePath.statsOperation(scopeID, operationID);
|
|
57
|
+
retained.add(key.join("/"));
|
|
58
|
+
const cached = await Storage.read(key).catch((error) => {
|
|
59
|
+
if (error instanceof Storage.NotFoundError)
|
|
60
|
+
return undefined;
|
|
61
|
+
throw error;
|
|
62
|
+
});
|
|
63
|
+
const parsed = OperationDigest.safeParse(cached);
|
|
64
|
+
if (parsed.success && parsed.data.rolloutRevision === revision) {
|
|
65
|
+
result.push(parsed.data);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const digest = Aggregator.operation(await RolloutSnapshot.read(owner, { revision }));
|
|
69
|
+
await Storage.write(key, digest);
|
|
70
|
+
result.push(digest);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const scopeID of await Storage.scan(StoragePath.statsOperations()))
|
|
74
|
+
for (const id of await Storage.scan([...StoragePath.statsOperations(), scopeID])) {
|
|
75
|
+
const key = StoragePath.statsOperation(scopeID, id);
|
|
76
|
+
if (!retained.has(key.join("/")))
|
|
77
|
+
await Storage.remove(key);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
async function refresh(full, onProgress) {
|
|
82
|
+
using lock = await Lock.write("stats-update");
|
|
83
|
+
const watermark = full ? undefined : await StatsStorage.getWatermark();
|
|
84
|
+
onProgress?.({ phase: "scan", current: 0, total: 1, message: "Scanning sessions..." });
|
|
85
|
+
const allSessions = await getAllSessions();
|
|
86
|
+
let newOrUpdated;
|
|
87
|
+
let deletedIDs;
|
|
88
|
+
if (watermark) {
|
|
89
|
+
const knownSet = new Set(watermark.sessionIDs);
|
|
90
|
+
const currentMap = new Map(allSessions.map((s) => [s.id, s]));
|
|
91
|
+
// Sessions that are new or updated since watermark
|
|
92
|
+
newOrUpdated = [];
|
|
93
|
+
for (let offset = 0; offset < allSessions.length; offset += 20) {
|
|
94
|
+
const batch = allSessions.slice(offset, offset + 20);
|
|
95
|
+
const changed = await Promise.all(batch.map(async (session) => {
|
|
96
|
+
if (!knownSet.has(session.id) || session.time.updated > watermark.lastUpdated)
|
|
97
|
+
return true;
|
|
98
|
+
const [digest, revision] = await Promise.all([
|
|
99
|
+
StatsStorage.getDigest(session.id),
|
|
100
|
+
readRolloutRevision({ kind: "session", scopeID: session.scope.id, sessionID: session.id }),
|
|
101
|
+
]);
|
|
102
|
+
return digest?.rolloutRevision !== revision;
|
|
103
|
+
}));
|
|
104
|
+
newOrUpdated.push(...batch.filter((_, index) => changed[index]));
|
|
105
|
+
}
|
|
106
|
+
// Sessions that were known but no longer exist
|
|
107
|
+
deletedIDs = watermark.sessionIDs.filter((id) => !currentMap.has(id));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
newOrUpdated = allSessions;
|
|
111
|
+
deletedIDs = [];
|
|
112
|
+
}
|
|
113
|
+
// Digest new/updated sessions with progress
|
|
114
|
+
const freshDigests = await Aggregator.digestAll(newOrUpdated, (current, total) => {
|
|
115
|
+
onProgress?.({ phase: "digest", current, total, message: `Digesting sessions ${current}/${total}...` });
|
|
116
|
+
});
|
|
117
|
+
// Write new/updated digests
|
|
118
|
+
for (const d of freshDigests) {
|
|
119
|
+
await Usage.preserveOwner({ kind: "session", scopeID: d.scopeID, sessionID: d.sessionID });
|
|
120
|
+
await StatsStorage.setDigest(d);
|
|
121
|
+
}
|
|
122
|
+
// Remove digests for deleted sessions
|
|
123
|
+
for (const id of deletedIDs) {
|
|
124
|
+
await StatsStorage.removeDigest(id);
|
|
125
|
+
}
|
|
126
|
+
// Load all digests for full snapshot
|
|
127
|
+
onProgress?.({ phase: "snapshot", current: 0, total: 1, message: "Computing snapshot..." });
|
|
128
|
+
const allDigests = await StatsStorage.getAllDigests();
|
|
129
|
+
// Compute new watermark
|
|
130
|
+
const maxUpdated = allSessions.length > 0 ? Math.max(...allSessions.map((s) => s.time.updated)) : 0;
|
|
131
|
+
const newWatermark = {
|
|
132
|
+
lastUpdated: maxUpdated,
|
|
133
|
+
sessionIDs: allSessions.map((s) => s.id),
|
|
134
|
+
lastFullScanAt: Date.now(),
|
|
135
|
+
};
|
|
136
|
+
// Compute and store snapshot
|
|
137
|
+
const snapshot = await CanonicalStats.apply(Rollup.snapshot(allDigests, maxUpdated, await operationDigests()));
|
|
138
|
+
onProgress?.({
|
|
139
|
+
phase: "bucket",
|
|
140
|
+
current: 0,
|
|
141
|
+
total: snapshot.timeSeries.days.length,
|
|
142
|
+
message: "Updating daily buckets...",
|
|
143
|
+
});
|
|
144
|
+
const days = new Set(snapshot.timeSeries.days.map((day) => day.day));
|
|
145
|
+
for (const day of snapshot.timeSeries.days)
|
|
146
|
+
await StatsStorage.setDailyBucket(day.day, day);
|
|
147
|
+
for (const day of await StatsStorage.listDailyKeys()) {
|
|
148
|
+
if (!days.has(day))
|
|
149
|
+
await Storage.remove(StoragePath.statsDaily(day));
|
|
150
|
+
}
|
|
151
|
+
await StatsStorage.setSnapshot(snapshot);
|
|
152
|
+
await StatsStorage.setWatermark(newWatermark);
|
|
153
|
+
onProgress?.({ phase: "snapshot", current: 1, total: 1, message: "Done" });
|
|
154
|
+
return snapshot;
|
|
155
|
+
}
|
|
156
|
+
async function get() {
|
|
157
|
+
return (await StatsStorage.getSnapshot()) ?? null;
|
|
158
|
+
}
|
|
159
|
+
Engine.get = get;
|
|
160
|
+
/**
|
|
161
|
+
* Force full recompute from scratch (clears all cached stats).
|
|
162
|
+
*/
|
|
163
|
+
async function recompute(onProgress) {
|
|
164
|
+
return requestRefresh(true, onProgress);
|
|
165
|
+
}
|
|
166
|
+
Engine.recompute = recompute;
|
|
167
|
+
// -----------------------------------------------------------------------
|
|
168
|
+
// Internal helpers
|
|
169
|
+
// -----------------------------------------------------------------------
|
|
170
|
+
async function getAllSessions() {
|
|
171
|
+
const sessions = [];
|
|
172
|
+
for await (const record of Storage.records({ kind: "session" }))
|
|
173
|
+
sessions.push(record.value);
|
|
174
|
+
return sessions;
|
|
175
|
+
}
|
|
176
|
+
})(Engine || (Engine = {}));
|