apteva 0.2.6 → 0.2.7
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/App.3kb50qa3.js +213 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/db.ts +9 -0
- package/src/routes/api.ts +13 -4
- package/src/server.ts +14 -3
- package/src/web/components/agents/AgentCard.tsx +7 -20
- package/src/web/components/dashboard/Dashboard.tsx +27 -15
- package/src/web/components/layout/Header.tsx +20 -7
- package/src/web/components/settings/SettingsPage.tsx +88 -1
- package/src/web/components/telemetry/TelemetryPage.tsx +117 -29
- package/src/web/context/TelemetryContext.tsx +98 -76
- package/dist/App.0mzj9cz9.js +0 -213
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
1
|
+
import React, { useState, useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import { Select } from "../common/Select";
|
|
3
3
|
import { useTelemetryContext, type TelemetryEvent } from "../../context";
|
|
4
4
|
|
|
@@ -20,11 +20,34 @@ interface UsageByAgent {
|
|
|
20
20
|
errors: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// Helper to extract stats from a single event
|
|
24
|
+
function extractEventStats(event: TelemetryEvent): {
|
|
25
|
+
llm_calls: number;
|
|
26
|
+
tool_calls: number;
|
|
27
|
+
errors: number;
|
|
28
|
+
input_tokens: number;
|
|
29
|
+
output_tokens: number;
|
|
30
|
+
} {
|
|
31
|
+
const isLlm = event.category === "LLM";
|
|
32
|
+
const isTool = event.category === "TOOL";
|
|
33
|
+
const isError = event.level === "error";
|
|
34
|
+
const inputTokens = (event.data?.input_tokens as number) || 0;
|
|
35
|
+
const outputTokens = (event.data?.output_tokens as number) || 0;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
llm_calls: isLlm ? 1 : 0,
|
|
39
|
+
tool_calls: isTool ? 1 : 0,
|
|
40
|
+
errors: isError ? 1 : 0,
|
|
41
|
+
input_tokens: inputTokens,
|
|
42
|
+
output_tokens: outputTokens,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
export function TelemetryPage() {
|
|
24
|
-
const {
|
|
25
|
-
const [
|
|
47
|
+
const { events: realtimeEvents } = useTelemetryContext();
|
|
48
|
+
const [fetchedStats, setFetchedStats] = useState<TelemetryStats | null>(null);
|
|
26
49
|
const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
|
|
27
|
-
const [
|
|
50
|
+
const [fetchedUsage, setFetchedUsage] = useState<UsageByAgent[]>([]);
|
|
28
51
|
const [loading, setLoading] = useState(true);
|
|
29
52
|
const [filter, setFilter] = useState({
|
|
30
53
|
category: "",
|
|
@@ -34,6 +57,9 @@ export function TelemetryPage() {
|
|
|
34
57
|
const [agents, setAgents] = useState<Array<{ id: string; name: string }>>([]);
|
|
35
58
|
const [expandedEvent, setExpandedEvent] = useState<string | null>(null);
|
|
36
59
|
|
|
60
|
+
// Track IDs that were in the fetched stats to avoid double-counting
|
|
61
|
+
const countedEventIdsRef = useRef<Set<string>>(new Set());
|
|
62
|
+
|
|
37
63
|
// Fetch agents for dropdown
|
|
38
64
|
useEffect(() => {
|
|
39
65
|
const fetchAgents = async () => {
|
|
@@ -55,7 +81,7 @@ export function TelemetryPage() {
|
|
|
55
81
|
// Fetch stats
|
|
56
82
|
const statsRes = await fetch("/api/telemetry/stats");
|
|
57
83
|
const statsData = await statsRes.json();
|
|
58
|
-
|
|
84
|
+
setFetchedStats(statsData.stats);
|
|
59
85
|
|
|
60
86
|
// Fetch historical events with filters
|
|
61
87
|
const params = new URLSearchParams();
|
|
@@ -66,12 +92,16 @@ export function TelemetryPage() {
|
|
|
66
92
|
|
|
67
93
|
const eventsRes = await fetch(`/api/telemetry/events?${params}`);
|
|
68
94
|
const eventsData = await eventsRes.json();
|
|
69
|
-
|
|
95
|
+
const events = eventsData.events || [];
|
|
96
|
+
setHistoricalEvents(events);
|
|
97
|
+
|
|
98
|
+
// Mark all fetched event IDs as counted (stats already include them)
|
|
99
|
+
countedEventIdsRef.current = new Set(events.map((e: TelemetryEvent) => e.id));
|
|
70
100
|
|
|
71
101
|
// Fetch usage by agent
|
|
72
102
|
const usageRes = await fetch("/api/telemetry/usage?group_by=agent");
|
|
73
103
|
const usageData = await usageRes.json();
|
|
74
|
-
|
|
104
|
+
setFetchedUsage(usageData.usage || []);
|
|
75
105
|
} catch (e) {
|
|
76
106
|
console.error("Failed to fetch telemetry:", e);
|
|
77
107
|
}
|
|
@@ -85,6 +115,75 @@ export function TelemetryPage() {
|
|
|
85
115
|
return () => clearInterval(interval);
|
|
86
116
|
}, [filter]);
|
|
87
117
|
|
|
118
|
+
// Compute real-time stats from new events (not already counted in fetched stats)
|
|
119
|
+
const stats = useMemo(() => {
|
|
120
|
+
if (!fetchedStats) return null;
|
|
121
|
+
|
|
122
|
+
// Calculate deltas from real-time events not in fetched data
|
|
123
|
+
let deltaEvents = 0;
|
|
124
|
+
let deltaLlmCalls = 0;
|
|
125
|
+
let deltaToolCalls = 0;
|
|
126
|
+
let deltaErrors = 0;
|
|
127
|
+
let deltaInputTokens = 0;
|
|
128
|
+
let deltaOutputTokens = 0;
|
|
129
|
+
|
|
130
|
+
for (const event of realtimeEvents) {
|
|
131
|
+
if (!countedEventIdsRef.current.has(event.id)) {
|
|
132
|
+
deltaEvents++;
|
|
133
|
+
const eventStats = extractEventStats(event);
|
|
134
|
+
deltaLlmCalls += eventStats.llm_calls;
|
|
135
|
+
deltaToolCalls += eventStats.tool_calls;
|
|
136
|
+
deltaErrors += eventStats.errors;
|
|
137
|
+
deltaInputTokens += eventStats.input_tokens;
|
|
138
|
+
deltaOutputTokens += eventStats.output_tokens;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
total_events: fetchedStats.total_events + deltaEvents,
|
|
144
|
+
total_llm_calls: fetchedStats.total_llm_calls + deltaLlmCalls,
|
|
145
|
+
total_tool_calls: fetchedStats.total_tool_calls + deltaToolCalls,
|
|
146
|
+
total_errors: fetchedStats.total_errors + deltaErrors,
|
|
147
|
+
total_input_tokens: fetchedStats.total_input_tokens + deltaInputTokens,
|
|
148
|
+
total_output_tokens: fetchedStats.total_output_tokens + deltaOutputTokens,
|
|
149
|
+
};
|
|
150
|
+
}, [fetchedStats, realtimeEvents]);
|
|
151
|
+
|
|
152
|
+
// Compute real-time usage by agent
|
|
153
|
+
const usage = useMemo(() => {
|
|
154
|
+
// Start with a copy of fetched usage as a map
|
|
155
|
+
const usageMap = new Map<string, UsageByAgent>();
|
|
156
|
+
for (const u of fetchedUsage) {
|
|
157
|
+
usageMap.set(u.agent_id, { ...u });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Add deltas from real-time events
|
|
161
|
+
for (const event of realtimeEvents) {
|
|
162
|
+
if (!countedEventIdsRef.current.has(event.id)) {
|
|
163
|
+
const eventStats = extractEventStats(event);
|
|
164
|
+
const existing = usageMap.get(event.agent_id);
|
|
165
|
+
if (existing) {
|
|
166
|
+
existing.llm_calls += eventStats.llm_calls;
|
|
167
|
+
existing.tool_calls += eventStats.tool_calls;
|
|
168
|
+
existing.errors += eventStats.errors;
|
|
169
|
+
existing.input_tokens += eventStats.input_tokens;
|
|
170
|
+
existing.output_tokens += eventStats.output_tokens;
|
|
171
|
+
} else {
|
|
172
|
+
usageMap.set(event.agent_id, {
|
|
173
|
+
agent_id: event.agent_id,
|
|
174
|
+
llm_calls: eventStats.llm_calls,
|
|
175
|
+
tool_calls: eventStats.tool_calls,
|
|
176
|
+
errors: eventStats.errors,
|
|
177
|
+
input_tokens: eventStats.input_tokens,
|
|
178
|
+
output_tokens: eventStats.output_tokens,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return Array.from(usageMap.values());
|
|
185
|
+
}, [fetchedUsage, realtimeEvents]);
|
|
186
|
+
|
|
88
187
|
// Merge real-time events with historical, filtering and deduping
|
|
89
188
|
const allEvents = React.useMemo(() => {
|
|
90
189
|
// Apply filters to real-time events
|
|
@@ -173,21 +272,11 @@ export function TelemetryPage() {
|
|
|
173
272
|
<div className="flex-1 overflow-auto p-6">
|
|
174
273
|
<div className="max-w-6xl">
|
|
175
274
|
{/* Header */}
|
|
176
|
-
<div className="mb-6
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
</p>
|
|
182
|
-
</div>
|
|
183
|
-
<div className="flex items-center gap-2">
|
|
184
|
-
<span
|
|
185
|
-
className={`w-2 h-2 rounded-full ${connected ? "bg-green-400" : "bg-red-400"}`}
|
|
186
|
-
/>
|
|
187
|
-
<span className="text-xs text-[#666]">
|
|
188
|
-
{connected ? "Live" : "Reconnecting..."}
|
|
189
|
-
</span>
|
|
190
|
-
</div>
|
|
275
|
+
<div className="mb-6">
|
|
276
|
+
<h1 className="text-2xl font-semibold mb-1">Telemetry</h1>
|
|
277
|
+
<p className="text-[#666]">
|
|
278
|
+
Monitor agent activity, token usage, and errors.
|
|
279
|
+
</p>
|
|
191
280
|
</div>
|
|
192
281
|
|
|
193
282
|
{/* Stats Cards */}
|
|
@@ -294,16 +383,15 @@ export function TelemetryPage() {
|
|
|
294
383
|
</div>
|
|
295
384
|
) : (
|
|
296
385
|
<div className="divide-y divide-[#1a1a1a]">
|
|
297
|
-
{allEvents.map((event
|
|
298
|
-
//
|
|
299
|
-
const
|
|
386
|
+
{allEvents.map((event) => {
|
|
387
|
+
// Only mark as new if event arrived in last 10 seconds
|
|
388
|
+
const eventTime = new Date(event.timestamp).getTime();
|
|
389
|
+
const isNew = Date.now() - eventTime < 10000;
|
|
300
390
|
|
|
301
391
|
return (
|
|
302
392
|
<div
|
|
303
393
|
key={event.id}
|
|
304
|
-
className=
|
|
305
|
-
isNew ? "bg-[#0f1a0f]" : ""
|
|
306
|
-
}`}
|
|
394
|
+
className="p-3 hover:bg-[#0a0a0a] transition cursor-pointer"
|
|
307
395
|
onClick={() => setExpandedEvent(expandedEvent === event.id ? null : event.id)}
|
|
308
396
|
>
|
|
309
397
|
<div className="flex items-start gap-3">
|
|
@@ -320,7 +408,7 @@ export function TelemetryPage() {
|
|
|
320
408
|
<span className="text-xs text-[#555]">{event.duration_ms}ms</span>
|
|
321
409
|
)}
|
|
322
410
|
{isNew && (
|
|
323
|
-
<span className="
|
|
411
|
+
<span className="w-1.5 h-1.5 rounded-full bg-green-400" />
|
|
324
412
|
)}
|
|
325
413
|
</div>
|
|
326
414
|
<div className="text-xs text-[#555] mt-1">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from "react";
|
|
1
|
+
import React, { createContext, useContext, useEffect, useState, useCallback, useRef, useMemo } from "react";
|
|
2
2
|
|
|
3
3
|
export interface TelemetryEvent {
|
|
4
4
|
id: string;
|
|
@@ -18,6 +18,7 @@ interface TelemetryContextValue {
|
|
|
18
18
|
connected: boolean;
|
|
19
19
|
events: TelemetryEvent[];
|
|
20
20
|
lastActivityByAgent: Record<string, { timestamp: string; category: string; type: string }>;
|
|
21
|
+
activeAgents: Record<string, { type: string; expiresAt: number }>;
|
|
21
22
|
clearEvents: () => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -29,70 +30,111 @@ export function TelemetryProvider({ children }: { children: React.ReactNode }) {
|
|
|
29
30
|
const [connected, setConnected] = useState(false);
|
|
30
31
|
const [events, setEvents] = useState<TelemetryEvent[]>([]);
|
|
31
32
|
const [lastActivityByAgent, setLastActivityByAgent] = useState<Record<string, { timestamp: string; category: string; type: string }>>({});
|
|
33
|
+
const [activeAgents, setActiveAgents] = useState<Record<string, { type: string; expiresAt: number }>>({});
|
|
32
34
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
33
35
|
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
34
36
|
|
|
37
|
+
// Clean up expired active states
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const interval = setInterval(() => {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
setActiveAgents(prev => {
|
|
42
|
+
const updated: Record<string, { type: string; expiresAt: number }> = {};
|
|
43
|
+
for (const [agentId, state] of Object.entries(prev)) {
|
|
44
|
+
if (state.expiresAt > now) {
|
|
45
|
+
updated[agentId] = state;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return updated;
|
|
49
|
+
});
|
|
50
|
+
}, 500);
|
|
51
|
+
return () => clearInterval(interval);
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
35
54
|
const connect = useCallback(() => {
|
|
36
55
|
if (eventSourceRef.current) {
|
|
37
56
|
eventSourceRef.current.close();
|
|
57
|
+
eventSourceRef.current = null;
|
|
38
58
|
}
|
|
39
59
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
60
|
+
try {
|
|
61
|
+
const es = new EventSource("/api/telemetry/stream");
|
|
62
|
+
eventSourceRef.current = es;
|
|
63
|
+
|
|
64
|
+
es.onopen = () => {
|
|
65
|
+
setConnected(true);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
es.onmessage = (event) => {
|
|
69
|
+
// Ignore keepalive pings (comments starting with :)
|
|
70
|
+
if (!event.data || event.data.trim() === "") return;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const data = JSON.parse(event.data);
|
|
74
|
+
|
|
75
|
+
// Handle connection message
|
|
76
|
+
if (data.connected) {
|
|
77
|
+
setConnected(true);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Handle array of events
|
|
82
|
+
if (Array.isArray(data)) {
|
|
83
|
+
setEvents(prev => {
|
|
84
|
+
const combined = [...data, ...prev];
|
|
85
|
+
return combined.slice(0, MAX_EVENTS);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Update last activity per agent
|
|
89
|
+
setLastActivityByAgent(prev => {
|
|
90
|
+
const updated = { ...prev };
|
|
91
|
+
for (const evt of data) {
|
|
92
|
+
const existing = updated[evt.agent_id];
|
|
93
|
+
if (!existing || new Date(evt.timestamp) > new Date(existing.timestamp)) {
|
|
94
|
+
updated[evt.agent_id] = {
|
|
95
|
+
timestamp: evt.timestamp,
|
|
96
|
+
category: evt.category,
|
|
97
|
+
type: evt.type,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
75
100
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
101
|
+
return updated;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Set agents as active for 3 seconds (tracked in context, not component)
|
|
105
|
+
setActiveAgents(prev => {
|
|
106
|
+
const updated = { ...prev };
|
|
107
|
+
const expiresAt = Date.now() + 3000;
|
|
108
|
+
for (const evt of data) {
|
|
109
|
+
updated[evt.agent_id] = { type: evt.type, expiresAt };
|
|
110
|
+
}
|
|
111
|
+
return updated;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Ignore parse errors (likely keepalive or empty message)
|
|
79
116
|
}
|
|
80
|
-
}
|
|
81
|
-
console.error("Failed to parse telemetry event:", e);
|
|
82
|
-
}
|
|
83
|
-
};
|
|
117
|
+
};
|
|
84
118
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
119
|
+
es.onerror = () => {
|
|
120
|
+
setConnected(false);
|
|
121
|
+
es.close();
|
|
122
|
+
eventSourceRef.current = null;
|
|
89
123
|
|
|
90
|
-
|
|
124
|
+
// Reconnect after 2 seconds
|
|
125
|
+
if (reconnectTimeoutRef.current) {
|
|
126
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
127
|
+
}
|
|
128
|
+
reconnectTimeoutRef.current = setTimeout(connect, 2000);
|
|
129
|
+
};
|
|
130
|
+
} catch {
|
|
131
|
+
// Failed to create EventSource, retry
|
|
132
|
+
setConnected(false);
|
|
91
133
|
if (reconnectTimeoutRef.current) {
|
|
92
134
|
clearTimeout(reconnectTimeoutRef.current);
|
|
93
135
|
}
|
|
94
|
-
reconnectTimeoutRef.current = setTimeout(connect,
|
|
95
|
-
}
|
|
136
|
+
reconnectTimeoutRef.current = setTimeout(connect, 2000);
|
|
137
|
+
}
|
|
96
138
|
}, []);
|
|
97
139
|
|
|
98
140
|
useEffect(() => {
|
|
@@ -113,7 +155,7 @@ export function TelemetryProvider({ children }: { children: React.ReactNode }) {
|
|
|
113
155
|
}, []);
|
|
114
156
|
|
|
115
157
|
return (
|
|
116
|
-
<TelemetryContext.Provider value={{ connected, events, lastActivityByAgent, clearEvents }}>
|
|
158
|
+
<TelemetryContext.Provider value={{ connected, events, lastActivityByAgent, activeAgents, clearEvents }}>
|
|
117
159
|
{children}
|
|
118
160
|
</TelemetryContext.Provider>
|
|
119
161
|
);
|
|
@@ -170,33 +212,13 @@ export function useTelemetry(filter?: {
|
|
|
170
212
|
};
|
|
171
213
|
}
|
|
172
214
|
|
|
173
|
-
// Hook for agent activity indicator
|
|
215
|
+
// Hook for agent activity indicator - uses context-level tracking
|
|
174
216
|
export function useAgentActivity(agentId: string) {
|
|
175
|
-
const {
|
|
176
|
-
const
|
|
177
|
-
const lastActivity = lastActivityByAgent[agentId];
|
|
178
|
-
|
|
179
|
-
useEffect(() => {
|
|
180
|
-
if (!lastActivity) {
|
|
181
|
-
setIsActive(false);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Set active when we get new activity
|
|
186
|
-
setIsActive(true);
|
|
187
|
-
|
|
188
|
-
// Clear active state after 3 seconds of no activity
|
|
189
|
-
const timeout = setTimeout(() => {
|
|
190
|
-
setIsActive(false);
|
|
191
|
-
}, 3000);
|
|
192
|
-
|
|
193
|
-
return () => clearTimeout(timeout);
|
|
194
|
-
}, [lastActivity?.timestamp]);
|
|
217
|
+
const { activeAgents } = useTelemetryContext();
|
|
218
|
+
const activity = activeAgents[agentId];
|
|
195
219
|
|
|
196
220
|
return {
|
|
197
|
-
isActive,
|
|
198
|
-
|
|
199
|
-
category: lastActivity?.category,
|
|
200
|
-
type: lastActivity?.type,
|
|
221
|
+
isActive: !!activity,
|
|
222
|
+
type: activity?.type,
|
|
201
223
|
};
|
|
202
224
|
}
|