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.
- package/README.md +38 -148
- package/docker/entrypoint.js +568 -67
- package/lib/agent.js +3 -29
- package/lib/cli/build.js +1 -1
- package/lib/cli/clean.js +1 -0
- package/lib/cli/init.js +2 -1
- package/lib/cli/production-build.js +5 -1
- package/lib/cli/run.js +6 -1
- package/lib/cli/stop.js +1 -0
- package/lib/docker/manager.js +86 -7
- package/lib/project.js +2 -3
- package/package.json +1 -1
package/docker/entrypoint.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
918
|
+
res.json({
|
|
919
|
+
message: `🤖 ${this.agentName} HTTP Server`,
|
|
920
|
+
agent: this.agentName,
|
|
537
921
|
version: "1.0.0",
|
|
538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|