| 724 | } |
| 725 | |
| 726 | getToolMetrics(minutes: number, fromMs?: number, toMs?: number): { |
| 727 | tools: string[]; |
| 728 | series: Array<{ minute: string; [tool: string]: number | string }>; |
| 729 | aggregated: Array<{ tool: string; totalCalls: number; avgMs: number; p95Ms: number; errorCount: number }>; |
| 730 | } { |
| 731 | const since = fromMs ?? (Date.now() - minutes * 60 * 1000); |
| 732 | const until = toMs ?? Date.now(); |
| 733 | |
| 734 | const rows = this.db.prepare( |
| 735 | `SELECT tool_name, |
| 736 | duration_ms, |
| 737 | success, |
| 738 | strftime('%Y-%m-%d %H:%M', called_at/1000, 'unixepoch', 'localtime') as minute_key |
| 739 | FROM tool_calls |
| 740 | WHERE called_at >= ? AND called_at <= ? |
| 741 | ORDER BY called_at`, |
| 742 | ).all(since, until) as Array<{ tool_name: string; duration_ms: number; success: number; minute_key: string }>; |
| 743 | |
| 744 | const toolSet = new Set<string>(); |
| 745 | const minuteMap = new Map<string, Map<string, { total: number; count: number }>>(); |
| 746 | const aggMap = new Map<string, { durations: number[]; errors: number }>(); |
| 747 | |
| 748 | for (const r of rows) { |
| 749 | toolSet.add(r.tool_name); |
| 750 | |
| 751 | if (!aggMap.has(r.tool_name)) aggMap.set(r.tool_name, { durations: [], errors: 0 }); |
| 752 | const agg = aggMap.get(r.tool_name)!; |
| 753 | agg.durations.push(r.duration_ms); |
| 754 | if (!r.success) agg.errors++; |
| 755 | |
| 756 | if (!minuteMap.has(r.minute_key)) minuteMap.set(r.minute_key, new Map()); |
| 757 | const toolMap = minuteMap.get(r.minute_key)!; |
| 758 | if (!toolMap.has(r.tool_name)) toolMap.set(r.tool_name, { total: 0, count: 0 }); |
| 759 | const entry = toolMap.get(r.tool_name)!; |
| 760 | entry.total += r.duration_ms; |
| 761 | entry.count++; |
| 762 | } |
| 763 | |
| 764 | const tools = Array.from(toolSet).sort(); |
| 765 | |
| 766 | const allMinutes: string[] = []; |
| 767 | if (minutes > 0) { |
| 768 | const startMinute = new Date(since); |
| 769 | startMinute.setSeconds(0, 0); |
| 770 | const now = new Date(); |
| 771 | for (let t = startMinute.getTime(); t <= now.getTime(); t += 60000) { |
| 772 | const d = new Date(t); |
| 773 | const pad = (n: number) => String(n).padStart(2, "0"); |
| 774 | allMinutes.push(`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`); |
| 775 | } |
| 776 | } |
| 777 | |
| 778 | const series = allMinutes.map((m) => { |
| 779 | const entry: { minute: string; [tool: string]: number | string } = { minute: m }; |
| 780 | const toolMap = minuteMap.get(m); |
| 781 | for (const t of tools) { |
| 782 | const data = toolMap?.get(t); |
| 783 | entry[t] = data ? Math.round(data.total / data.count) : 0; |