@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.
Files changed (69) hide show
  1. package/ARCHITECTURE.md +183 -0
  2. package/agenticmail-enterprise.db +0 -0
  3. package/dashboards/README.md +120 -0
  4. package/dashboards/dotnet/Program.cs +261 -0
  5. package/dashboards/express/app.js +146 -0
  6. package/dashboards/go/main.go +513 -0
  7. package/dashboards/html/index.html +535 -0
  8. package/dashboards/java/AgenticMailDashboard.java +376 -0
  9. package/dashboards/php/index.php +414 -0
  10. package/dashboards/python/app.py +273 -0
  11. package/dashboards/ruby/app.rb +195 -0
  12. package/dist/chunk-77IDQJL3.js +7 -0
  13. package/dist/chunk-7RGCCHIT.js +115 -0
  14. package/dist/chunk-DXNKR3TG.js +1355 -0
  15. package/dist/chunk-IQWA44WT.js +970 -0
  16. package/dist/chunk-LCUZGIDH.js +965 -0
  17. package/dist/chunk-N2JVTNNJ.js +2553 -0
  18. package/dist/chunk-O462UJBH.js +363 -0
  19. package/dist/chunk-PNKVD2UK.js +26 -0
  20. package/dist/cli.js +218 -0
  21. package/dist/dashboard/index.html +558 -0
  22. package/dist/db-adapter-DEWEFNIV.js +7 -0
  23. package/dist/dynamodb-CCGL2E77.js +426 -0
  24. package/dist/engine/index.js +1261 -0
  25. package/dist/index.js +522 -0
  26. package/dist/mongodb-ODTXIVPV.js +319 -0
  27. package/dist/mysql-RM3S2FV5.js +521 -0
  28. package/dist/postgres-LN7A6MGQ.js +518 -0
  29. package/dist/routes-2JEPIIKC.js +441 -0
  30. package/dist/routes-74ZLKJKP.js +399 -0
  31. package/dist/server.js +7 -0
  32. package/dist/sqlite-3K5YOZ4K.js +439 -0
  33. package/dist/turso-LDWODSDI.js +442 -0
  34. package/package.json +49 -0
  35. package/src/admin/routes.ts +331 -0
  36. package/src/auth/routes.ts +130 -0
  37. package/src/cli.ts +260 -0
  38. package/src/dashboard/index.html +558 -0
  39. package/src/db/adapter.ts +230 -0
  40. package/src/db/dynamodb.ts +456 -0
  41. package/src/db/factory.ts +51 -0
  42. package/src/db/mongodb.ts +360 -0
  43. package/src/db/mysql.ts +472 -0
  44. package/src/db/postgres.ts +479 -0
  45. package/src/db/sql-schema.ts +123 -0
  46. package/src/db/sqlite.ts +391 -0
  47. package/src/db/turso.ts +411 -0
  48. package/src/deploy/fly.ts +368 -0
  49. package/src/deploy/managed.ts +213 -0
  50. package/src/engine/activity.ts +474 -0
  51. package/src/engine/agent-config.ts +429 -0
  52. package/src/engine/agenticmail-bridge.ts +296 -0
  53. package/src/engine/approvals.ts +278 -0
  54. package/src/engine/db-adapter.ts +682 -0
  55. package/src/engine/db-schema.ts +335 -0
  56. package/src/engine/deployer.ts +595 -0
  57. package/src/engine/index.ts +134 -0
  58. package/src/engine/knowledge.ts +486 -0
  59. package/src/engine/lifecycle.ts +635 -0
  60. package/src/engine/openclaw-hook.ts +371 -0
  61. package/src/engine/routes.ts +528 -0
  62. package/src/engine/skills.ts +473 -0
  63. package/src/engine/tenant.ts +345 -0
  64. package/src/engine/tool-catalog.ts +189 -0
  65. package/src/index.ts +64 -0
  66. package/src/lib/resilience.ts +326 -0
  67. package/src/middleware/index.ts +286 -0
  68. package/src/server.ts +310 -0
  69. 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
+ }