dank-ai 1.0.34 → 1.0.36

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * Dank Agent Container Entrypoint
5
- *
5
+ *
6
6
  * This script runs inside each agent container and handles:
7
7
  * - Loading agent code from the drop-off directory
8
8
  * - Setting up the LLM client
@@ -15,6 +15,10 @@ const path = require("path");
15
15
  const express = require("express");
16
16
  const winston = require("winston");
17
17
  const { v4: uuidv4 } = require("uuid");
18
+ const os = require("os");
19
+ const { EventEmitter } = require("events");
20
+ const http = require("http");
21
+ const { WebSocketServer } = require("ws");
18
22
 
19
23
  // Load environment variables
20
24
  require("dotenv").config();
@@ -37,6 +41,148 @@ const logger = winston.createLogger({
37
41
  ],
38
42
  });
39
43
 
44
+ /**
45
+ * Log Buffer Service - Captures and stores stdout/stderr logs in memory
46
+ */
47
+ class LogBufferService extends EventEmitter {
48
+ constructor(options = {}) {
49
+ super();
50
+ this.maxSize = options.maxSize || 10000; // Max 10k log entries
51
+ this.maxAge = options.maxAge || 24 * 60 * 60 * 1000; // 24 hours
52
+ this.logs = []; // Circular buffer
53
+ this.isCapturing = false;
54
+ this.originalStdoutWrite = null;
55
+ this.originalStderrWrite = null;
56
+ }
57
+
58
+ /**
59
+ * Start capturing stdout/stderr from main process
60
+ */
61
+ start() {
62
+ if (this.isCapturing) return;
63
+
64
+ // Capture stdout
65
+ this.originalStdoutWrite = process.stdout.write.bind(process.stdout);
66
+ process.stdout.write = (chunk, encoding, callback) => {
67
+ this.addLog('stdout', chunk.toString());
68
+ return this.originalStdoutWrite(chunk, encoding, callback);
69
+ };
70
+
71
+ // Capture stderr
72
+ this.originalStderrWrite = process.stderr.write.bind(process.stderr);
73
+ process.stderr.write = (chunk, encoding, callback) => {
74
+ this.addLog('stderr', chunk.toString());
75
+ return this.originalStderrWrite(chunk, encoding, callback);
76
+ };
77
+
78
+ this.isCapturing = true;
79
+ logger.info('✅ Log buffer service started');
80
+ }
81
+
82
+ /**
83
+ * Add log entry to buffer
84
+ */
85
+ addLog(stream, message) {
86
+ const now = Date.now();
87
+ const entry = {
88
+ timestamp: now,
89
+ stream, // 'stdout' or 'stderr'
90
+ message: message.trim(),
91
+ };
92
+
93
+ // Add to buffer
94
+ this.logs.push(entry);
95
+
96
+ // Trim to max size (circular buffer)
97
+ if (this.logs.length > this.maxSize) {
98
+ this.logs.shift(); // Remove oldest
99
+ }
100
+
101
+ // Emit for real-time streaming (if needed in future)
102
+ this.emit('log', entry);
103
+
104
+ // Cleanup old logs periodically
105
+ if (this.logs.length % 100 === 0) {
106
+ this.cleanup();
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Remove logs older than maxAge
112
+ */
113
+ cleanup() {
114
+ const now = Date.now();
115
+ const cutoff = now - this.maxAge;
116
+
117
+ // Remove old logs
118
+ while (this.logs.length > 0 && this.logs[0].timestamp < cutoff) {
119
+ this.logs.shift();
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Get logs with filters
125
+ */
126
+ getLogs(options = {}) {
127
+ const {
128
+ startTime = null,
129
+ endTime = null,
130
+ limit = 100,
131
+ offset = 0,
132
+ stream = null, // 'stdout', 'stderr', or null for both
133
+ } = options;
134
+
135
+ let filtered = [...this.logs];
136
+
137
+ // Filter by time range
138
+ if (startTime) {
139
+ filtered = filtered.filter(log => log.timestamp >= startTime);
140
+ }
141
+ if (endTime) {
142
+ filtered = filtered.filter(log => log.timestamp <= endTime);
143
+ }
144
+
145
+ // Filter by stream
146
+ if (stream) {
147
+ filtered = filtered.filter(log => log.stream === stream);
148
+ }
149
+
150
+ // Sort by timestamp (oldest first)
151
+ filtered.sort((a, b) => a.timestamp - b.timestamp);
152
+
153
+ // Pagination
154
+ const total = filtered.length;
155
+ const paginated = filtered.slice(offset, offset + limit);
156
+
157
+ return {
158
+ logs: paginated,
159
+ total,
160
+ limit,
161
+ offset,
162
+ hasMore: offset + limit < total,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Get logs from time range (e.g., "last 10 minutes")
168
+ */
169
+ getLogsByTimeRange(startTime, endTime) {
170
+ return this.getLogs({
171
+ startTime,
172
+ endTime,
173
+ limit: 10000, // Get all in range
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Clear all logs
179
+ */
180
+ clear() {
181
+ this.logs = [];
182
+ this.emit('clear');
183
+ }
184
+ }
185
+
40
186
  class AgentRuntime {
41
187
  constructor() {
42
188
  this.agentName = process.env.AGENT_NAME || "unknown";
@@ -45,13 +191,24 @@ class AgentRuntime {
45
191
  this.llmModel = process.env.LLM_MODEL || "gpt-3.5-turbo";
46
192
  this.agentPrompt =
47
193
  process.env.AGENT_PROMPT || "You are a helpful AI assistant.";
48
-
194
+
49
195
  this.llmClient = null;
50
196
  this.agentCode = null;
51
197
  this.handlers = new Map();
52
198
  this.isRunning = false;
53
199
  this.startTime = new Date();
54
-
200
+
201
+ // CPU usage tracking for metrics endpoint
202
+ // Track cumulative CPU usage since startup
203
+ this.cpuUsageStart = process.cpuUsage();
204
+ this.cpuUsageStartTime = process.hrtime.bigint();
205
+
206
+ // Initialize log buffer service
207
+ this.logBuffer = new LogBufferService({
208
+ maxSize: 10000, // 10k log entries
209
+ maxAge: 24 * 60 * 60 * 1000, // 24 hours
210
+ });
211
+
55
212
  // HTTP server configuration
56
213
  this.httpEnabled = process.env.HTTP_ENABLED === "true";
57
214
  this.httpPort = parseInt(process.env.HTTP_PORT) || 3000;
@@ -66,8 +223,15 @@ class AgentRuntime {
66
223
  this.mainApp.use(express.json({ limit: "10mb" }));
67
224
  this.mainApp.use(express.urlencoded({ extended: true, limit: "10mb" }));
68
225
 
226
+ // HTTP server and WebSocket server (for log streaming)
227
+ this.httpServer = null;
228
+ this.wss = null;
229
+
69
230
  // Setup health endpoints on main app
70
231
  this.setupHealthEndpoints();
232
+
233
+ // Setup log endpoints on main app
234
+ this.setupLogEndpoints();
71
235
  }
72
236
 
73
237
  /**
@@ -77,15 +241,18 @@ class AgentRuntime {
77
241
  try {
78
242
  logger.info(`Initializing agent: ${this.agentName} (${this.agentId})`);
79
243
 
244
+ // Start log buffer service to capture logs
245
+ this.logBuffer.start();
246
+
80
247
  // Load agent code
81
248
  await this.loadAgentCode();
82
-
249
+
83
250
  // Initialize LLM client
84
251
  await this.initializeLLM();
85
-
252
+
86
253
  // Setup agent handlers
87
254
  await this.setupHandlers();
88
-
255
+
89
256
  // Setup HTTP middleware (CORS, rate limiting) if HTTP is enabled
90
257
  // This must be done BEFORE routes are added
91
258
  if (this.httpEnabled) {
@@ -104,12 +271,12 @@ class AgentRuntime {
104
271
 
105
272
  // Start the main server (handles health, prompting, and user routes)
106
273
  this.startMainServer();
107
-
274
+
108
275
  // Mark as running
109
276
  this.isRunning = true;
110
-
277
+
111
278
  logger.info(`Agent ${this.agentName} initialized successfully`);
112
-
279
+
113
280
  // Execute agent main function if it exists
114
281
  if (this.agentCode && typeof this.agentCode.main === "function") {
115
282
  // Create agent context with tools and capabilities
@@ -123,12 +290,12 @@ class AgentRuntime {
123
290
  prompt: this.agentPrompt,
124
291
  },
125
292
  };
126
-
293
+
127
294
  // Execute main function without awaiting to prevent blocking
128
295
  // This allows the agent to run asynchronously while keeping the container alive
129
296
  this.executeAgentMain(agentContext);
130
297
  }
131
-
298
+
132
299
  // Keep the container alive - this is essential for agent runtime
133
300
  this.keepAlive();
134
301
  } catch (error) {
@@ -143,10 +310,10 @@ class AgentRuntime {
143
310
  async executeAgentMain(agentContext) {
144
311
  try {
145
312
  logger.info("Executing agent main function...");
146
-
313
+
147
314
  // Call the main function and handle different return patterns
148
315
  const result = this.agentCode.main(agentContext);
149
-
316
+
150
317
  // If it returns a promise, handle it properly
151
318
  if (result && typeof result.then === "function") {
152
319
  result.catch((error) => {
@@ -161,7 +328,7 @@ class AgentRuntime {
161
328
  });
162
329
  });
163
330
  }
164
-
331
+
165
332
  logger.info("Agent main function started successfully");
166
333
  } catch (error) {
167
334
  logger.error("Failed to execute agent main function:", error);
@@ -181,17 +348,11 @@ class AgentRuntime {
181
348
  */
182
349
  keepAlive() {
183
350
  logger.info("Starting keep-alive mechanism...");
184
-
351
+
185
352
  // Set up a heartbeat interval to keep the container running
186
353
  this.heartbeatInterval = setInterval(() => {
187
354
  if (this.isRunning) {
188
- logger.debug(
189
- `Agent ${this.agentName} heartbeat - uptime: ${Math.floor(
190
- process.uptime()
191
- )}s`
192
- );
193
-
194
- // Trigger heartbeat handlers
355
+ // Trigger heartbeat handlers (user-defined handlers only, no default logging)
195
356
  const heartbeatHandlers = this.handlers.get("heartbeat") || [];
196
357
  heartbeatHandlers.forEach((handler) => {
197
358
  try {
@@ -202,14 +363,14 @@ class AgentRuntime {
202
363
  });
203
364
  }
204
365
  }, 30000); // Heartbeat every 30 seconds
205
-
366
+
206
367
  // Also set up a simple keep-alive mechanism
207
368
  // This ensures the event loop stays active
208
369
  this.keepAliveTimeout = setTimeout(() => {
209
370
  // This timeout will never fire, but keeps the event loop active
210
371
  logger.debug("Keep-alive timeout triggered (this should not happen)");
211
372
  }, 2147483647); // Maximum timeout value
212
-
373
+
213
374
  logger.info("Keep-alive mechanism started - container will stay running");
214
375
  }
215
376
 
@@ -219,7 +380,7 @@ class AgentRuntime {
219
380
  async loadAgentCode() {
220
381
  const codeDir = "/app/agent-code";
221
382
  const mainFile = path.join(codeDir, "index.js");
222
-
383
+
223
384
  if (fs.existsSync(mainFile)) {
224
385
  logger.info("Loading agent code from index.js");
225
386
  this.agentCode = require(mainFile);
@@ -231,7 +392,7 @@ class AgentRuntime {
231
392
  logger.info(
232
393
  "Basic mode agent is ready and will respond to HTTP requests if enabled"
233
394
  );
234
-
395
+
235
396
  // In basic mode, the agent just stays alive and responds to HTTP requests
236
397
  // The keep-alive mechanism will handle keeping the container running
237
398
  return Promise.resolve();
@@ -306,22 +467,21 @@ class AgentRuntime {
306
467
  (error) => logger.error("Agent error:", error),
307
468
  ]);
308
469
 
309
- this.handlers.set("heartbeat", [
310
- () => logger.debug(`Agent ${this.agentName} heartbeat`),
311
- ]);
470
+ // No default heartbeat handler - users can add their own if needed
471
+ this.handlers.set("heartbeat", []);
312
472
 
313
473
  // Load custom handlers from agent code
314
474
  if (this.agentCode && this.agentCode.handlers) {
315
475
  Object.entries(this.agentCode.handlers).forEach(
316
476
  ([event, handlerList]) => {
317
- if (!this.handlers.has(event)) {
318
- this.handlers.set(event, []);
319
- }
320
-
477
+ if (!this.handlers.has(event)) {
478
+ this.handlers.set(event, []);
479
+ }
480
+
321
481
  const handlers = Array.isArray(handlerList)
322
482
  ? handlerList
323
483
  : [handlerList];
324
- this.handlers.get(event).push(...handlers);
484
+ this.handlers.get(event).push(...handlers);
325
485
  }
326
486
  );
327
487
  }
@@ -469,7 +629,7 @@ class AgentRuntime {
469
629
  http: {
470
630
  enabled: this.httpEnabled,
471
631
  port: this.httpEnabled ? this.httpPort : null,
472
- routes: this.httpEnabled && this.mainApp ? this.getRoutesList() : [],
632
+ endpoints: this.mainApp ? this.getRoutesList() : { builtin: [], userDefined: [], all: [] },
473
633
  },
474
634
  handlers: Array.from(this.handlers.keys()),
475
635
  environment: {
@@ -479,6 +639,230 @@ class AgentRuntime {
479
639
  },
480
640
  });
481
641
  });
642
+
643
+ this.mainApp.get("/metrics", async (req, res) => {
644
+ const memUsage = process.memoryUsage();
645
+
646
+ // Take a delta measurement for accurate CPU percentage
647
+ // Measure CPU usage over a period (500ms for better accuracy)
648
+ const measurementStart = process.cpuUsage();
649
+ const measurementStartTime = process.hrtime.bigint(); // High-resolution time in nanoseconds
650
+
651
+ // Wait a period to measure CPU delta (longer period = more accurate)
652
+ await new Promise(resolve => setTimeout(resolve, 500));
653
+
654
+ const measurementEnd = process.cpuUsage(measurementStart);
655
+ const measurementEndTime = process.hrtime.bigint();
656
+
657
+ // Calculate elapsed time in milliseconds (hrtime is in nanoseconds)
658
+ const measurementDeltaNs = Number(measurementEndTime - measurementStartTime);
659
+ const measurementDeltaMs = measurementDeltaNs / 1000000; // Convert nanoseconds to milliseconds
660
+
661
+ // Calculate CPU percentage from delta
662
+ // cpuUsage.user and cpuUsage.system are in microseconds
663
+ const totalCpuTimeMicroseconds = measurementEnd.user + measurementEnd.system;
664
+ const totalCpuTimeMilliseconds = totalCpuTimeMicroseconds / 1000;
665
+
666
+ // CPU percentage = (CPU time used / elapsed time) * 100
667
+ // This gives us the percentage of one CPU core used
668
+ // For multi-core systems, values can exceed 100% if using multiple cores
669
+ const cpuPercent = measurementDeltaMs > 0
670
+ ? (totalCpuTimeMilliseconds / measurementDeltaMs) * 100
671
+ : 0;
672
+
673
+ // Calculate cumulative CPU usage since startup
674
+ const cumulativeCpuUsage = process.cpuUsage(this.cpuUsageStart);
675
+ const cumulativeCpuTimeMicroseconds = cumulativeCpuUsage.user + cumulativeCpuUsage.system;
676
+ const cumulativeCpuTimeMilliseconds = cumulativeCpuTimeMicroseconds / 1000;
677
+ const cumulativeTimeMs = Number(process.hrtime.bigint() - this.cpuUsageStartTime) / 1000000;
678
+ const cumulativeCpuPercent = cumulativeTimeMs > 0
679
+ ? (cumulativeCpuTimeMilliseconds / cumulativeTimeMs) * 100
680
+ : 0;
681
+
682
+ // Get system memory info
683
+ const totalMemory = os.totalmem();
684
+ const freeMemory = os.freemem();
685
+ const usedMemory = totalMemory - freeMemory;
686
+
687
+ res.json({
688
+ timestamp: new Date().toISOString(),
689
+ agent: {
690
+ name: this.agentName,
691
+ id: this.agentId,
692
+ uptime: Date.now() - this.startTime.getTime(),
693
+ },
694
+ cpu: {
695
+ current: {
696
+ user: measurementEnd.user, // microseconds (delta over measurement period)
697
+ system: measurementEnd.system, // microseconds (delta over measurement period)
698
+ total: totalCpuTimeMilliseconds, // milliseconds (delta over measurement period)
699
+ percent: parseFloat(cpuPercent.toFixed(2)), // percentage over measurement period
700
+ measurementPeriod: measurementDeltaMs, // milliseconds
701
+ },
702
+ cumulative: {
703
+ user: cumulativeCpuUsage.user, // microseconds (since startup)
704
+ system: cumulativeCpuUsage.system, // microseconds (since startup)
705
+ total: cumulativeCpuTimeMilliseconds, // milliseconds (since startup)
706
+ percent: parseFloat(cumulativeCpuPercent.toFixed(2)), // average percentage since startup
707
+ uptime: cumulativeTimeMs, // milliseconds since startup
708
+ },
709
+ cores: os.cpus().length,
710
+ model: os.cpus()[0]?.model || "unknown",
711
+ },
712
+ memory: {
713
+ process: {
714
+ rss: memUsage.rss, // Resident Set Size - total memory allocated
715
+ heapTotal: memUsage.heapTotal, // Total heap memory allocated
716
+ heapUsed: memUsage.heapUsed, // Heap memory used
717
+ external: memUsage.external, // Memory used by C++ objects bound to JS objects
718
+ arrayBuffers: memUsage.arrayBuffers, // Memory allocated for ArrayBuffers
719
+ },
720
+ system: {
721
+ total: totalMemory, // Total system memory
722
+ free: freeMemory, // Free system memory
723
+ used: usedMemory, // Used system memory
724
+ percent: ((usedMemory / totalMemory) * 100).toFixed(2), // Percentage used
725
+ },
726
+ // Human-readable formats
727
+ processFormatted: {
728
+ rss: `${(memUsage.rss / 1024 / 1024).toFixed(2)} MB`,
729
+ heapTotal: `${(memUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
730
+ heapUsed: `${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`,
731
+ external: `${(memUsage.external / 1024 / 1024).toFixed(2)} MB`,
732
+ },
733
+ systemFormatted: {
734
+ total: `${(totalMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
735
+ free: `${(freeMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
736
+ used: `${(usedMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
737
+ },
738
+ },
739
+ loadAverage: os.loadavg(), // 1, 5, and 15 minute load averages
740
+ });
741
+ });
742
+ }
743
+
744
+ /**
745
+ * Setup log endpoints on main app
746
+ */
747
+ setupLogEndpoints() {
748
+ /**
749
+ * GET /logs
750
+ * Get historical logs with pagination and time filtering
751
+ *
752
+ * Query params:
753
+ * - startTime: Unix timestamp (ms) - start of time range
754
+ * - endTime: Unix timestamp (ms) - end of time range
755
+ * - limit: Number of logs per page (default: 100)
756
+ * - offset: Pagination offset (default: 0)
757
+ * - stream: 'stdout' | 'stderr' | null (default: both)
758
+ */
759
+ this.mainApp.get("/logs", (req, res) => {
760
+ try {
761
+ const {
762
+ startTime,
763
+ endTime,
764
+ limit = 100,
765
+ offset = 0,
766
+ stream,
767
+ } = req.query;
768
+
769
+ const options = {
770
+ startTime: startTime ? parseInt(startTime) : null,
771
+ endTime: endTime ? parseInt(endTime) : null,
772
+ limit: parseInt(limit),
773
+ offset: parseInt(offset),
774
+ stream: stream || null,
775
+ };
776
+
777
+ const result = this.logBuffer.getLogs(options);
778
+
779
+ res.json({
780
+ success: true,
781
+ data: result,
782
+ });
783
+ } catch (error) {
784
+ logger.error('Error getting logs:', error);
785
+ res.status(500).json({
786
+ success: false,
787
+ error: 'Failed to get logs',
788
+ message: error.message,
789
+ });
790
+ }
791
+ });
792
+
793
+ /**
794
+ * GET /logs/range
795
+ * Get logs from specific time range (convenience endpoint)
796
+ *
797
+ * Query params:
798
+ * - minutesAgo: Number of minutes to look back (default: 10)
799
+ * - stream: 'stdout' | 'stderr' | null
800
+ */
801
+ this.mainApp.get("/logs/range", (req, res) => {
802
+ try {
803
+ const { minutesAgo = 10, stream } = req.query;
804
+ const endTime = Date.now();
805
+ const startTime = endTime - (parseInt(minutesAgo) * 60 * 1000);
806
+
807
+ const result = this.logBuffer.getLogsByTimeRange(startTime, endTime);
808
+
809
+ // Filter by stream if specified
810
+ let logs = result.logs;
811
+ if (stream) {
812
+ logs = logs.filter(log => log.stream === stream);
813
+ }
814
+
815
+ res.json({
816
+ success: true,
817
+ data: {
818
+ logs,
819
+ count: logs.length,
820
+ startTime,
821
+ endTime,
822
+ },
823
+ });
824
+ } catch (error) {
825
+ logger.error('Error getting logs by range:', error);
826
+ res.status(500).json({
827
+ success: false,
828
+ error: 'Failed to get logs',
829
+ message: error.message,
830
+ });
831
+ }
832
+ });
833
+
834
+ /**
835
+ * GET /logs/stats
836
+ * Get log statistics
837
+ */
838
+ this.mainApp.get("/logs/stats", (req, res) => {
839
+ try {
840
+ const allLogs = this.logBuffer.getLogs({ limit: 10000 });
841
+
842
+ const stats = {
843
+ total: allLogs.total,
844
+ oldest: allLogs.logs[0]?.timestamp || null,
845
+ newest: allLogs.logs[allLogs.logs.length - 1]?.timestamp || null,
846
+ stdout: allLogs.logs.filter(l => l.stream === 'stdout').length,
847
+ stderr: allLogs.logs.filter(l => l.stream === 'stderr').length,
848
+ bufferSize: this.logBuffer.logs.length,
849
+ maxSize: this.logBuffer.maxSize,
850
+ maxAge: this.logBuffer.maxAge,
851
+ };
852
+
853
+ res.json({
854
+ success: true,
855
+ data: stats,
856
+ });
857
+ } catch (error) {
858
+ logger.error('Error getting log stats:', error);
859
+ res.status(500).json({
860
+ success: false,
861
+ error: 'Failed to get log stats',
862
+ message: error.message,
863
+ });
864
+ }
865
+ });
482
866
  }
483
867
 
484
868
  /**
@@ -531,14 +915,14 @@ class AgentRuntime {
531
915
  if (!this.agentCode || !this.agentCode.routes) {
532
916
  // Set up default root route only (404 handler will be set up later)
533
917
  this.mainApp.get("/", (req, res) => {
534
- res.json({
535
- message: `🤖 ${this.agentName} HTTP Server`,
536
- agent: this.agentName,
918
+ res.json({
919
+ message: `🤖 ${this.agentName} HTTP Server`,
920
+ agent: this.agentName,
537
921
  version: "1.0.0",
538
- endpoints: this.getRoutesList(),
922
+ endpoints: this.getRoutesList(),
539
923
  timestamp: new Date().toISOString(),
540
- });
541
924
  });
925
+ });
542
926
  return;
543
927
  }
544
928
 
@@ -592,7 +976,7 @@ class AgentRuntime {
592
976
  res.status(404).json({
593
977
  error: "Not Found",
594
978
  message: `Cannot ${req.method} ${req.path}`,
595
- availableRoutes: this.getRoutesList(),
979
+ availableEndpoints: this.getRoutesList(),
596
980
  });
597
981
  });
598
982
 
@@ -610,23 +994,131 @@ class AgentRuntime {
610
994
  }
611
995
 
612
996
  /**
613
- * Get list of registered routes
997
+ * Get list of registered routes, categorized as built-in or user-defined
614
998
  */
615
999
  getRoutesList() {
616
- if (!this.mainApp) return [];
617
-
618
- const routes = [];
1000
+ if (!this.mainApp) return { builtin: [], userDefined: [], all: [] };
1001
+
1002
+ // List of built-in endpoints
1003
+ const builtInPaths = [
1004
+ '/health',
1005
+ '/status',
1006
+ '/metrics',
1007
+ '/logs',
1008
+ '/logs/range',
1009
+ '/logs/stats',
1010
+ '/prompt',
1011
+ '/'
1012
+ ];
1013
+
1014
+ const builtin = [];
1015
+ const userDefined = [];
1016
+
1017
+ // Add WebSocket endpoint manually (it won't appear in router stack)
1018
+ if (this.wss) {
1019
+ builtin.push({
1020
+ path: '/logs/stream',
1021
+ methods: ['WS'],
1022
+ type: 'websocket'
1023
+ });
1024
+ }
1025
+
619
1026
  this.mainApp._router.stack.forEach((middleware) => {
620
1027
  if (middleware.route) {
621
- const methods = Object.keys(middleware.route.methods);
622
- routes.push({
623
- path: middleware.route.path,
624
- methods: methods.map((m) => m.toUpperCase()),
625
- });
1028
+ const path = middleware.route.path;
1029
+ const methods = Object.keys(middleware.route.methods).map((m) => m.toUpperCase());
1030
+ const routeInfo = {
1031
+ path: path,
1032
+ methods: methods,
1033
+ type: 'http'
1034
+ };
1035
+
1036
+ // Check if it's a built-in endpoint
1037
+ if (builtInPaths.includes(path)) {
1038
+ builtin.push(routeInfo);
1039
+ } else {
1040
+ userDefined.push(routeInfo);
1041
+ }
626
1042
  }
627
1043
  });
1044
+
1045
+ return {
1046
+ builtin: builtin,
1047
+ userDefined: userDefined,
1048
+ all: [...builtin, ...userDefined]
1049
+ };
1050
+ }
1051
+
1052
+ /**
1053
+ * Setup WebSocket server for log streaming on /logs/stream
1054
+ */
1055
+ setupLogStreaming() {
1056
+ if (!this.httpServer) {
1057
+ logger.warn('HTTP server not initialized, cannot setup WebSocket');
1058
+ return;
1059
+ }
1060
+
1061
+ this.wss = new WebSocketServer({
1062
+ server: this.httpServer,
1063
+ path: '/logs/stream',
1064
+ });
1065
+
1066
+ this.wss.on('connection', (ws, req) => {
1067
+ logger.info('📡 Log stream WebSocket connected');
628
1068
 
629
- return routes;
1069
+ // Send recent logs immediately (last 100)
1070
+ const recentLogs = this.logBuffer.getLogs({ limit: 100 });
1071
+ if (ws.readyState === ws.OPEN) {
1072
+ ws.send(JSON.stringify({
1073
+ type: 'initial',
1074
+ data: recentLogs.logs,
1075
+ }));
1076
+ }
1077
+
1078
+ // Listen for new logs
1079
+ const onLog = (logEntry) => {
1080
+ if (ws.readyState === ws.OPEN) {
1081
+ ws.send(JSON.stringify({
1082
+ type: 'log',
1083
+ data: logEntry,
1084
+ }));
1085
+ }
1086
+ };
1087
+
1088
+ this.logBuffer.on('log', onLog);
1089
+
1090
+ // Cleanup on disconnect
1091
+ ws.on('close', () => {
1092
+ this.logBuffer.off('log', onLog);
1093
+ logger.info('📡 Log stream WebSocket disconnected');
1094
+ });
1095
+
1096
+ // Handle ping/pong for keepalive
1097
+ ws.on('pong', () => {
1098
+ ws.isAlive = true;
1099
+ });
1100
+
1101
+ // Mark as alive initially
1102
+ ws.isAlive = true;
1103
+ });
1104
+
1105
+ // Keepalive ping every 30 seconds
1106
+ const keepaliveInterval = setInterval(() => {
1107
+ if (!this.wss) {
1108
+ clearInterval(keepaliveInterval);
1109
+ return;
1110
+ }
1111
+
1112
+ this.wss.clients.forEach((ws) => {
1113
+ if (ws.isAlive === false) {
1114
+ return ws.terminate();
1115
+ }
1116
+ ws.isAlive = false;
1117
+ ws.ping();
1118
+ });
1119
+ }, 30000);
1120
+
1121
+ logger.info('✅ Log streaming WebSocket server started on /logs/stream');
630
1122
  }
631
1123
 
632
1124
  /**
@@ -637,10 +1129,20 @@ class AgentRuntime {
637
1129
  const port = this.mainPort;
638
1130
  const host = "0.0.0.0";
639
1131
 
640
- this.mainApp.listen(port, host, () => {
1132
+ // Create HTTP server from Express app (needed for WebSocket support)
1133
+ this.httpServer = http.createServer(this.mainApp);
1134
+
1135
+ // Setup WebSocket for log streaming
1136
+ this.setupLogStreaming();
1137
+
1138
+ // Start the HTTP server
1139
+ this.httpServer.listen(port, host, () => {
641
1140
  logger.info(`🌐 Main HTTP server listening on ${host}:${port}`);
642
1141
  logger.info(`🔗 Health check: GET http://localhost:${port}/health`);
643
1142
  logger.info(`🔗 Status: GET http://localhost:${port}/status`);
1143
+ logger.info(`🔗 Metrics: GET http://localhost:${port}/metrics`);
1144
+ logger.info(`📋 Logs: GET http://localhost:${port}/logs`);
1145
+ logger.info(`📡 Log stream: WSS ws://localhost:${port}/logs/stream`);
644
1146
 
645
1147
  const directPromptingEnabled =
646
1148
  process.env.DIRECT_PROMPTING_ENABLED !== "false";
@@ -859,7 +1361,7 @@ class AgentRuntime {
859
1361
  data: params.data,
860
1362
  timeout: params.timeout || 10000,
861
1363
  });
862
-
1364
+
863
1365
  return {
864
1366
  status: response.status,
865
1367
  headers: response.headers,
@@ -870,11 +1372,11 @@ class AgentRuntime {
870
1372
  throw new Error(`HTTP request failed: ${error.message}`);
871
1373
  }
872
1374
  },
873
-
1375
+
874
1376
  getCurrentTime: (params = {}) => {
875
1377
  const now = new Date();
876
1378
  const format = params.format || "iso";
877
-
1379
+
878
1380
  switch (format) {
879
1381
  case "iso":
880
1382
  return {
@@ -898,14 +1400,14 @@ class AgentRuntime {
898
1400
  };
899
1401
  }
900
1402
  },
901
-
1403
+
902
1404
  analyzeText: (params) => {
903
1405
  const text = params.text;
904
1406
  const words = text.split(/\s+/).filter((word) => word.length > 0);
905
1407
  const sentences = text
906
1408
  .split(/[.!?]+/)
907
1409
  .filter((s) => s.trim().length > 0);
908
-
1410
+
909
1411
  const result = {
910
1412
  length: text.length,
911
1413
  stats: {
@@ -918,7 +1420,7 @@ class AgentRuntime {
918
1420
  : 0,
919
1421
  },
920
1422
  };
921
-
1423
+
922
1424
  if (params.includeSentiment) {
923
1425
  const positiveWords = [
924
1426
  "good",
@@ -934,7 +1436,7 @@ class AgentRuntime {
934
1436
  "horrible",
935
1437
  "disappointing",
936
1438
  ];
937
-
1439
+
938
1440
  const lowerText = text.toLowerCase();
939
1441
  const positiveCount = positiveWords.filter((word) =>
940
1442
  lowerText.includes(word)
@@ -942,7 +1444,7 @@ class AgentRuntime {
942
1444
  const negativeCount = negativeWords.filter((word) =>
943
1445
  lowerText.includes(word)
944
1446
  ).length;
945
-
1447
+
946
1448
  result.sentiment = {
947
1449
  score: positiveCount - negativeCount,
948
1450
  label:
@@ -953,7 +1455,7 @@ class AgentRuntime {
953
1455
  : "neutral",
954
1456
  };
955
1457
  }
956
-
1458
+
957
1459
  return result;
958
1460
  },
959
1461
  };
@@ -964,20 +1466,19 @@ class AgentRuntime {
964
1466
  */
965
1467
  async shutdown() {
966
1468
  logger.info("Shutting down agent...");
967
-
1469
+
968
1470
  this.isRunning = false;
969
-
1471
+
970
1472
  // Clean up keep-alive mechanisms
971
1473
  if (this.heartbeatInterval) {
972
1474
  clearInterval(this.heartbeatInterval);
973
- logger.debug("Heartbeat interval cleared");
974
1475
  }
975
-
1476
+
976
1477
  if (this.keepAliveTimeout) {
977
1478
  clearTimeout(this.keepAliveTimeout);
978
1479
  logger.debug("Keep-alive timeout cleared");
979
1480
  }
980
-
1481
+
981
1482
  // Call shutdown handlers if they exist
982
1483
  const shutdownHandlers = this.handlers.get("shutdown") || [];
983
1484
  for (const handler of shutdownHandlers) {
@@ -987,7 +1488,7 @@ class AgentRuntime {
987
1488
  logger.error("Error in shutdown handler:", error);
988
1489
  }
989
1490
  }
990
-
1491
+
991
1492
  logger.info("Agent shutdown complete");
992
1493
  process.exit(0);
993
1494
  }