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.
@@ -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 { connected, events: realtimeEvents } = useTelemetryContext();
25
- const [stats, setStats] = useState<TelemetryStats | null>(null);
47
+ const { events: realtimeEvents } = useTelemetryContext();
48
+ const [fetchedStats, setFetchedStats] = useState<TelemetryStats | null>(null);
26
49
  const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
27
- const [usage, setUsage] = useState<UsageByAgent[]>([]);
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
- setStats(statsData.stats);
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
- setHistoricalEvents(eventsData.events || []);
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
- setUsage(usageData.usage || []);
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 flex items-center justify-between">
177
- <div>
178
- <h1 className="text-2xl font-semibold mb-1">Telemetry</h1>
179
- <p className="text-[#666]">
180
- Monitor agent activity, token usage, and errors.
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, index) => {
298
- // Check if this is a new real-time event (in first few positions and recent)
299
- const isNew = index < 3 && realtimeEvents.some(e => e.id === event.id);
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={`p-3 hover:bg-[#0a0a0a] transition cursor-pointer ${
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="text-xs text-green-400">new</span>
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
- const es = new EventSource("/api/telemetry/stream");
41
- eventSourceRef.current = es;
42
-
43
- es.onopen = () => {
44
- setConnected(true);
45
- };
46
-
47
- es.onmessage = (event) => {
48
- try {
49
- const data = JSON.parse(event.data);
50
-
51
- // Handle connection message
52
- if (data.connected) {
53
- setConnected(true);
54
- return;
55
- }
56
-
57
- // Handle array of events
58
- if (Array.isArray(data)) {
59
- setEvents(prev => {
60
- const combined = [...data, ...prev];
61
- return combined.slice(0, MAX_EVENTS);
62
- });
63
-
64
- // Update last activity per agent
65
- setLastActivityByAgent(prev => {
66
- const updated = { ...prev };
67
- for (const evt of data) {
68
- const existing = updated[evt.agent_id];
69
- if (!existing || new Date(evt.timestamp) > new Date(existing.timestamp)) {
70
- updated[evt.agent_id] = {
71
- timestamp: evt.timestamp,
72
- category: evt.category,
73
- type: evt.type,
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
- return updated;
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
- } catch (e) {
81
- console.error("Failed to parse telemetry event:", e);
82
- }
83
- };
117
+ };
84
118
 
85
- es.onerror = () => {
86
- setConnected(false);
87
- es.close();
88
- eventSourceRef.current = null;
119
+ es.onerror = () => {
120
+ setConnected(false);
121
+ es.close();
122
+ eventSourceRef.current = null;
89
123
 
90
- // Reconnect after 3 seconds
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, 3000);
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 { lastActivityByAgent } = useTelemetryContext();
176
- const [isActive, setIsActive] = useState(false);
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
- lastActivity,
199
- category: lastActivity?.category,
200
- type: lastActivity?.type,
221
+ isActive: !!activity,
222
+ type: activity?.type,
201
223
  };
202
224
  }