apteva 0.2.5 → 0.2.6

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,5 +1,6 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { Select } from "../common/Select";
3
+ import { useTelemetryContext, type TelemetryEvent } from "../../context";
3
4
 
4
5
  interface TelemetryStats {
5
6
  total_events: number;
@@ -10,20 +11,6 @@ interface TelemetryStats {
10
11
  total_output_tokens: number;
11
12
  }
12
13
 
13
- interface TelemetryEvent {
14
- id: string;
15
- agent_id: string;
16
- timestamp: string;
17
- category: string;
18
- type: string;
19
- level: string;
20
- trace_id: string | null;
21
- thread_id: string | null;
22
- data: Record<string, unknown> | null;
23
- duration_ms: number | null;
24
- error: string | null;
25
- }
26
-
27
14
  interface UsageByAgent {
28
15
  agent_id: string;
29
16
  input_tokens: number;
@@ -34,8 +21,9 @@ interface UsageByAgent {
34
21
  }
35
22
 
36
23
  export function TelemetryPage() {
24
+ const { connected, events: realtimeEvents } = useTelemetryContext();
37
25
  const [stats, setStats] = useState<TelemetryStats | null>(null);
38
- const [events, setEvents] = useState<TelemetryEvent[]>([]);
26
+ const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
39
27
  const [usage, setUsage] = useState<UsageByAgent[]>([]);
40
28
  const [loading, setLoading] = useState(true);
41
29
  const [filter, setFilter] = useState({
@@ -60,7 +48,7 @@ export function TelemetryPage() {
60
48
  fetchAgents();
61
49
  }, []);
62
50
 
63
- // Fetch telemetry data
51
+ // Fetch stats and historical data (less frequently now since we have real-time)
64
52
  const fetchData = async () => {
65
53
  setLoading(true);
66
54
  try {
@@ -69,7 +57,7 @@ export function TelemetryPage() {
69
57
  const statsData = await statsRes.json();
70
58
  setStats(statsData.stats);
71
59
 
72
- // Fetch events with filters
60
+ // Fetch historical events with filters
73
61
  const params = new URLSearchParams();
74
62
  if (filter.category) params.set("category", filter.category);
75
63
  if (filter.level) params.set("level", filter.level);
@@ -78,7 +66,7 @@ export function TelemetryPage() {
78
66
 
79
67
  const eventsRes = await fetch(`/api/telemetry/events?${params}`);
80
68
  const eventsData = await eventsRes.json();
81
- setEvents(eventsData.events || []);
69
+ setHistoricalEvents(eventsData.events || []);
82
70
 
83
71
  // Fetch usage by agent
84
72
  const usageRes = await fetch("/api/telemetry/usage?group_by=agent");
@@ -92,11 +80,41 @@ export function TelemetryPage() {
92
80
 
93
81
  useEffect(() => {
94
82
  fetchData();
95
- // Auto-refresh every 30 seconds
96
- const interval = setInterval(fetchData, 30000);
83
+ // Refresh stats every 60 seconds (events come in real-time)
84
+ const interval = setInterval(fetchData, 60000);
97
85
  return () => clearInterval(interval);
98
86
  }, [filter]);
99
87
 
88
+ // Merge real-time events with historical, filtering and deduping
89
+ const allEvents = React.useMemo(() => {
90
+ // Apply filters to real-time events
91
+ let filtered = realtimeEvents;
92
+ if (filter.agent_id) {
93
+ filtered = filtered.filter(e => e.agent_id === filter.agent_id);
94
+ }
95
+ if (filter.category) {
96
+ filtered = filtered.filter(e => e.category === filter.category);
97
+ }
98
+ if (filter.level) {
99
+ filtered = filtered.filter(e => e.level === filter.level);
100
+ }
101
+
102
+ // Merge with historical, dedupe by ID
103
+ const seen = new Set(filtered.map(e => e.id));
104
+ const merged = [...filtered];
105
+ for (const evt of historicalEvents) {
106
+ if (!seen.has(evt.id)) {
107
+ merged.push(evt);
108
+ seen.add(evt.id);
109
+ }
110
+ }
111
+
112
+ // Sort by timestamp descending
113
+ merged.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
114
+
115
+ return merged.slice(0, 100);
116
+ }, [realtimeEvents, historicalEvents, filter]);
117
+
100
118
  const getAgentName = (agentId: string) => {
101
119
  const agent = agents.find(a => a.id === agentId);
102
120
  return agent?.name || agentId;
@@ -155,11 +173,21 @@ export function TelemetryPage() {
155
173
  <div className="flex-1 overflow-auto p-6">
156
174
  <div className="max-w-6xl">
157
175
  {/* Header */}
158
- <div className="mb-6">
159
- <h1 className="text-2xl font-semibold mb-1">Telemetry</h1>
160
- <p className="text-[#666]">
161
- Monitor agent activity, token usage, and errors.
162
- </p>
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>
163
191
  </div>
164
192
 
165
193
  {/* Stats Cards */}
@@ -249,53 +277,68 @@ export function TelemetryPage() {
249
277
 
250
278
  {/* Events List */}
251
279
  <div className="bg-[#111] border border-[#1a1a1a] rounded-lg">
252
- <div className="p-3 border-b border-[#1a1a1a]">
280
+ <div className="p-3 border-b border-[#1a1a1a] flex items-center justify-between">
253
281
  <h2 className="font-medium">Recent Events</h2>
282
+ {realtimeEvents.length > 0 && (
283
+ <span className="text-xs text-[#666]">
284
+ {realtimeEvents.length} new
285
+ </span>
286
+ )}
254
287
  </div>
255
288
 
256
- {loading && events.length === 0 ? (
289
+ {loading && allEvents.length === 0 ? (
257
290
  <div className="p-8 text-center text-[#666]">Loading...</div>
258
- ) : events.length === 0 ? (
291
+ ) : allEvents.length === 0 ? (
259
292
  <div className="p-8 text-center text-[#666]">
260
- No telemetry events yet. Events will appear here once agents start sending data.
293
+ No telemetry events yet. Events will appear here in real-time once agents start sending data.
261
294
  </div>
262
295
  ) : (
263
296
  <div className="divide-y divide-[#1a1a1a]">
264
- {events.map((event) => (
265
- <div
266
- key={event.id}
267
- className="p-3 hover:bg-[#0a0a0a] transition cursor-pointer"
268
- onClick={() => setExpandedEvent(expandedEvent === event.id ? null : event.id)}
269
- >
270
- <div className="flex items-start gap-3">
271
- <span className={`px-2 py-0.5 rounded text-xs border ${categoryColors[event.category] || "bg-[#222] text-[#888] border-[#333]"}`}>
272
- {event.category}
273
- </span>
274
- <div className="flex-1 min-w-0">
275
- <div className="flex items-center gap-2">
276
- <span className="font-medium text-sm">{event.type}</span>
277
- <span className={`text-xs ${levelColors[event.level] || "text-[#666]"}`}>
278
- {event.level}
279
- </span>
280
- {event.duration_ms && (
281
- <span className="text-xs text-[#555]">{event.duration_ms}ms</span>
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);
300
+
301
+ return (
302
+ <div
303
+ key={event.id}
304
+ className={`p-3 hover:bg-[#0a0a0a] transition cursor-pointer ${
305
+ isNew ? "bg-[#0f1a0f]" : ""
306
+ }`}
307
+ onClick={() => setExpandedEvent(expandedEvent === event.id ? null : event.id)}
308
+ >
309
+ <div className="flex items-start gap-3">
310
+ <span className={`px-2 py-0.5 rounded text-xs border ${categoryColors[event.category] || "bg-[#222] text-[#888] border-[#333]"}`}>
311
+ {event.category}
312
+ </span>
313
+ <div className="flex-1 min-w-0">
314
+ <div className="flex items-center gap-2">
315
+ <span className="font-medium text-sm">{event.type}</span>
316
+ <span className={`text-xs ${levelColors[event.level] || "text-[#666]"}`}>
317
+ {event.level}
318
+ </span>
319
+ {event.duration_ms && (
320
+ <span className="text-xs text-[#555]">{event.duration_ms}ms</span>
321
+ )}
322
+ {isNew && (
323
+ <span className="text-xs text-green-400">new</span>
324
+ )}
325
+ </div>
326
+ <div className="text-xs text-[#555] mt-1">
327
+ {getAgentName(event.agent_id)} · {new Date(event.timestamp).toLocaleString()}
328
+ </div>
329
+ {event.error && (
330
+ <div className="text-xs text-red-400 mt-1 font-mono">{event.error}</div>
331
+ )}
332
+ {expandedEvent === event.id && event.data && Object.keys(event.data).length > 0 && (
333
+ <pre className="text-xs text-[#666] mt-2 p-2 bg-[#0a0a0a] rounded overflow-x-auto">
334
+ {JSON.stringify(event.data, null, 2)}
335
+ </pre>
282
336
  )}
283
337
  </div>
284
- <div className="text-xs text-[#555] mt-1">
285
- {getAgentName(event.agent_id)} · {new Date(event.timestamp).toLocaleString()}
286
- </div>
287
- {event.error && (
288
- <div className="text-xs text-red-400 mt-1 font-mono">{event.error}</div>
289
- )}
290
- {expandedEvent === event.id && event.data && Object.keys(event.data).length > 0 && (
291
- <pre className="text-xs text-[#666] mt-2 p-2 bg-[#0a0a0a] rounded overflow-x-auto">
292
- {JSON.stringify(event.data, null, 2)}
293
- </pre>
294
- )}
295
338
  </div>
296
339
  </div>
297
- </div>
298
- ))}
340
+ );
341
+ })}
299
342
  </div>
300
343
  )}
301
344
  </div>
@@ -0,0 +1,202 @@
1
+ import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from "react";
2
+
3
+ export interface TelemetryEvent {
4
+ id: string;
5
+ agent_id: string;
6
+ timestamp: string;
7
+ category: string;
8
+ type: string;
9
+ level: string;
10
+ trace_id?: string;
11
+ thread_id?: string;
12
+ data?: Record<string, unknown>;
13
+ duration_ms?: number;
14
+ error?: string;
15
+ }
16
+
17
+ interface TelemetryContextValue {
18
+ connected: boolean;
19
+ events: TelemetryEvent[];
20
+ lastActivityByAgent: Record<string, { timestamp: string; category: string; type: string }>;
21
+ clearEvents: () => void;
22
+ }
23
+
24
+ const TelemetryContext = createContext<TelemetryContextValue | null>(null);
25
+
26
+ const MAX_EVENTS = 200; // Keep last 200 events in memory
27
+
28
+ export function TelemetryProvider({ children }: { children: React.ReactNode }) {
29
+ const [connected, setConnected] = useState(false);
30
+ const [events, setEvents] = useState<TelemetryEvent[]>([]);
31
+ const [lastActivityByAgent, setLastActivityByAgent] = useState<Record<string, { timestamp: string; category: string; type: string }>>({});
32
+ const eventSourceRef = useRef<EventSource | null>(null);
33
+ const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
34
+
35
+ const connect = useCallback(() => {
36
+ if (eventSourceRef.current) {
37
+ eventSourceRef.current.close();
38
+ }
39
+
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
+ };
75
+ }
76
+ }
77
+ return updated;
78
+ });
79
+ }
80
+ } catch (e) {
81
+ console.error("Failed to parse telemetry event:", e);
82
+ }
83
+ };
84
+
85
+ es.onerror = () => {
86
+ setConnected(false);
87
+ es.close();
88
+ eventSourceRef.current = null;
89
+
90
+ // Reconnect after 3 seconds
91
+ if (reconnectTimeoutRef.current) {
92
+ clearTimeout(reconnectTimeoutRef.current);
93
+ }
94
+ reconnectTimeoutRef.current = setTimeout(connect, 3000);
95
+ };
96
+ }, []);
97
+
98
+ useEffect(() => {
99
+ connect();
100
+
101
+ return () => {
102
+ if (eventSourceRef.current) {
103
+ eventSourceRef.current.close();
104
+ }
105
+ if (reconnectTimeoutRef.current) {
106
+ clearTimeout(reconnectTimeoutRef.current);
107
+ }
108
+ };
109
+ }, [connect]);
110
+
111
+ const clearEvents = useCallback(() => {
112
+ setEvents([]);
113
+ }, []);
114
+
115
+ return (
116
+ <TelemetryContext.Provider value={{ connected, events, lastActivityByAgent, clearEvents }}>
117
+ {children}
118
+ </TelemetryContext.Provider>
119
+ );
120
+ }
121
+
122
+ // Hook to access all telemetry
123
+ export function useTelemetryContext() {
124
+ const context = useContext(TelemetryContext);
125
+ if (!context) {
126
+ throw new Error("useTelemetryContext must be used within TelemetryProvider");
127
+ }
128
+ return context;
129
+ }
130
+
131
+ // Hook to filter telemetry for a specific agent or category
132
+ export function useTelemetry(filter?: {
133
+ agent_id?: string;
134
+ category?: string;
135
+ limit?: number;
136
+ }) {
137
+ const { connected, events, lastActivityByAgent } = useTelemetryContext();
138
+
139
+ const filteredEvents = React.useMemo(() => {
140
+ let result = events;
141
+
142
+ if (filter?.agent_id) {
143
+ result = result.filter(e => e.agent_id === filter.agent_id);
144
+ }
145
+ if (filter?.category) {
146
+ result = result.filter(e => e.category === filter.category);
147
+ }
148
+ if (filter?.limit) {
149
+ result = result.slice(0, filter.limit);
150
+ }
151
+
152
+ return result;
153
+ }, [events, filter?.agent_id, filter?.category, filter?.limit]);
154
+
155
+ const lastActivity = filter?.agent_id ? lastActivityByAgent[filter.agent_id] : undefined;
156
+
157
+ // Check if agent is "active" (had activity in last 10 seconds)
158
+ const isActive = React.useMemo(() => {
159
+ if (!lastActivity) return false;
160
+ const activityTime = new Date(lastActivity.timestamp).getTime();
161
+ const now = Date.now();
162
+ return now - activityTime < 10000; // 10 seconds
163
+ }, [lastActivity]);
164
+
165
+ return {
166
+ connected,
167
+ events: filteredEvents,
168
+ lastActivity,
169
+ isActive,
170
+ };
171
+ }
172
+
173
+ // Hook for agent activity indicator
174
+ 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]);
195
+
196
+ return {
197
+ isActive,
198
+ lastActivity,
199
+ category: lastActivity?.category,
200
+ type: lastActivity?.type,
201
+ };
202
+ }
@@ -0,0 +1,2 @@
1
+ export { TelemetryProvider, useTelemetryContext, useTelemetry, useAgentActivity } from "./TelemetryContext";
2
+ export type { TelemetryEvent } from "./TelemetryContext";