/** * AgentGuard(TM) Spend: local Advisor anomaly review skeleton. * * Reads local decision logs only. No network calls are made. * * Patent notice: Protected by U.S. patent-pending technology * (App. Nos. 63/983,615; 63/983,621; 63/983,843; 63/984,626; * 64/071,781; 64/071,789). */ import * as fs from 'fs'; import * as path from 'path'; import { agentguardHome } from './output'; export interface AdvisorSpendPoint { ts: string; scope: string; agentId: string; cents: number; } export interface AdvisorAnomaly { scope: string; agentId: string; last24hCents: number; baselineCents: number; sigmaCents: number; suggestion: string; } export function readDecisionSpend(scope = 'default', home = agentguardHome()): AdvisorSpendPoint[] { const file = path.join(home, scope, 'decisions.ndjson'); if (!fs.existsSync(file)) return []; const points: AdvisorSpendPoint[] = []; for (const line of fs.readFileSync(file, 'utf8').split('\n')) { if (!line.trim()) continue; try { const parsed = JSON.parse(line); const decision = parsed.decision ?? parsed; const cents = Number(decision.actualCents ?? decision.projectedCents ?? 0); const ts = String(decision.timestamp ?? parsed.timestamp ?? new Date(0).toISOString()); const scopeObj = decision.scope ?? {}; points.push({ ts, scope: String(scopeObj.tenantId ?? decision.triggeredScopeKey ?? scope), agentId: String(scopeObj.agentId ?? decision.agentId ?? 'unknown'), cents: Number.isFinite(cents) ? cents : 0, }); } catch { continue; } } return points; } export function reviewAnomalies(points: AdvisorSpendPoint[], now = new Date()): AdvisorAnomaly[] { const cutoff = now.getTime() - 24 * 60 * 60 * 1000; const byAgent = new Map(); for (const point of points) { const key = `${point.scope}\t${point.agentId}`; byAgent.set(key, [...(byAgent.get(key) ?? []), point]); } const anomalies: AdvisorAnomaly[] = []; for (const [key, rows] of byAgent) { const [scope, agentId] = key.split('\t'); const daily = bucketDaily(rows); const historical = daily.filter((row) => row.day < dayKey(new Date(cutoff))).map((row) => row.cents); const recent = rows.filter((row) => Date.parse(row.ts) >= cutoff).reduce((sum, row) => sum + row.cents, 0); if (historical.length < 3 || recent <= 0) continue; const mean = historical.reduce((sum, value) => sum + value, 0) / historical.length; const variance = historical.reduce((sum, value) => sum + Math.pow(value - mean, 2), 0) / historical.length; const sigma = Math.sqrt(variance); if (recent > mean + 3 * sigma) { anomalies.push({ scope: scope ?? 'unknown', agentId: agentId ?? 'unknown', last24hCents: recent, baselineCents: Math.round(mean), sigmaCents: Math.round(sigma), suggestion: 'Lower the per_day cap or add a per_minute block cap for this agent.', }); } } return anomalies.sort((a, b) => b.last24hCents - a.last24hCents); } function bucketDaily(points: AdvisorSpendPoint[]): Array<{ day: string; cents: number }> { const buckets = new Map(); for (const point of points) { const day = dayKey(new Date(point.ts)); buckets.set(day, (buckets.get(day) ?? 0) + point.cents); } return [...buckets.entries()].map(([day, cents]) => ({ day, cents })).sort((a, b) => a.day.localeCompare(b.day)); } function dayKey(date: Date): string { return date.toISOString().slice(0, 10); }