@agenticmail/enterprise 0.2.1
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/ARCHITECTURE.md +183 -0
- package/agenticmail-enterprise.db +0 -0
- package/dashboards/README.md +120 -0
- package/dashboards/dotnet/Program.cs +261 -0
- package/dashboards/express/app.js +146 -0
- package/dashboards/go/main.go +513 -0
- package/dashboards/html/index.html +535 -0
- package/dashboards/java/AgenticMailDashboard.java +376 -0
- package/dashboards/php/index.php +414 -0
- package/dashboards/python/app.py +273 -0
- package/dashboards/ruby/app.rb +195 -0
- package/dist/chunk-77IDQJL3.js +7 -0
- package/dist/chunk-7RGCCHIT.js +115 -0
- package/dist/chunk-DXNKR3TG.js +1355 -0
- package/dist/chunk-IQWA44WT.js +970 -0
- package/dist/chunk-LCUZGIDH.js +965 -0
- package/dist/chunk-N2JVTNNJ.js +2553 -0
- package/dist/chunk-O462UJBH.js +363 -0
- package/dist/chunk-PNKVD2UK.js +26 -0
- package/dist/cli.js +218 -0
- package/dist/dashboard/index.html +558 -0
- package/dist/db-adapter-DEWEFNIV.js +7 -0
- package/dist/dynamodb-CCGL2E77.js +426 -0
- package/dist/engine/index.js +1261 -0
- package/dist/index.js +522 -0
- package/dist/mongodb-ODTXIVPV.js +319 -0
- package/dist/mysql-RM3S2FV5.js +521 -0
- package/dist/postgres-LN7A6MGQ.js +518 -0
- package/dist/routes-2JEPIIKC.js +441 -0
- package/dist/routes-74ZLKJKP.js +399 -0
- package/dist/server.js +7 -0
- package/dist/sqlite-3K5YOZ4K.js +439 -0
- package/dist/turso-LDWODSDI.js +442 -0
- package/package.json +49 -0
- package/src/admin/routes.ts +331 -0
- package/src/auth/routes.ts +130 -0
- package/src/cli.ts +260 -0
- package/src/dashboard/index.html +558 -0
- package/src/db/adapter.ts +230 -0
- package/src/db/dynamodb.ts +456 -0
- package/src/db/factory.ts +51 -0
- package/src/db/mongodb.ts +360 -0
- package/src/db/mysql.ts +472 -0
- package/src/db/postgres.ts +479 -0
- package/src/db/sql-schema.ts +123 -0
- package/src/db/sqlite.ts +391 -0
- package/src/db/turso.ts +411 -0
- package/src/deploy/fly.ts +368 -0
- package/src/deploy/managed.ts +213 -0
- package/src/engine/activity.ts +474 -0
- package/src/engine/agent-config.ts +429 -0
- package/src/engine/agenticmail-bridge.ts +296 -0
- package/src/engine/approvals.ts +278 -0
- package/src/engine/db-adapter.ts +682 -0
- package/src/engine/db-schema.ts +335 -0
- package/src/engine/deployer.ts +595 -0
- package/src/engine/index.ts +134 -0
- package/src/engine/knowledge.ts +486 -0
- package/src/engine/lifecycle.ts +635 -0
- package/src/engine/openclaw-hook.ts +371 -0
- package/src/engine/routes.ts +528 -0
- package/src/engine/skills.ts +473 -0
- package/src/engine/tenant.ts +345 -0
- package/src/engine/tool-catalog.ts +189 -0
- package/src/index.ts +64 -0
- package/src/lib/resilience.ts +326 -0
- package/src/middleware/index.ts +286 -0
- package/src/server.ts +310 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-Time Activity & Observability
|
|
3
|
+
*
|
|
4
|
+
* See what every agent is doing RIGHT NOW:
|
|
5
|
+
* - Live tool call stream
|
|
6
|
+
* - Conversation logs
|
|
7
|
+
* - Error tracking
|
|
8
|
+
* - Cost tracking per call
|
|
9
|
+
* - Session timeline
|
|
10
|
+
*
|
|
11
|
+
* This powers the "live view" in the dashboard where admins
|
|
12
|
+
* can watch their agent employee work in real-time.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ─── Types ──────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export interface ActivityEvent {
|
|
18
|
+
id: string;
|
|
19
|
+
agentId: string;
|
|
20
|
+
orgId: string;
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
type: ActivityType;
|
|
24
|
+
data: Record<string, any>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ActivityType =
|
|
28
|
+
| 'session_start'
|
|
29
|
+
| 'session_end'
|
|
30
|
+
| 'message_received' // Agent received a message from user/channel
|
|
31
|
+
| 'message_sent' // Agent sent a reply
|
|
32
|
+
| 'tool_call_start' // Agent is calling a tool
|
|
33
|
+
| 'tool_call_end' // Tool returned result
|
|
34
|
+
| 'tool_call_error' // Tool call failed
|
|
35
|
+
| 'tool_blocked' // Permission engine blocked a tool
|
|
36
|
+
| 'approval_requested' // Waiting for human approval
|
|
37
|
+
| 'approval_decided' // Human approved/denied
|
|
38
|
+
| 'email_received'
|
|
39
|
+
| 'email_sent'
|
|
40
|
+
| 'task_assigned' // Agent assigned to another agent
|
|
41
|
+
| 'task_completed'
|
|
42
|
+
| 'error' // General error
|
|
43
|
+
| 'warning' // Non-fatal issue
|
|
44
|
+
| 'heartbeat' // Periodic check-in
|
|
45
|
+
| 'memory_write' // Agent wrote to memory
|
|
46
|
+
| 'budget_alert'; // Approaching or exceeding budget
|
|
47
|
+
|
|
48
|
+
export interface ToolCallRecord {
|
|
49
|
+
id: string;
|
|
50
|
+
agentId: string;
|
|
51
|
+
orgId: string;
|
|
52
|
+
sessionId: string;
|
|
53
|
+
toolId: string;
|
|
54
|
+
toolName: string;
|
|
55
|
+
parameters: Record<string, any>; // Sanitized (no secrets)
|
|
56
|
+
result?: {
|
|
57
|
+
success: boolean;
|
|
58
|
+
truncatedOutput?: string; // First 500 chars
|
|
59
|
+
error?: string;
|
|
60
|
+
};
|
|
61
|
+
timing: {
|
|
62
|
+
startedAt: string;
|
|
63
|
+
completedAt?: string;
|
|
64
|
+
durationMs?: number;
|
|
65
|
+
};
|
|
66
|
+
cost?: {
|
|
67
|
+
inputTokens: number;
|
|
68
|
+
outputTokens: number;
|
|
69
|
+
estimatedCostUsd: number;
|
|
70
|
+
};
|
|
71
|
+
permission: {
|
|
72
|
+
allowed: boolean;
|
|
73
|
+
reason: string;
|
|
74
|
+
requiredApproval: boolean;
|
|
75
|
+
approvalId?: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ConversationEntry {
|
|
80
|
+
id: string;
|
|
81
|
+
agentId: string;
|
|
82
|
+
sessionId: string;
|
|
83
|
+
timestamp: string;
|
|
84
|
+
role: 'user' | 'assistant' | 'system';
|
|
85
|
+
content: string; // Truncated for storage
|
|
86
|
+
channel?: string; // Which channel (email, whatsapp, slack, etc.)
|
|
87
|
+
tokenCount: number;
|
|
88
|
+
toolCalls?: string[]; // Tool call IDs referenced in this turn
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface AgentTimeline {
|
|
92
|
+
agentId: string;
|
|
93
|
+
date: string; // YYYY-MM-DD
|
|
94
|
+
events: TimelineEntry[];
|
|
95
|
+
summary: {
|
|
96
|
+
totalSessions: number;
|
|
97
|
+
totalMessages: number;
|
|
98
|
+
totalToolCalls: number;
|
|
99
|
+
totalErrors: number;
|
|
100
|
+
totalTokens: number;
|
|
101
|
+
totalCostUsd: number;
|
|
102
|
+
topTools: { toolId: string; count: number }[];
|
|
103
|
+
activeHours: number[]; // Which hours the agent was active (0-23)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface TimelineEntry {
|
|
108
|
+
timestamp: string;
|
|
109
|
+
type: ActivityType;
|
|
110
|
+
summary: string; // Human-readable one-liner
|
|
111
|
+
details?: Record<string, any>;
|
|
112
|
+
durationMs?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Activity Tracker ───────────────────────────────────
|
|
116
|
+
|
|
117
|
+
export class ActivityTracker {
|
|
118
|
+
private events: ActivityEvent[] = [];
|
|
119
|
+
private toolCalls = new Map<string, ToolCallRecord>();
|
|
120
|
+
private conversations: ConversationEntry[] = [];
|
|
121
|
+
private listeners: ((event: ActivityEvent) => void)[] = [];
|
|
122
|
+
private sseClients = new Set<(event: ActivityEvent) => void>();
|
|
123
|
+
|
|
124
|
+
// Buffer settings
|
|
125
|
+
private maxEvents = 10_000; // Keep last N events in memory
|
|
126
|
+
private maxToolCalls = 5_000;
|
|
127
|
+
private maxConversations = 5_000;
|
|
128
|
+
|
|
129
|
+
// ─── Record Events ───────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Record a generic activity event
|
|
133
|
+
*/
|
|
134
|
+
record(event: Omit<ActivityEvent, 'id' | 'timestamp'>): ActivityEvent {
|
|
135
|
+
const full: ActivityEvent = {
|
|
136
|
+
...event,
|
|
137
|
+
id: crypto.randomUUID(),
|
|
138
|
+
timestamp: new Date().toISOString(),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
this.events.push(full);
|
|
142
|
+
if (this.events.length > this.maxEvents) {
|
|
143
|
+
this.events = this.events.slice(-this.maxEvents);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Notify listeners
|
|
147
|
+
for (const listener of this.listeners) {
|
|
148
|
+
try { listener(full); } catch { /* ignore */ }
|
|
149
|
+
}
|
|
150
|
+
for (const client of this.sseClients) {
|
|
151
|
+
try { client(full); } catch { this.sseClients.delete(client); }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return full;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Record a tool call starting
|
|
159
|
+
*/
|
|
160
|
+
startToolCall(opts: {
|
|
161
|
+
agentId: string;
|
|
162
|
+
orgId: string;
|
|
163
|
+
sessionId: string;
|
|
164
|
+
toolId: string;
|
|
165
|
+
toolName: string;
|
|
166
|
+
parameters: Record<string, any>;
|
|
167
|
+
permission: ToolCallRecord['permission'];
|
|
168
|
+
}): ToolCallRecord {
|
|
169
|
+
const record: ToolCallRecord = {
|
|
170
|
+
id: crypto.randomUUID(),
|
|
171
|
+
agentId: opts.agentId,
|
|
172
|
+
orgId: opts.orgId,
|
|
173
|
+
sessionId: opts.sessionId,
|
|
174
|
+
toolId: opts.toolId,
|
|
175
|
+
toolName: opts.toolName,
|
|
176
|
+
parameters: this.sanitizeParams(opts.parameters),
|
|
177
|
+
timing: { startedAt: new Date().toISOString() },
|
|
178
|
+
permission: opts.permission,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
this.toolCalls.set(record.id, record);
|
|
182
|
+
if (this.toolCalls.size > this.maxToolCalls) {
|
|
183
|
+
// Remove oldest entries
|
|
184
|
+
const keys = Array.from(this.toolCalls.keys());
|
|
185
|
+
for (let i = 0; i < 1000; i++) this.toolCalls.delete(keys[i]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.record({
|
|
189
|
+
agentId: opts.agentId,
|
|
190
|
+
orgId: opts.orgId,
|
|
191
|
+
sessionId: opts.sessionId,
|
|
192
|
+
type: opts.permission.allowed ? 'tool_call_start' : 'tool_blocked',
|
|
193
|
+
data: { toolCallId: record.id, toolId: opts.toolId, toolName: opts.toolName },
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return record;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Record a tool call completing
|
|
201
|
+
*/
|
|
202
|
+
endToolCall(toolCallId: string, result: {
|
|
203
|
+
success: boolean;
|
|
204
|
+
output?: string;
|
|
205
|
+
error?: string;
|
|
206
|
+
inputTokens?: number;
|
|
207
|
+
outputTokens?: number;
|
|
208
|
+
costUsd?: number;
|
|
209
|
+
}) {
|
|
210
|
+
const record = this.toolCalls.get(toolCallId);
|
|
211
|
+
if (!record) return;
|
|
212
|
+
|
|
213
|
+
record.timing.completedAt = new Date().toISOString();
|
|
214
|
+
record.timing.durationMs = new Date(record.timing.completedAt).getTime() -
|
|
215
|
+
new Date(record.timing.startedAt).getTime();
|
|
216
|
+
|
|
217
|
+
record.result = {
|
|
218
|
+
success: result.success,
|
|
219
|
+
truncatedOutput: result.output?.slice(0, 500),
|
|
220
|
+
error: result.error,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (result.inputTokens || result.outputTokens || result.costUsd) {
|
|
224
|
+
record.cost = {
|
|
225
|
+
inputTokens: result.inputTokens || 0,
|
|
226
|
+
outputTokens: result.outputTokens || 0,
|
|
227
|
+
estimatedCostUsd: result.costUsd || 0,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.record({
|
|
232
|
+
agentId: record.agentId,
|
|
233
|
+
orgId: record.orgId,
|
|
234
|
+
sessionId: record.sessionId,
|
|
235
|
+
type: result.success ? 'tool_call_end' : 'tool_call_error',
|
|
236
|
+
data: {
|
|
237
|
+
toolCallId, toolId: record.toolId,
|
|
238
|
+
durationMs: record.timing.durationMs,
|
|
239
|
+
success: result.success,
|
|
240
|
+
error: result.error,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Record a conversation message
|
|
247
|
+
*/
|
|
248
|
+
recordMessage(entry: Omit<ConversationEntry, 'id' | 'timestamp'>): ConversationEntry {
|
|
249
|
+
const full: ConversationEntry = {
|
|
250
|
+
...entry,
|
|
251
|
+
id: crypto.randomUUID(),
|
|
252
|
+
timestamp: new Date().toISOString(),
|
|
253
|
+
content: entry.content.slice(0, 2000), // Truncate
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
this.conversations.push(full);
|
|
257
|
+
if (this.conversations.length > this.maxConversations) {
|
|
258
|
+
this.conversations = this.conversations.slice(-this.maxConversations);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.record({
|
|
262
|
+
agentId: entry.agentId,
|
|
263
|
+
orgId: '',
|
|
264
|
+
sessionId: entry.sessionId,
|
|
265
|
+
type: entry.role === 'user' ? 'message_received' : 'message_sent',
|
|
266
|
+
data: { messageId: full.id, role: entry.role, channel: entry.channel, tokenCount: entry.tokenCount },
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return full;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ─── Query ──────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get recent events for an agent
|
|
276
|
+
*/
|
|
277
|
+
getEvents(opts: {
|
|
278
|
+
agentId?: string;
|
|
279
|
+
orgId?: string;
|
|
280
|
+
types?: ActivityType[];
|
|
281
|
+
since?: string;
|
|
282
|
+
limit?: number;
|
|
283
|
+
}): ActivityEvent[] {
|
|
284
|
+
let events = [...this.events];
|
|
285
|
+
|
|
286
|
+
if (opts.agentId) events = events.filter(e => e.agentId === opts.agentId);
|
|
287
|
+
if (opts.orgId) events = events.filter(e => e.orgId === opts.orgId);
|
|
288
|
+
if (opts.types?.length) events = events.filter(e => opts.types!.includes(e.type));
|
|
289
|
+
if (opts.since) {
|
|
290
|
+
const sinceTs = new Date(opts.since).getTime();
|
|
291
|
+
events = events.filter(e => new Date(e.timestamp).getTime() >= sinceTs);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
events.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
295
|
+
return events.slice(0, opts.limit || 50);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get tool call history for an agent
|
|
300
|
+
*/
|
|
301
|
+
getToolCalls(opts: {
|
|
302
|
+
agentId?: string;
|
|
303
|
+
orgId?: string;
|
|
304
|
+
toolId?: string;
|
|
305
|
+
limit?: number;
|
|
306
|
+
}): ToolCallRecord[] {
|
|
307
|
+
let calls = Array.from(this.toolCalls.values());
|
|
308
|
+
if (opts.agentId) calls = calls.filter(c => c.agentId === opts.agentId);
|
|
309
|
+
if (opts.orgId) calls = calls.filter(c => c.orgId === opts.orgId);
|
|
310
|
+
if (opts.toolId) calls = calls.filter(c => c.toolId === opts.toolId);
|
|
311
|
+
calls.sort((a, b) => new Date(b.timing.startedAt).getTime() - new Date(a.timing.startedAt).getTime());
|
|
312
|
+
return calls.slice(0, opts.limit || 50);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get conversation history for a session
|
|
317
|
+
*/
|
|
318
|
+
getConversation(sessionId: string, limit: number = 50): ConversationEntry[] {
|
|
319
|
+
return this.conversations
|
|
320
|
+
.filter(c => c.sessionId === sessionId)
|
|
321
|
+
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
|
|
322
|
+
.slice(-limit);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Generate a daily timeline for an agent
|
|
327
|
+
*/
|
|
328
|
+
getTimeline(agentId: string, date: string): AgentTimeline {
|
|
329
|
+
const dayStart = new Date(date + 'T00:00:00Z');
|
|
330
|
+
const dayEnd = new Date(date + 'T23:59:59Z');
|
|
331
|
+
|
|
332
|
+
const dayEvents = this.events.filter(e => {
|
|
333
|
+
if (e.agentId !== agentId) return false;
|
|
334
|
+
const ts = new Date(e.timestamp).getTime();
|
|
335
|
+
return ts >= dayStart.getTime() && ts <= dayEnd.getTime();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const dayToolCalls = Array.from(this.toolCalls.values()).filter(tc => {
|
|
339
|
+
if (tc.agentId !== agentId) return false;
|
|
340
|
+
const ts = new Date(tc.timing.startedAt).getTime();
|
|
341
|
+
return ts >= dayStart.getTime() && ts <= dayEnd.getTime();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Compute tool usage stats
|
|
345
|
+
const toolCounts = new Map<string, number>();
|
|
346
|
+
let totalTokens = 0, totalCost = 0, totalErrors = 0;
|
|
347
|
+
const activeHours = new Set<number>();
|
|
348
|
+
|
|
349
|
+
for (const tc of dayToolCalls) {
|
|
350
|
+
toolCounts.set(tc.toolId, (toolCounts.get(tc.toolId) || 0) + 1);
|
|
351
|
+
if (tc.cost) {
|
|
352
|
+
totalTokens += tc.cost.inputTokens + tc.cost.outputTokens;
|
|
353
|
+
totalCost += tc.cost.estimatedCostUsd;
|
|
354
|
+
}
|
|
355
|
+
if (tc.result && !tc.result.success) totalErrors++;
|
|
356
|
+
activeHours.add(new Date(tc.timing.startedAt).getUTCHours());
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const topTools = Array.from(toolCounts.entries())
|
|
360
|
+
.map(([toolId, count]) => ({ toolId, count }))
|
|
361
|
+
.sort((a, b) => b.count - a.count)
|
|
362
|
+
.slice(0, 10);
|
|
363
|
+
|
|
364
|
+
const totalMessages = dayEvents.filter(e =>
|
|
365
|
+
e.type === 'message_received' || e.type === 'message_sent'
|
|
366
|
+
).length;
|
|
367
|
+
|
|
368
|
+
const totalSessions = new Set(dayEvents.filter(e => e.sessionId).map(e => e.sessionId)).size;
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
agentId,
|
|
372
|
+
date,
|
|
373
|
+
events: dayEvents.map(e => ({
|
|
374
|
+
timestamp: e.timestamp,
|
|
375
|
+
type: e.type,
|
|
376
|
+
summary: this.summarizeEvent(e),
|
|
377
|
+
details: e.data,
|
|
378
|
+
})),
|
|
379
|
+
summary: {
|
|
380
|
+
totalSessions,
|
|
381
|
+
totalMessages,
|
|
382
|
+
totalToolCalls: dayToolCalls.length,
|
|
383
|
+
totalErrors,
|
|
384
|
+
totalTokens,
|
|
385
|
+
totalCostUsd: totalCost,
|
|
386
|
+
topTools,
|
|
387
|
+
activeHours: Array.from(activeHours).sort((a, b) => a - b),
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ─── Real-time Streaming (SSE) ────────────────────────
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Subscribe to real-time events (for SSE endpoint)
|
|
396
|
+
*/
|
|
397
|
+
subscribe(callback: (event: ActivityEvent) => void): () => void {
|
|
398
|
+
this.sseClients.add(callback);
|
|
399
|
+
return () => this.sseClients.delete(callback);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Subscribe to events (general listener)
|
|
404
|
+
*/
|
|
405
|
+
onEvent(listener: (event: ActivityEvent) => void): () => void {
|
|
406
|
+
this.listeners.push(listener);
|
|
407
|
+
return () => { this.listeners = this.listeners.filter(l => l !== listener); };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ─── Stats ──────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Get real-time stats for dashboard
|
|
414
|
+
*/
|
|
415
|
+
getStats(orgId?: string): {
|
|
416
|
+
eventsLast5min: number;
|
|
417
|
+
toolCallsLast5min: number;
|
|
418
|
+
errorsLast5min: number;
|
|
419
|
+
activeAgents: string[];
|
|
420
|
+
activeSessions: number;
|
|
421
|
+
} {
|
|
422
|
+
const fiveMinAgo = new Date(Date.now() - 5 * 60_000).toISOString();
|
|
423
|
+
let recent = this.events.filter(e => e.timestamp >= fiveMinAgo);
|
|
424
|
+
if (orgId) recent = recent.filter(e => e.orgId === orgId);
|
|
425
|
+
|
|
426
|
+
const toolCalls = recent.filter(e => e.type === 'tool_call_start' || e.type === 'tool_call_end');
|
|
427
|
+
const errors = recent.filter(e => e.type === 'tool_call_error' || e.type === 'error');
|
|
428
|
+
const activeAgents = [...new Set(recent.map(e => e.agentId))];
|
|
429
|
+
const activeSessions = new Set(recent.filter(e => e.sessionId).map(e => e.sessionId)).size;
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
eventsLast5min: recent.length,
|
|
433
|
+
toolCallsLast5min: toolCalls.length,
|
|
434
|
+
errorsLast5min: errors.length,
|
|
435
|
+
activeAgents,
|
|
436
|
+
activeSessions,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ─── Private ──────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
private sanitizeParams(params: Record<string, any>): Record<string, any> {
|
|
443
|
+
const sanitized = { ...params };
|
|
444
|
+
const sensitiveKeys = ['password', 'token', 'secret', 'key', 'apiKey', 'credential', 'authorization'];
|
|
445
|
+
for (const key of Object.keys(sanitized)) {
|
|
446
|
+
if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk))) {
|
|
447
|
+
sanitized[key] = '***';
|
|
448
|
+
}
|
|
449
|
+
// Truncate large values
|
|
450
|
+
if (typeof sanitized[key] === 'string' && sanitized[key].length > 200) {
|
|
451
|
+
sanitized[key] = sanitized[key].slice(0, 200) + '...';
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return sanitized;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private summarizeEvent(event: ActivityEvent): string {
|
|
458
|
+
switch (event.type) {
|
|
459
|
+
case 'session_start': return 'Session started';
|
|
460
|
+
case 'session_end': return 'Session ended';
|
|
461
|
+
case 'message_received': return `Received message via ${event.data.channel || 'unknown'}`;
|
|
462
|
+
case 'message_sent': return `Sent reply via ${event.data.channel || 'unknown'}`;
|
|
463
|
+
case 'tool_call_start': return `Called ${event.data.toolName || event.data.toolId}`;
|
|
464
|
+
case 'tool_call_end': return `${event.data.toolName || event.data.toolId} completed (${event.data.durationMs}ms)`;
|
|
465
|
+
case 'tool_call_error': return `${event.data.toolName || event.data.toolId} failed: ${event.data.error}`;
|
|
466
|
+
case 'tool_blocked': return `Blocked: ${event.data.toolName || event.data.toolId}`;
|
|
467
|
+
case 'email_received': return 'Received email';
|
|
468
|
+
case 'email_sent': return 'Sent email';
|
|
469
|
+
case 'error': return `Error: ${event.data.message || 'Unknown'}`;
|
|
470
|
+
case 'heartbeat': return 'Heartbeat check';
|
|
471
|
+
default: return event.type;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|