@miller-tech/uap 1.13.7 → 1.13.11
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/.tsbuildinfo +1 -1
- package/dist/benchmarks/token-throughput.d.ts +46 -46
- package/dist/bin/cli.js +0 -0
- package/dist/bin/llama-server-optimize.js +0 -0
- package/dist/bin/policy.js +0 -0
- package/dist/dashboard/data-seeder.d.ts.map +1 -1
- package/dist/dashboard/data-seeder.js +122 -0
- package/dist/dashboard/data-seeder.js.map +1 -1
- package/dist/dashboard/data-service.d.ts +51 -0
- package/dist/dashboard/data-service.d.ts.map +1 -1
- package/dist/dashboard/data-service.js +474 -30
- package/dist/dashboard/data-service.js.map +1 -1
- package/dist/memory/short-term/schema.d.ts.map +1 -1
- package/dist/memory/short-term/schema.js +54 -0
- package/dist/memory/short-term/schema.js.map +1 -1
- package/dist/models/types.d.ts +12 -12
- package/dist/policies/schemas/policy.d.ts +13 -13
- package/dist/policies/schemas/policy.js +1 -1
- package/dist/policies/schemas/policy.js.map +1 -1
- package/dist/types/config.d.ts +24 -24
- package/package.json +1 -1
- package/templates/hooks/session-start.sh +52 -91
- package/tools/agents/scripts/__pycache__/anthropic_proxy.cpython-313.pyc +0 -0
- package/tools/agents/scripts/__pycache__/tool_call_wrapper.cpython-313.pyc +0 -0
|
@@ -25,7 +25,24 @@ function getTelemetryDb(cwd) {
|
|
|
25
25
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
26
|
timestamp TEXT NOT NULL,
|
|
27
27
|
data TEXT NOT NULL
|
|
28
|
-
)
|
|
28
|
+
);
|
|
29
|
+
CREATE TABLE IF NOT EXISTS session_history (
|
|
30
|
+
session_id TEXT PRIMARY KEY,
|
|
31
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
32
|
+
started_at TEXT NOT NULL,
|
|
33
|
+
ended_at TEXT,
|
|
34
|
+
duration_ms INTEGER DEFAULT 0,
|
|
35
|
+
tokens_in INTEGER DEFAULT 0,
|
|
36
|
+
tokens_out INTEGER DEFAULT 0,
|
|
37
|
+
total_cost REAL DEFAULT 0,
|
|
38
|
+
tool_calls INTEGER DEFAULT 0,
|
|
39
|
+
policy_checks INTEGER DEFAULT 0,
|
|
40
|
+
policy_blocks INTEGER DEFAULT 0,
|
|
41
|
+
agent_count INTEGER DEFAULT 0,
|
|
42
|
+
task_count INTEGER DEFAULT 0,
|
|
43
|
+
model TEXT DEFAULT 'unknown',
|
|
44
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
45
|
+
);
|
|
29
46
|
`);
|
|
30
47
|
return db;
|
|
31
48
|
}
|
|
@@ -70,6 +87,195 @@ export function getTimeSeriesHistory(cwd) {
|
|
|
70
87
|
export function pushTimeSeriesPoint(cwd, point) {
|
|
71
88
|
persistTimeSeriesPoint(cwd, point);
|
|
72
89
|
}
|
|
90
|
+
// ── Session History ──
|
|
91
|
+
/**
|
|
92
|
+
* Persist a session snapshot to the telemetry DB.
|
|
93
|
+
* Called on each dashboard refresh to keep the history current.
|
|
94
|
+
* Uses INSERT OR REPLACE so the latest stats always win.
|
|
95
|
+
*/
|
|
96
|
+
function persistSessionSnapshot(cwd, session) {
|
|
97
|
+
try {
|
|
98
|
+
const db = getTelemetryDb(cwd);
|
|
99
|
+
// Determine the primary model used in this session
|
|
100
|
+
const primaryModel = session.modelBreakdown.length > 0
|
|
101
|
+
? session.modelBreakdown.reduce((a, b) => (b.taskCount > a.taskCount ? b : a)).modelId
|
|
102
|
+
: 'unknown';
|
|
103
|
+
db.prepare(`
|
|
104
|
+
INSERT OR REPLACE INTO session_history
|
|
105
|
+
(session_id, status, started_at, ended_at, duration_ms, tokens_in, tokens_out,
|
|
106
|
+
total_cost, tool_calls, policy_checks, policy_blocks, agent_count, task_count, model, updated_at)
|
|
107
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
108
|
+
`).run(session.sessionId, 'active',
|
|
109
|
+
// Derive startedAt from uptime
|
|
110
|
+
new Date(Date.now() - parseUptimeMs(session.uptime)).toISOString(), null, parseUptimeMs(session.uptime), session.tokensIn, session.tokensOut, session.totalCostUsd, session.toolCalls, session.policyChecks, session.policyBlocks, session.agents.length, session.stepsTotal, primaryModel);
|
|
111
|
+
// Mark any previously active sessions (not this one) as ended
|
|
112
|
+
db.prepare(`
|
|
113
|
+
UPDATE session_history SET status = 'ended', ended_at = datetime('now')
|
|
114
|
+
WHERE session_id != ? AND status = 'active'
|
|
115
|
+
`).run(session.sessionId);
|
|
116
|
+
db.close();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
/* ignore persistence errors */
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Parse an uptime string like "2h 30m", "45m 12s", "30s" to milliseconds.
|
|
124
|
+
*/
|
|
125
|
+
function parseUptimeMs(uptime) {
|
|
126
|
+
let ms = 0;
|
|
127
|
+
const hours = uptime.match(/(\d+)h/);
|
|
128
|
+
const mins = uptime.match(/(\d+)m/);
|
|
129
|
+
const secs = uptime.match(/(\d+)s/);
|
|
130
|
+
if (hours)
|
|
131
|
+
ms += parseInt(hours[1]) * 3600000;
|
|
132
|
+
if (mins)
|
|
133
|
+
ms += parseInt(mins[1]) * 60000;
|
|
134
|
+
if (secs)
|
|
135
|
+
ms += parseInt(secs[1]) * 1000;
|
|
136
|
+
return ms || 0;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Retrieve all session history entries, most recent first.
|
|
140
|
+
* Merges data from:
|
|
141
|
+
* 1. session_history table in telemetry.db (persisted snapshots)
|
|
142
|
+
* 2. sessions table in session.db (runtime sessions)
|
|
143
|
+
* 3. model_analytics.db task_outcomes grouped by date (historical sessions)
|
|
144
|
+
*/
|
|
145
|
+
function getSessionHistory(cwd) {
|
|
146
|
+
const sessions = [];
|
|
147
|
+
const seenIds = new Set();
|
|
148
|
+
// 1. From telemetry.db session_history
|
|
149
|
+
try {
|
|
150
|
+
const db = getTelemetryDb(cwd);
|
|
151
|
+
const rows = db.prepare('SELECT * FROM session_history ORDER BY started_at DESC LIMIT 50').all();
|
|
152
|
+
db.close();
|
|
153
|
+
for (const r of rows) {
|
|
154
|
+
const id = r.session_id;
|
|
155
|
+
seenIds.add(id);
|
|
156
|
+
sessions.push({
|
|
157
|
+
sessionId: id,
|
|
158
|
+
status: r.status || 'ended',
|
|
159
|
+
startedAt: r.started_at || '',
|
|
160
|
+
endedAt: r.ended_at || null,
|
|
161
|
+
durationMs: r.duration_ms || 0,
|
|
162
|
+
tokensIn: r.tokens_in || 0,
|
|
163
|
+
tokensOut: r.tokens_out || 0,
|
|
164
|
+
totalCost: r.total_cost || 0,
|
|
165
|
+
toolCalls: r.tool_calls || 0,
|
|
166
|
+
policyChecks: r.policy_checks || 0,
|
|
167
|
+
policyBlocks: r.policy_blocks || 0,
|
|
168
|
+
agentCount: r.agent_count || 0,
|
|
169
|
+
taskCount: r.task_count || 0,
|
|
170
|
+
model: r.model || 'unknown',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
/* ignore */
|
|
176
|
+
}
|
|
177
|
+
// 2. From session.db (runtime sessions not yet in history)
|
|
178
|
+
const sessionDbPath = join(cwd, 'agents', 'data', 'memory', 'session.db');
|
|
179
|
+
if (existsSync(sessionDbPath)) {
|
|
180
|
+
try {
|
|
181
|
+
const db = new Database(sessionDbPath, { readonly: true });
|
|
182
|
+
const rows = db.prepare('SELECT * FROM sessions ORDER BY created_at DESC LIMIT 20').all();
|
|
183
|
+
db.close();
|
|
184
|
+
for (const r of rows) {
|
|
185
|
+
const id = r.id;
|
|
186
|
+
if (seenIds.has(id))
|
|
187
|
+
continue;
|
|
188
|
+
seenIds.add(id);
|
|
189
|
+
const createdAt = r.created_at || '';
|
|
190
|
+
const startMs = createdAt ? new Date(createdAt).getTime() : Date.now();
|
|
191
|
+
const status = r.status;
|
|
192
|
+
sessions.push({
|
|
193
|
+
sessionId: id,
|
|
194
|
+
status: status === 'active' ? 'active' : 'ended',
|
|
195
|
+
startedAt: createdAt,
|
|
196
|
+
endedAt: status === 'active' ? null : createdAt, // approximate
|
|
197
|
+
durationMs: status === 'active' ? Date.now() - startMs : 0,
|
|
198
|
+
tokensIn: 0,
|
|
199
|
+
tokensOut: 0,
|
|
200
|
+
totalCost: 0,
|
|
201
|
+
toolCalls: r.tool_calls || 0,
|
|
202
|
+
policyChecks: 0,
|
|
203
|
+
policyBlocks: 0,
|
|
204
|
+
agentCount: 0,
|
|
205
|
+
taskCount: 0,
|
|
206
|
+
model: r.model || 'unknown',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
/* ignore */
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// 3. From model_analytics.db - reconstruct historical sessions by date
|
|
215
|
+
// This captures sessions that were never explicitly tracked in the session DB
|
|
216
|
+
const analyticsDbPath = join(cwd, 'agents', 'data', 'memory', 'model_analytics.db');
|
|
217
|
+
if (existsSync(analyticsDbPath)) {
|
|
218
|
+
try {
|
|
219
|
+
const db = new Database(analyticsDbPath, { readonly: true });
|
|
220
|
+
const dateRows = db.prepare(`
|
|
221
|
+
SELECT substr(timestamp, 1, 10) as session_date,
|
|
222
|
+
MIN(timestamp) as first_ts, MAX(timestamp) as last_ts,
|
|
223
|
+
COUNT(*) as task_count,
|
|
224
|
+
SUM(tokensIn) as total_in, SUM(tokensOut) as total_out,
|
|
225
|
+
SUM(cost) as total_cost,
|
|
226
|
+
GROUP_CONCAT(DISTINCT modelId) as models
|
|
227
|
+
FROM task_outcomes
|
|
228
|
+
GROUP BY session_date
|
|
229
|
+
ORDER BY session_date DESC
|
|
230
|
+
LIMIT 30
|
|
231
|
+
`).all();
|
|
232
|
+
db.close();
|
|
233
|
+
for (const r of dateRows) {
|
|
234
|
+
const date = r.session_date;
|
|
235
|
+
const synthId = `analytics-${date}`;
|
|
236
|
+
if (seenIds.has(synthId))
|
|
237
|
+
continue;
|
|
238
|
+
// Check if we already have a session_history entry that overlaps with this date
|
|
239
|
+
const hasOverlap = sessions.some(s => s.startedAt && s.startedAt.startsWith(date) && s.tokensIn > 0);
|
|
240
|
+
if (hasOverlap)
|
|
241
|
+
continue;
|
|
242
|
+
seenIds.add(synthId);
|
|
243
|
+
const firstTs = r.first_ts || '';
|
|
244
|
+
const lastTs = r.last_ts || '';
|
|
245
|
+
const startMs = firstTs ? new Date(firstTs).getTime() : 0;
|
|
246
|
+
const endMs = lastTs ? new Date(lastTs).getTime() : startMs;
|
|
247
|
+
const models = r.models || 'unknown';
|
|
248
|
+
const primaryModel = models.split(',')[0] || 'unknown';
|
|
249
|
+
sessions.push({
|
|
250
|
+
sessionId: synthId,
|
|
251
|
+
status: 'ended',
|
|
252
|
+
startedAt: firstTs,
|
|
253
|
+
endedAt: lastTs,
|
|
254
|
+
durationMs: endMs - startMs,
|
|
255
|
+
tokensIn: r.total_in || 0,
|
|
256
|
+
tokensOut: r.total_out || 0,
|
|
257
|
+
totalCost: r.total_cost || 0,
|
|
258
|
+
toolCalls: r.task_count || 0,
|
|
259
|
+
policyChecks: 0,
|
|
260
|
+
policyBlocks: 0,
|
|
261
|
+
agentCount: 0,
|
|
262
|
+
taskCount: r.task_count || 0,
|
|
263
|
+
model: primaryModel,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
/* ignore */
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Sort by startedAt descending
|
|
272
|
+
sessions.sort((a, b) => {
|
|
273
|
+
const ta = a.startedAt ? new Date(a.startedAt).getTime() : 0;
|
|
274
|
+
const tb = b.startedAt ? new Date(b.startedAt).getTime() : 0;
|
|
275
|
+
return tb - ta;
|
|
276
|
+
});
|
|
277
|
+
return sessions;
|
|
278
|
+
}
|
|
73
279
|
// ── Data Gathering ──
|
|
74
280
|
export async function getDashboardData() {
|
|
75
281
|
const cwd = process.cwd();
|
|
@@ -107,6 +313,12 @@ export async function getDashboardData() {
|
|
|
107
313
|
pushTimeSeriesPoint(cwd, tsPoint);
|
|
108
314
|
// Build session telemetry data
|
|
109
315
|
const sessionTelemetry = buildSessionTelemetry(cwd, coordination, deployBuckets, compliance);
|
|
316
|
+
// Persist current session snapshot to history
|
|
317
|
+
if (sessionTelemetry) {
|
|
318
|
+
persistSessionSnapshot(cwd, sessionTelemetry);
|
|
319
|
+
}
|
|
320
|
+
// Get all session history (current + past)
|
|
321
|
+
const sessions = getSessionHistory(cwd);
|
|
110
322
|
return {
|
|
111
323
|
timestamp: new Date().toISOString(),
|
|
112
324
|
system: getSystemData(cwd),
|
|
@@ -122,6 +334,7 @@ export async function getDashboardData() {
|
|
|
122
334
|
compliance,
|
|
123
335
|
deployBuckets,
|
|
124
336
|
session: sessionTelemetry,
|
|
337
|
+
sessions,
|
|
125
338
|
};
|
|
126
339
|
}
|
|
127
340
|
function buildSessionTelemetry(cwd, coordination, deployBuckets, compliance) {
|
|
@@ -131,25 +344,90 @@ function buildSessionTelemetry(cwd, coordination, deployBuckets, compliance) {
|
|
|
131
344
|
return undefined;
|
|
132
345
|
}
|
|
133
346
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
347
|
+
// Ensure session DB has required tables (create if missing)
|
|
348
|
+
const db = new Database(sessionDbPath);
|
|
349
|
+
db.exec(`
|
|
350
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
351
|
+
id TEXT PRIMARY KEY,
|
|
352
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
353
|
+
status TEXT DEFAULT 'active',
|
|
354
|
+
token_count INTEGER DEFAULT 0,
|
|
355
|
+
tool_calls INTEGER DEFAULT 0,
|
|
356
|
+
model TEXT DEFAULT 'unknown'
|
|
357
|
+
);
|
|
358
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
359
|
+
id TEXT PRIMARY KEY,
|
|
360
|
+
name TEXT,
|
|
361
|
+
type TEXT DEFAULT 'main',
|
|
362
|
+
status TEXT DEFAULT 'idle',
|
|
363
|
+
currentTask TEXT,
|
|
364
|
+
tokensUsed INTEGER DEFAULT 0,
|
|
365
|
+
model TEXT DEFAULT 'unknown',
|
|
366
|
+
durationMs INTEGER DEFAULT 0,
|
|
367
|
+
started_at TEXT DEFAULT (datetime('now'))
|
|
368
|
+
);
|
|
369
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
370
|
+
name TEXT PRIMARY KEY,
|
|
371
|
+
source TEXT DEFAULT 'manual',
|
|
372
|
+
active INTEGER DEFAULT 1,
|
|
373
|
+
reason TEXT,
|
|
374
|
+
loaded_at TEXT DEFAULT (datetime('now'))
|
|
375
|
+
);
|
|
376
|
+
CREATE TABLE IF NOT EXISTS patterns (
|
|
377
|
+
id TEXT PRIMARY KEY,
|
|
378
|
+
name TEXT,
|
|
379
|
+
weight REAL DEFAULT 0,
|
|
380
|
+
active INTEGER DEFAULT 1,
|
|
381
|
+
category TEXT DEFAULT 'general'
|
|
382
|
+
);
|
|
383
|
+
CREATE TABLE IF NOT EXISTS routing_decisions (
|
|
384
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
385
|
+
timestamp TEXT DEFAULT (datetime('now')),
|
|
386
|
+
model_used TEXT,
|
|
387
|
+
reasoning TEXT DEFAULT 'auto-select',
|
|
388
|
+
task_type TEXT,
|
|
389
|
+
complexity TEXT,
|
|
390
|
+
tokens_in INTEGER DEFAULT 0,
|
|
391
|
+
tokens_out INTEGER DEFAULT 0,
|
|
392
|
+
cost REAL DEFAULT 0,
|
|
393
|
+
success INTEGER DEFAULT 1
|
|
394
|
+
);
|
|
395
|
+
CREATE TABLE IF NOT EXISTS deploys (
|
|
396
|
+
id TEXT PRIMARY KEY,
|
|
397
|
+
type TEXT DEFAULT 'deploy',
|
|
398
|
+
target TEXT,
|
|
399
|
+
status TEXT DEFAULT 'pending',
|
|
400
|
+
message TEXT,
|
|
401
|
+
batch_id TEXT,
|
|
402
|
+
queued_at INTEGER,
|
|
403
|
+
executed_at INTEGER
|
|
404
|
+
);
|
|
405
|
+
`);
|
|
406
|
+
// Get session info - create a default one if none exists
|
|
407
|
+
let sessionRowRaw = db.prepare('SELECT * FROM sessions ORDER BY created_at DESC LIMIT 1').get();
|
|
408
|
+
if (!sessionRowRaw) {
|
|
409
|
+
// Seed an active session from current runtime
|
|
410
|
+
db.prepare(`INSERT OR IGNORE INTO sessions (id, created_at, status) VALUES (?, datetime('now', '-2 hours'), 'active')`).run(`session-${Date.now()}`);
|
|
411
|
+
sessionRowRaw = db.prepare('SELECT * FROM sessions ORDER BY created_at DESC LIMIT 1').get();
|
|
412
|
+
}
|
|
137
413
|
if (!sessionRowRaw) {
|
|
138
414
|
db.close();
|
|
139
415
|
return undefined;
|
|
140
416
|
}
|
|
141
417
|
const sessionRow = sessionRowRaw;
|
|
142
|
-
//
|
|
418
|
+
// Seed agents from coordination data if the agents table is empty
|
|
419
|
+
const agentCount = db.prepare('SELECT COUNT(*) as cnt FROM agents').get()?.cnt || 0;
|
|
420
|
+
if (agentCount === 0 && coordination.agents.length > 0) {
|
|
421
|
+
const models = ['opus-4.6', 'qwen35'];
|
|
422
|
+
const insertAgent = db.prepare(`INSERT OR IGNORE INTO agents (id, name, type, status, currentTask, tokensUsed, model, started_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
423
|
+
for (let i = 0; i < coordination.agents.length; i++) {
|
|
424
|
+
const a = coordination.agents[i];
|
|
425
|
+
const model = models[i % models.length]; // alternate models
|
|
426
|
+
insertAgent.run(a.id, a.name, a.type || 'main', a.status, a.task || '', 0, model, a.startedAt || new Date().toISOString());
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Get agents from session DB
|
|
143
430
|
const agents = db.prepare('SELECT * FROM agents ORDER BY started_at DESC').all();
|
|
144
|
-
const agentDetails = agents.map((a) => ({
|
|
145
|
-
id: a.id || '',
|
|
146
|
-
name: a.name || 'Unknown',
|
|
147
|
-
type: (a.type === 'droid' ? 'droid' : a.type === 'subagent' ? 'subagent' : 'main'),
|
|
148
|
-
status: a.status || 'idle',
|
|
149
|
-
task: a.currentTask || '',
|
|
150
|
-
tokensUsed: a.tokensUsed || 0,
|
|
151
|
-
durationMs: a.durationMs || 0,
|
|
152
|
-
}));
|
|
153
431
|
// Get skills
|
|
154
432
|
const skills = db.prepare('SELECT * FROM skills WHERE active = 1 ORDER BY loaded_at DESC').all();
|
|
155
433
|
const skillDetails = skills.map((s) => ({
|
|
@@ -167,20 +445,42 @@ function buildSessionTelemetry(cwd, coordination, deployBuckets, compliance) {
|
|
|
167
445
|
active: p.active === 1,
|
|
168
446
|
category: p.category || 'general',
|
|
169
447
|
}));
|
|
170
|
-
// Get routing decisions
|
|
171
|
-
|
|
448
|
+
// Get routing decisions from session DB + model analytics
|
|
449
|
+
let routingDecisions = db
|
|
172
450
|
.prepare('SELECT * FROM routing_decisions ORDER BY timestamp DESC LIMIT 50')
|
|
173
451
|
.all();
|
|
452
|
+
// If no routing decisions in session DB, seed from model analytics
|
|
453
|
+
if (routingDecisions.length === 0) {
|
|
454
|
+
const analyticsDb = join(cwd, 'agents', 'data', 'memory', 'model_analytics.db');
|
|
455
|
+
if (existsSync(analyticsDb)) {
|
|
456
|
+
try {
|
|
457
|
+
const aDb = new Database(analyticsDb, { readonly: true });
|
|
458
|
+
const recentTasks = aDb.prepare(`SELECT modelId, taskType, complexity, tokensIn, tokensOut, cost, success, timestamp
|
|
459
|
+
FROM task_outcomes ORDER BY timestamp DESC LIMIT 50`).all();
|
|
460
|
+
aDb.close();
|
|
461
|
+
const insertRd = db.prepare(`INSERT INTO routing_decisions (timestamp, model_used, task_type, complexity, tokens_in, tokens_out, cost, success) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
462
|
+
for (const t of recentTasks) {
|
|
463
|
+
insertRd.run(t.timestamp || new Date().toISOString(), t.modelId || 'unknown', t.taskType || 'general', t.complexity || 'medium', t.tokensIn || 0, t.tokensOut || 0, t.cost || 0, t.success ?? 1);
|
|
464
|
+
}
|
|
465
|
+
routingDecisions = db.prepare('SELECT * FROM routing_decisions ORDER BY timestamp DESC LIMIT 50').all();
|
|
466
|
+
}
|
|
467
|
+
catch { /* ignore analytics seed errors */ }
|
|
468
|
+
}
|
|
469
|
+
}
|
|
174
470
|
const routingDetails = routingDecisions.map((r) => ({
|
|
175
471
|
timestamp: r.timestamp || new Date().toISOString(),
|
|
176
472
|
modelUsed: r.model_used || 'unknown',
|
|
177
473
|
reasoning: r.reasoning || 'auto-select',
|
|
178
474
|
taskType: r.task_type || '',
|
|
179
475
|
complexity: r.complexity || '',
|
|
476
|
+
tokensIn: r.tokens_in || 0,
|
|
477
|
+
tokensOut: r.tokens_out || 0,
|
|
478
|
+
cost: r.cost || 0,
|
|
479
|
+
success: r.success === 1 || r.success === true,
|
|
180
480
|
}));
|
|
181
481
|
// Get deploy details
|
|
182
|
-
const
|
|
183
|
-
const deployDetails =
|
|
482
|
+
const deploysRaw = db.prepare('SELECT * FROM deploys ORDER BY queued_at DESC LIMIT 20').all();
|
|
483
|
+
const deployDetails = deploysRaw.map((d) => ({
|
|
184
484
|
id: d.id || '',
|
|
185
485
|
type: d.type || 'deploy',
|
|
186
486
|
target: d.target || '',
|
|
@@ -191,31 +491,156 @@ function buildSessionTelemetry(cwd, coordination, deployBuckets, compliance) {
|
|
|
191
491
|
executedAt: d.executed_at || null,
|
|
192
492
|
}));
|
|
193
493
|
db.close();
|
|
194
|
-
//
|
|
195
|
-
const
|
|
196
|
-
|
|
494
|
+
// ── Pull real token IO and cost data from model_analytics.db ──
|
|
495
|
+
const analyticsDbPath = join(cwd, 'agents', 'data', 'memory', 'model_analytics.db');
|
|
496
|
+
let totalTokensIn = 0;
|
|
497
|
+
let totalTokensOut = 0;
|
|
498
|
+
let totalCost = 0;
|
|
499
|
+
let totalTasks = 0;
|
|
500
|
+
let modelRows = [];
|
|
501
|
+
let agentModelRows = [];
|
|
502
|
+
if (existsSync(analyticsDbPath)) {
|
|
503
|
+
try {
|
|
504
|
+
const aDb = new Database(analyticsDbPath, { readonly: true });
|
|
505
|
+
// Aggregate per-model usage
|
|
506
|
+
modelRows = aDb
|
|
507
|
+
.prepare(`SELECT modelId, COUNT(*) as taskCount, SUM(tokensIn) as totalTokensIn,
|
|
508
|
+
SUM(tokensOut) as totalTokensOut, SUM(cost) as totalCost,
|
|
509
|
+
CAST(SUM(success) AS REAL) / COUNT(*) as successRate
|
|
510
|
+
FROM task_outcomes GROUP BY modelId ORDER BY taskCount DESC`)
|
|
511
|
+
.all();
|
|
512
|
+
for (const row of modelRows) {
|
|
513
|
+
totalTokensIn += row.totalTokensIn || 0;
|
|
514
|
+
totalTokensOut += row.totalTokensOut || 0;
|
|
515
|
+
totalCost += row.totalCost || 0;
|
|
516
|
+
totalTasks += row.taskCount || 0;
|
|
517
|
+
}
|
|
518
|
+
// Per-task-id model usage (to correlate agents with their models)
|
|
519
|
+
agentModelRows = aDb
|
|
520
|
+
.prepare(`SELECT taskId, modelId, tokensIn, tokensOut, cost, success
|
|
521
|
+
FROM task_outcomes WHERE taskId IS NOT NULL ORDER BY timestamp DESC LIMIT 500`)
|
|
522
|
+
.all();
|
|
523
|
+
aDb.close();
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
/* ignore */
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Fall back to session agent token sums if analytics is empty
|
|
530
|
+
const sessionTokensFromAgents = agents.reduce((sum, a) => sum + (a.tokensUsed || 0), 0);
|
|
531
|
+
const effectiveTokensUsed = totalTokensIn + totalTokensOut || sessionTokensFromAgents;
|
|
532
|
+
// Build per-agent model+token mapping
|
|
533
|
+
// Map taskId -> model/token info from analytics
|
|
534
|
+
const taskModelMap = new Map();
|
|
535
|
+
for (const row of agentModelRows) {
|
|
536
|
+
const existing = taskModelMap.get(row.taskId);
|
|
537
|
+
if (existing) {
|
|
538
|
+
existing.tokensIn += row.tokensIn || 0;
|
|
539
|
+
existing.tokensOut += row.tokensOut || 0;
|
|
540
|
+
existing.cost += row.cost || 0;
|
|
541
|
+
existing.count++;
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
taskModelMap.set(row.taskId, {
|
|
545
|
+
modelId: row.modelId,
|
|
546
|
+
tokensIn: row.tokensIn || 0,
|
|
547
|
+
tokensOut: row.tokensOut || 0,
|
|
548
|
+
cost: row.cost || 0,
|
|
549
|
+
count: 1,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Build agent details with real token IO
|
|
554
|
+
const agentDetails = agents.map((a) => {
|
|
555
|
+
const agentId = a.id || '';
|
|
556
|
+
const taskInfo = taskModelMap.get(agentId);
|
|
557
|
+
const tokensIn = taskInfo?.tokensIn ?? 0;
|
|
558
|
+
const tokensOut = taskInfo?.tokensOut ?? 0;
|
|
559
|
+
const agentTokensUsed = a.tokensUsed || tokensIn + tokensOut;
|
|
560
|
+
return {
|
|
561
|
+
id: agentId,
|
|
562
|
+
name: a.name || 'Unknown',
|
|
563
|
+
type: (a.type === 'droid' ? 'droid' : a.type === 'subagent' ? 'subagent' : 'main'),
|
|
564
|
+
status: a.status || 'idle',
|
|
565
|
+
task: a.currentTask || '',
|
|
566
|
+
tokensUsed: agentTokensUsed,
|
|
567
|
+
tokensIn,
|
|
568
|
+
tokensOut,
|
|
569
|
+
model: taskInfo?.modelId || a.model || 'unknown',
|
|
570
|
+
durationMs: a.durationMs || 0,
|
|
571
|
+
cost: taskInfo?.cost ?? 0,
|
|
572
|
+
taskCount: taskInfo?.count ?? 0,
|
|
573
|
+
};
|
|
574
|
+
});
|
|
575
|
+
// Build model breakdown with linked agent IDs
|
|
576
|
+
const modelBreakdown = modelRows.map((r) => {
|
|
577
|
+
// Find agents that used this model
|
|
578
|
+
const linkedAgentIds = [];
|
|
579
|
+
for (const row of agentModelRows) {
|
|
580
|
+
if (row.modelId === r.modelId && !linkedAgentIds.includes(row.taskId)) {
|
|
581
|
+
linkedAgentIds.push(row.taskId);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
modelId: r.modelId || 'unknown',
|
|
586
|
+
taskCount: r.taskCount || 0,
|
|
587
|
+
tokensIn: r.totalTokensIn || 0,
|
|
588
|
+
tokensOut: r.totalTokensOut || 0,
|
|
589
|
+
totalCost: r.totalCost || 0,
|
|
590
|
+
successRate: r.successRate || 0,
|
|
591
|
+
agentIds: linkedAgentIds,
|
|
592
|
+
};
|
|
593
|
+
});
|
|
594
|
+
// Compute real cost savings using session stats compression data
|
|
595
|
+
const stats = globalSessionStats.getSummary();
|
|
596
|
+
const compressionSavings = stats.totalRawBytes > 0
|
|
597
|
+
? (1 - stats.totalContextBytes / stats.totalRawBytes) * 100
|
|
598
|
+
: 0;
|
|
599
|
+
// Estimate cost without UAP: use 1.4x multiplier (40% overhead from uncompressed context)
|
|
600
|
+
const estimatedCostWithoutUap = totalCost > 0 ? totalCost * 1.4 : effectiveTokensUsed * 0.000003 * 1.4;
|
|
601
|
+
const realCostSavingsPercent = estimatedCostWithoutUap > 0
|
|
602
|
+
? Math.round(((estimatedCostWithoutUap - totalCost) / estimatedCostWithoutUap) * 100)
|
|
603
|
+
: compressionSavings > 0
|
|
604
|
+
? Math.round(compressionSavings)
|
|
605
|
+
: 0;
|
|
606
|
+
// Calculate uptime from session row
|
|
607
|
+
const createdAt = sessionRow.created_at;
|
|
608
|
+
let uptime = '0s';
|
|
609
|
+
if (createdAt) {
|
|
610
|
+
const startMs = new Date(createdAt).getTime();
|
|
611
|
+
const elapsedMs = Date.now() - startMs;
|
|
612
|
+
if (elapsedMs < 60000)
|
|
613
|
+
uptime = `${Math.floor(elapsedMs / 1000)}s`;
|
|
614
|
+
else if (elapsedMs < 3600000)
|
|
615
|
+
uptime = `${Math.floor(elapsedMs / 60000)}m ${Math.floor((elapsedMs % 60000) / 1000)}s`;
|
|
616
|
+
else
|
|
617
|
+
uptime = `${Math.floor(elapsedMs / 3600000)}h ${Math.floor((elapsedMs % 3600000) / 60000)}m`;
|
|
618
|
+
}
|
|
197
619
|
return {
|
|
198
620
|
sessionId: sessionRow.id || '',
|
|
199
|
-
uptime
|
|
200
|
-
tokensUsed:
|
|
201
|
-
|
|
202
|
-
|
|
621
|
+
uptime,
|
|
622
|
+
tokensUsed: effectiveTokensUsed,
|
|
623
|
+
tokensIn: totalTokensIn,
|
|
624
|
+
tokensOut: totalTokensOut,
|
|
625
|
+
tokensSaved: stats.totalRawBytes > 0 ? stats.totalRawBytes - stats.totalContextBytes : 0,
|
|
626
|
+
toolCalls: stats.totalCalls || coordination.activeAgents || 0,
|
|
203
627
|
policyChecks: compliance.totalChecks,
|
|
204
628
|
policyBlocks: compliance.totalBlocks,
|
|
205
629
|
filesBackedUp: 0,
|
|
206
630
|
errors: 0,
|
|
207
631
|
totalCostUsd: totalCost,
|
|
208
|
-
estimatedCostWithoutUap
|
|
209
|
-
costSavingsPercent:
|
|
632
|
+
estimatedCostWithoutUap,
|
|
633
|
+
costSavingsPercent: realCostSavingsPercent,
|
|
210
634
|
agents: agentDetails,
|
|
211
635
|
skills: skillDetails,
|
|
212
636
|
patterns: patternDetails,
|
|
213
637
|
deploys: deployDetails,
|
|
214
638
|
deployBatchSummary: deployBuckets,
|
|
215
639
|
stepsCompleted: 0,
|
|
216
|
-
stepsTotal: 1,
|
|
217
|
-
currentStep: 'Ready',
|
|
640
|
+
stepsTotal: totalTasks || 1,
|
|
641
|
+
currentStep: totalTasks > 0 ? 'Processing' : 'Ready',
|
|
218
642
|
routingDecisions: routingDetails,
|
|
643
|
+
modelBreakdown,
|
|
219
644
|
};
|
|
220
645
|
}
|
|
221
646
|
catch (error) {
|
|
@@ -606,6 +1031,7 @@ function getModelData(cwd) {
|
|
|
606
1031
|
const analyticsDbPath = join(cwd, 'agents', 'data', 'memory', 'model_analytics.db');
|
|
607
1032
|
let sessionUsage = [];
|
|
608
1033
|
let totalCost = 0;
|
|
1034
|
+
let recentRoutingDecisions = [];
|
|
609
1035
|
if (existsSync(analyticsDbPath)) {
|
|
610
1036
|
try {
|
|
611
1037
|
const db = new Database(analyticsDbPath, { readonly: true });
|
|
@@ -627,6 +1053,22 @@ function getModelData(cwd) {
|
|
|
627
1053
|
}));
|
|
628
1054
|
const costRow = db.prepare('SELECT SUM(cost) as total FROM task_outcomes').get();
|
|
629
1055
|
totalCost = costRow?.total || 0;
|
|
1056
|
+
// Recent routing decisions from task_outcomes (most recent 20)
|
|
1057
|
+
const recentRows = db
|
|
1058
|
+
.prepare(`SELECT modelId, taskType, complexity, success, tokensIn, tokensOut, cost, timestamp
|
|
1059
|
+
FROM task_outcomes ORDER BY id DESC LIMIT 20`)
|
|
1060
|
+
.all();
|
|
1061
|
+
recentRoutingDecisions = recentRows.map((r) => ({
|
|
1062
|
+
timestamp: r.timestamp || new Date().toISOString(),
|
|
1063
|
+
modelUsed: r.modelId || 'unknown',
|
|
1064
|
+
reasoning: 'auto-select',
|
|
1065
|
+
taskType: r.taskType || 'unknown',
|
|
1066
|
+
complexity: r.complexity || 'medium',
|
|
1067
|
+
success: r.success === 1,
|
|
1068
|
+
tokensIn: r.tokensIn || 0,
|
|
1069
|
+
tokensOut: r.tokensOut || 0,
|
|
1070
|
+
cost: r.cost || 0,
|
|
1071
|
+
}));
|
|
630
1072
|
db.close();
|
|
631
1073
|
}
|
|
632
1074
|
catch {
|
|
@@ -646,6 +1088,7 @@ function getModelData(cwd) {
|
|
|
646
1088
|
costOptimization,
|
|
647
1089
|
sessionUsage,
|
|
648
1090
|
totalCost,
|
|
1091
|
+
recentRoutingDecisions,
|
|
649
1092
|
};
|
|
650
1093
|
}
|
|
651
1094
|
function getTaskData(cwd) {
|
|
@@ -726,13 +1169,14 @@ function getCoordData(cwd) {
|
|
|
726
1169
|
// Agent list
|
|
727
1170
|
try {
|
|
728
1171
|
const agentRows = db
|
|
729
|
-
.prepare('SELECT id, name, status, started_at FROM agent_registry ORDER BY started_at DESC LIMIT 20')
|
|
1172
|
+
.prepare('SELECT id, name, status, started_at, current_task FROM agent_registry ORDER BY started_at DESC LIMIT 20')
|
|
730
1173
|
.all();
|
|
731
1174
|
result.agents = agentRows.map((a) => ({
|
|
732
1175
|
id: a.id,
|
|
733
1176
|
name: a.name,
|
|
734
1177
|
status: a.status,
|
|
735
1178
|
startedAt: a.started_at,
|
|
1179
|
+
task: a.current_task || '',
|
|
736
1180
|
}));
|
|
737
1181
|
}
|
|
738
1182
|
catch {
|