dank-ai 1.0.0

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.
@@ -0,0 +1,1227 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Dank Agent Container Entrypoint
5
+ *
6
+ * This script runs inside each agent container and handles:
7
+ * - Loading agent code from the drop-off directory
8
+ * - Setting up the LLM client
9
+ * - Managing agent lifecycle
10
+ * - Health checks and monitoring
11
+ */
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+ const express = require("express");
16
+ const winston = require("winston");
17
+ const { v4: uuidv4 } = require("uuid");
18
+
19
+ // Load environment variables
20
+ require("dotenv").config();
21
+
22
+ // Setup logging
23
+ const logger = winston.createLogger({
24
+ level: process.env.LOG_LEVEL || "info",
25
+ format: winston.format.combine(
26
+ winston.format.timestamp(),
27
+ winston.format.errors({ stack: true }),
28
+ winston.format.json()
29
+ ),
30
+ transports: [
31
+ new winston.transports.Console({
32
+ format: winston.format.combine(
33
+ winston.format.colorize(),
34
+ winston.format.simple()
35
+ ),
36
+ }),
37
+ ],
38
+ });
39
+
40
+ class AgentRuntime {
41
+ constructor() {
42
+ this.agentName = process.env.AGENT_NAME || "unknown";
43
+ this.agentId = process.env.AGENT_ID || uuidv4();
44
+ this.llmProvider = process.env.LLM_PROVIDER || "openai";
45
+ this.llmModel = process.env.LLM_MODEL || "gpt-3.5-turbo";
46
+ this.agentPrompt =
47
+ process.env.AGENT_PROMPT || "You are a helpful AI assistant.";
48
+
49
+ this.llmClient = null;
50
+ this.agentCode = null;
51
+ this.handlers = new Map();
52
+ this.isRunning = false;
53
+ this.startTime = new Date();
54
+
55
+ // HTTP server configuration
56
+ this.httpEnabled = process.env.HTTP_ENABLED === "true";
57
+ this.httpPort = parseInt(process.env.HTTP_PORT) || 3000;
58
+ this.httpHost = process.env.HTTP_HOST || "0.0.0.0";
59
+
60
+ // Setup express servers
61
+ this.healthApp = express(); // Health check server (always running)
62
+ this.httpApp = null; // Main HTTP server (optional)
63
+
64
+ this.setupHealthEndpoints();
65
+ }
66
+
67
+ /**
68
+ * Initialize the agent runtime
69
+ */
70
+ async initialize() {
71
+ try {
72
+ logger.info(`Initializing agent: ${this.agentName} (${this.agentId})`);
73
+
74
+ // Load agent code
75
+ await this.loadAgentCode();
76
+
77
+ // Initialize LLM client
78
+ await this.initializeLLM();
79
+
80
+ // Setup agent handlers
81
+ await this.setupHandlers();
82
+
83
+ // Start health check server
84
+ this.startHealthServer();
85
+
86
+ // Start HTTP server if enabled
87
+ if (this.httpEnabled) {
88
+ await this.setupHttpServer();
89
+ this.startHttpServer();
90
+ }
91
+
92
+ // Setup direct prompting server
93
+ await this.setupDirectPromptingServer();
94
+
95
+ // Mark as running
96
+ this.isRunning = true;
97
+
98
+ logger.info(`Agent ${this.agentName} initialized successfully`);
99
+
100
+ // Execute agent main function if it exists
101
+ if (this.agentCode && typeof this.agentCode.main === "function") {
102
+ // Create agent context with tools and capabilities
103
+ const agentContext = {
104
+ llmClient: this.llmClient,
105
+ handlers: this.handlers,
106
+ tools: this.createToolsProxy(),
107
+ config: {
108
+ name: this.agentName,
109
+ id: this.agentId,
110
+ prompt: this.agentPrompt,
111
+ },
112
+ };
113
+
114
+ // Execute main function without awaiting to prevent blocking
115
+ // This allows the agent to run asynchronously while keeping the container alive
116
+ this.executeAgentMain(agentContext);
117
+ }
118
+
119
+ // Keep the container alive - this is essential for agent runtime
120
+ this.keepAlive();
121
+ } catch (error) {
122
+ logger.error("Failed to initialize agent:", error);
123
+ process.exit(1);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Execute agent main function with proper error handling
129
+ */
130
+ async executeAgentMain(agentContext) {
131
+ try {
132
+ logger.info("Executing agent main function...");
133
+
134
+ // Call the main function and handle different return patterns
135
+ const result = this.agentCode.main(agentContext);
136
+
137
+ // If it returns a promise, handle it properly
138
+ if (result && typeof result.then === "function") {
139
+ result.catch((error) => {
140
+ logger.error("Agent main function error:", error);
141
+ // Don't exit the container, just log the error
142
+ this.handlers.get("error")?.forEach((handler) => {
143
+ try {
144
+ handler(error);
145
+ } catch (handlerError) {
146
+ logger.error("Error handler failed:", handlerError);
147
+ }
148
+ });
149
+ });
150
+ }
151
+
152
+ logger.info("Agent main function started successfully");
153
+ } catch (error) {
154
+ logger.error("Failed to execute agent main function:", error);
155
+ // Don't exit the container, just log the error and continue
156
+ this.handlers.get("error")?.forEach((handler) => {
157
+ try {
158
+ handler(error);
159
+ } catch (handlerError) {
160
+ logger.error("Error handler failed:", handlerError);
161
+ }
162
+ });
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Keep the container alive with a heartbeat mechanism
168
+ */
169
+ keepAlive() {
170
+ logger.info("Starting keep-alive mechanism...");
171
+
172
+ // Set up a heartbeat interval to keep the container running
173
+ this.heartbeatInterval = setInterval(() => {
174
+ if (this.isRunning) {
175
+ logger.debug(
176
+ `Agent ${this.agentName} heartbeat - uptime: ${Math.floor(
177
+ process.uptime()
178
+ )}s`
179
+ );
180
+
181
+ // Trigger heartbeat handlers
182
+ const heartbeatHandlers = this.handlers.get("heartbeat") || [];
183
+ heartbeatHandlers.forEach((handler) => {
184
+ try {
185
+ handler();
186
+ } catch (error) {
187
+ logger.error("Heartbeat handler error:", error);
188
+ }
189
+ });
190
+ }
191
+ }, 30000); // Heartbeat every 30 seconds
192
+
193
+ // Also set up a simple keep-alive mechanism
194
+ // This ensures the event loop stays active
195
+ this.keepAliveTimeout = setTimeout(() => {
196
+ // This timeout will never fire, but keeps the event loop active
197
+ logger.debug("Keep-alive timeout triggered (this should not happen)");
198
+ }, 2147483647); // Maximum timeout value
199
+
200
+ logger.info("Keep-alive mechanism started - container will stay running");
201
+ }
202
+
203
+ /**
204
+ * Load agent code from drop-off directory
205
+ */
206
+ async loadAgentCode() {
207
+ const codeDir = "/app/agent-code";
208
+ const mainFile = path.join(codeDir, "index.js");
209
+
210
+ if (fs.existsSync(mainFile)) {
211
+ logger.info("Loading agent code from index.js");
212
+ this.agentCode = require(mainFile);
213
+ } else {
214
+ logger.warn("No agent code found, running in basic mode");
215
+ this.agentCode = {
216
+ main: async (agentContext) => {
217
+ logger.info("Agent running in basic mode - no custom code loaded");
218
+ logger.info(
219
+ "Basic mode agent is ready and will respond to HTTP requests if enabled"
220
+ );
221
+
222
+ // In basic mode, the agent just stays alive and responds to HTTP requests
223
+ // The keep-alive mechanism will handle keeping the container running
224
+ return Promise.resolve();
225
+ },
226
+ };
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Initialize LLM client based on provider
232
+ */
233
+ async initializeLLM() {
234
+ const apiKey = process.env.LLM_API_KEY;
235
+ const baseURL = process.env.LLM_BASE_URL;
236
+
237
+ switch (this.llmProvider) {
238
+ case "openai":
239
+ const { OpenAI } = require("openai");
240
+ this.llmClient = new OpenAI({
241
+ apiKey,
242
+ baseURL,
243
+ });
244
+ break;
245
+
246
+ case "anthropic":
247
+ const { Anthropic } = require("@anthropic-ai/sdk");
248
+ this.llmClient = new Anthropic({
249
+ apiKey,
250
+ });
251
+ break;
252
+
253
+ case "cohere":
254
+ const { CohereClient } = require("cohere-ai");
255
+ this.llmClient = new CohereClient({
256
+ token: apiKey,
257
+ });
258
+ break;
259
+
260
+ case "ollama":
261
+ // Custom implementation for Ollama
262
+ const axios = require("axios");
263
+ this.llmClient = {
264
+ baseURL: baseURL || "http://localhost:11434",
265
+ async chat(messages) {
266
+ const response = await axios.post(`${this.baseURL}/api/chat`, {
267
+ model: process.env.LLM_MODEL,
268
+ messages,
269
+ stream: false,
270
+ });
271
+ return response.data;
272
+ },
273
+ };
274
+ break;
275
+
276
+ default:
277
+ throw new Error(`Unsupported LLM provider: ${this.llmProvider}`);
278
+ }
279
+
280
+ logger.info(
281
+ `LLM client initialized: ${this.llmProvider} (${this.llmModel})`
282
+ );
283
+ }
284
+
285
+ /**
286
+ * Setup event handlers
287
+ */
288
+ async setupHandlers() {
289
+ // Default handlers
290
+ this.handlers.set("output", [(data) => logger.info("Agent output:", data)]);
291
+
292
+ this.handlers.set("error", [
293
+ (error) => logger.error("Agent error:", error),
294
+ ]);
295
+
296
+ this.handlers.set("heartbeat", [
297
+ () => logger.debug(`Agent ${this.agentName} heartbeat`),
298
+ ]);
299
+
300
+ // Load custom handlers from agent code
301
+ if (this.agentCode && this.agentCode.handlers) {
302
+ Object.entries(this.agentCode.handlers).forEach(
303
+ ([event, handlerList]) => {
304
+ if (!this.handlers.has(event)) {
305
+ this.handlers.set(event, []);
306
+ }
307
+
308
+ const handlers = Array.isArray(handlerList)
309
+ ? handlerList
310
+ : [handlerList];
311
+ this.handlers.get(event).push(...handlers);
312
+ }
313
+ );
314
+ }
315
+
316
+ logger.info(
317
+ `Handlers setup complete. Events: ${Array.from(this.handlers.keys()).join(
318
+ ", "
319
+ )}`
320
+ );
321
+ }
322
+
323
+ /**
324
+ * Emit an event to all matching handlers
325
+ * Supports pattern matching for tool events
326
+ */
327
+ emitEvent(eventName, data = null) {
328
+ // Find all matching handlers (exact match and pattern match)
329
+ const matchingHandlers = [];
330
+
331
+ for (const [handlerPattern, handlers] of this.handlers) {
332
+ if (this.matchesEventPattern(eventName, handlerPattern)) {
333
+ matchingHandlers.push(...handlers);
334
+ }
335
+ }
336
+
337
+ // Execute all matching handlers
338
+ matchingHandlers.forEach((handler) => {
339
+ try {
340
+ if (typeof handler === "function") {
341
+ handler(data);
342
+ } else if (handler.handler && typeof handler.handler === "function") {
343
+ handler.handler(data);
344
+ }
345
+ } catch (error) {
346
+ logger.error(`Error in event handler for '${eventName}':`, error);
347
+ }
348
+ });
349
+
350
+ if (matchingHandlers.length > 0) {
351
+ logger.debug(
352
+ `Emitted event '${eventName}' to ${matchingHandlers.length} handlers`
353
+ );
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Emit an event and collect responses from handlers that return modified data
359
+ */
360
+ emitEventWithResponse(eventName, data) {
361
+ // Find all matching handlers (exact match and pattern match)
362
+ const matchingHandlers = [];
363
+
364
+ for (const [handlerPattern, handlers] of this.handlers) {
365
+ if (this.matchesEventPattern(eventName, handlerPattern)) {
366
+ matchingHandlers.push(...handlers);
367
+ }
368
+ }
369
+
370
+ let modifiedData = { ...data };
371
+
372
+ // Execute all matching handlers and collect responses
373
+ matchingHandlers.forEach((handler) => {
374
+ try {
375
+ let result;
376
+ if (typeof handler === "function") {
377
+ result = handler(modifiedData);
378
+ } else if (handler.handler && typeof handler.handler === "function") {
379
+ result = handler.handler(modifiedData);
380
+ }
381
+
382
+ // If handler returns an object, merge it with the current data
383
+ if (result && typeof result === "object" && !Array.isArray(result)) {
384
+ modifiedData = { ...modifiedData, ...result };
385
+ }
386
+ } catch (error) {
387
+ logger.error(`Handler error for event '${eventName}':`, error);
388
+ }
389
+ });
390
+
391
+ if (matchingHandlers.length > 0) {
392
+ logger.debug(
393
+ `Emitted event '${eventName}' to ${matchingHandlers.length} handlers with response capability`
394
+ );
395
+ }
396
+
397
+ return modifiedData;
398
+ }
399
+
400
+ /**
401
+ * Check if an event name matches a handler pattern
402
+ * Supports wildcards and specific patterns
403
+ */
404
+ matchesEventPattern(eventName, pattern) {
405
+ // Exact match
406
+ if (eventName === pattern) {
407
+ return true;
408
+ }
409
+
410
+ // Wildcard patterns
411
+ if (pattern.includes("*")) {
412
+ const regexPattern = pattern.replace(/\*/g, ".*").replace(/:/g, ":");
413
+ const regex = new RegExp(`^${regexPattern}$`);
414
+ return regex.test(eventName);
415
+ }
416
+
417
+ return false;
418
+ }
419
+
420
+ /**
421
+ * Setup health check endpoints
422
+ */
423
+ setupHealthEndpoints() {
424
+ this.healthApp.get("/health", (req, res) => {
425
+ res.json({
426
+ status: this.isRunning ? "healthy" : "starting",
427
+ agent: {
428
+ name: this.agentName,
429
+ id: this.agentId,
430
+ provider: this.llmProvider,
431
+ model: this.llmModel,
432
+ },
433
+ http: {
434
+ enabled: this.httpEnabled,
435
+ port: this.httpEnabled ? this.httpPort : null,
436
+ },
437
+ uptime: Date.now() - this.startTime.getTime(),
438
+ timestamp: new Date().toISOString(),
439
+ });
440
+ });
441
+
442
+ this.healthApp.get("/status", (req, res) => {
443
+ res.json({
444
+ agent: {
445
+ name: this.agentName,
446
+ id: this.agentId,
447
+ status: this.isRunning ? "running" : "starting",
448
+ startTime: this.startTime.toISOString(),
449
+ uptime: Date.now() - this.startTime.getTime(),
450
+ },
451
+ llm: {
452
+ provider: this.llmProvider,
453
+ model: this.llmModel,
454
+ hasClient: !!this.llmClient,
455
+ },
456
+ http: {
457
+ enabled: this.httpEnabled,
458
+ port: this.httpEnabled ? this.httpPort : null,
459
+ routes: this.httpEnabled && this.httpApp ? this.getRoutesList() : [],
460
+ },
461
+ handlers: Array.from(this.handlers.keys()),
462
+ environment: {
463
+ nodeVersion: process.version,
464
+ platform: process.platform,
465
+ memory: process.memoryUsage(),
466
+ },
467
+ });
468
+ });
469
+ }
470
+
471
+ /**
472
+ * Setup HTTP server with Express.js
473
+ */
474
+ async setupHttpServer() {
475
+ if (!this.httpEnabled) return;
476
+
477
+ logger.info(`Setting up HTTP server on port ${this.httpPort}`);
478
+
479
+ this.httpApp = express();
480
+
481
+ // Basic middleware
482
+ this.httpApp.use(express.json({ limit: "10mb" }));
483
+ this.httpApp.use(express.urlencoded({ extended: true, limit: "10mb" }));
484
+
485
+ // CORS if enabled
486
+ if (process.env.HTTP_CORS !== "false") {
487
+ this.httpApp.use((req, res, next) => {
488
+ res.header("Access-Control-Allow-Origin", "*");
489
+ res.header(
490
+ "Access-Control-Allow-Methods",
491
+ "GET, POST, PUT, DELETE, PATCH, OPTIONS"
492
+ );
493
+ res.header(
494
+ "Access-Control-Allow-Headers",
495
+ "Origin, X-Requested-With, Content-Type, Accept, Authorization"
496
+ );
497
+
498
+ if (req.method === "OPTIONS") {
499
+ res.sendStatus(200);
500
+ } else {
501
+ next();
502
+ }
503
+ });
504
+ }
505
+
506
+ // Rate limiting if configured
507
+ if (process.env.HTTP_RATE_LIMIT === "true") {
508
+ const rateLimit = require("express-rate-limit");
509
+ const limiter = rateLimit({
510
+ windowMs:
511
+ parseInt(process.env.HTTP_RATE_LIMIT_WINDOW) || 15 * 60 * 1000,
512
+ max: parseInt(process.env.HTTP_RATE_LIMIT_MAX) || 100,
513
+ message: process.env.HTTP_RATE_LIMIT_MESSAGE || "Too many requests",
514
+ });
515
+ this.httpApp.use(limiter);
516
+ }
517
+
518
+ // Setup routes from agent configuration
519
+ await this.setupAgentRoutes();
520
+
521
+ // Default routes
522
+ this.httpApp.get("/", (req, res) => {
523
+ res.json({
524
+ message: `🤖 ${this.agentName} HTTP Server`,
525
+ agent: this.agentName,
526
+ version: "1.0.0",
527
+ endpoints: this.getRoutesList(),
528
+ timestamp: new Date().toISOString(),
529
+ });
530
+ });
531
+
532
+ // 404 handler
533
+ this.httpApp.use((req, res) => {
534
+ res.status(404).json({
535
+ error: "Not Found",
536
+ message: `Cannot ${req.method} ${req.path}`,
537
+ availableRoutes: this.getRoutesList(),
538
+ });
539
+ });
540
+
541
+ // Error handler
542
+ this.httpApp.use((err, req, res, next) => {
543
+ logger.error("HTTP server error:", err);
544
+ res.status(500).json({
545
+ error: "Internal Server Error",
546
+ message:
547
+ process.env.NODE_ENV === "development"
548
+ ? err.message
549
+ : "Something went wrong",
550
+ });
551
+ });
552
+ }
553
+
554
+ /**
555
+ * Setup routes defined in agent configuration
556
+ */
557
+ async setupAgentRoutes() {
558
+ if (!this.agentCode || !this.agentCode.routes) return;
559
+
560
+ // Setup routes from agent code
561
+ Object.entries(this.agentCode.routes).forEach(([path, handlers]) => {
562
+ if (typeof handlers === "object") {
563
+ Object.entries(handlers).forEach(([method, handler]) => {
564
+ if (typeof handler === "function") {
565
+ const lowerMethod = method.toLowerCase();
566
+ if (this.httpApp[lowerMethod]) {
567
+ // Wrap handler to emit tool events
568
+ const wrappedHandler = this.wrapHttpHandlerWithEvents(
569
+ method,
570
+ path,
571
+ handler
572
+ );
573
+ this.httpApp[lowerMethod](path, wrappedHandler);
574
+ logger.info(`Registered route: ${method.toUpperCase()} ${path}`);
575
+ }
576
+ }
577
+ });
578
+ }
579
+ });
580
+
581
+ // Setup middleware from agent code
582
+ if (this.agentCode.middleware && Array.isArray(this.agentCode.middleware)) {
583
+ this.agentCode.middleware.forEach((middleware) => {
584
+ if (typeof middleware === "function") {
585
+ this.httpApp.use(middleware);
586
+ logger.info("Registered custom middleware");
587
+ }
588
+ });
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Get list of registered routes
594
+ */
595
+ getRoutesList() {
596
+ if (!this.httpApp) return [];
597
+
598
+ const routes = [];
599
+ this.httpApp._router.stack.forEach((middleware) => {
600
+ if (middleware.route) {
601
+ const methods = Object.keys(middleware.route.methods);
602
+ routes.push({
603
+ path: middleware.route.path,
604
+ methods: methods.map((m) => m.toUpperCase()),
605
+ });
606
+ }
607
+ });
608
+
609
+ return routes;
610
+ }
611
+
612
+ /**
613
+ * Start health check server
614
+ */
615
+ startHealthServer() {
616
+ const port = process.env.HEALTH_PORT || 3001;
617
+
618
+ this.healthApp.listen(port, "0.0.0.0", () => {
619
+ logger.info(`Health server listening on port ${port}`);
620
+ });
621
+ }
622
+
623
+ /**
624
+ * Start HTTP server
625
+ */
626
+ startHttpServer() {
627
+ if (!this.httpEnabled || !this.httpApp) return;
628
+
629
+ this.httpApp.listen(this.httpPort, this.httpHost, () => {
630
+ logger.info(
631
+ `🌐 HTTP server listening on ${this.httpHost}:${this.httpPort}`
632
+ );
633
+ logger.info(
634
+ `🔗 Agent HTTP endpoint: http://${this.httpHost}:${this.httpPort}`
635
+ );
636
+ });
637
+ }
638
+
639
+ /**
640
+ * Setup direct prompting server (WebSocket/HTTP)
641
+ */
642
+ async setupDirectPromptingServer() {
643
+ const directPromptingEnabled =
644
+ process.env.DIRECT_PROMPTING_ENABLED !== "false";
645
+ logger.info(`🔍 Direct prompting enabled: ${directPromptingEnabled}`);
646
+ if (!directPromptingEnabled) return;
647
+
648
+ const protocol = process.env.DIRECT_PROMPTING_PROTOCOL || "websocket";
649
+ const port = parseInt(process.env.DOCKER_PORT) || 3000;
650
+
651
+ logger.info(
652
+ `Setting up direct prompting server (${protocol}) on port ${port}`
653
+ );
654
+
655
+ if (protocol === "websocket") {
656
+ logger.info("📡 Setting up WebSocket server...");
657
+ await this.setupWebSocketServer(port);
658
+ } else if (protocol === "http") {
659
+ logger.info("🌐 Setting up HTTP prompting endpoints...");
660
+ await this.setupHttpPromptingEndpoints();
661
+ } else {
662
+ logger.warn(`⚠️ Unknown protocol: ${protocol}`);
663
+ }
664
+
665
+ logger.info("✅ Direct prompting server setup completed");
666
+ }
667
+
668
+ /**
669
+ * Setup WebSocket server for direct prompting
670
+ */
671
+ async setupWebSocketServer(port) {
672
+ const WebSocket = require("ws");
673
+ const maxConnections =
674
+ parseInt(process.env.DIRECT_PROMPTING_MAX_CONNECTIONS) || 100;
675
+ const authentication =
676
+ process.env.DIRECT_PROMPTING_AUTHENTICATION === "true";
677
+
678
+ this.wsServer = new WebSocket.Server({
679
+ port: port,
680
+ host: "0.0.0.0",
681
+ maxClients: maxConnections,
682
+ });
683
+
684
+ this.activeConnections = 0;
685
+
686
+ logger.info(
687
+ `WebSocket server configured with max ${maxConnections} connections, auth: ${authentication}`
688
+ );
689
+
690
+ this.wsServer.on("connection", (ws, req) => {
691
+ const clientId = require("uuid").v4();
692
+ this.activeConnections++;
693
+ logger.info(
694
+ `WebSocket client connected: ${clientId} (${this.activeConnections}/${maxConnections})`
695
+ );
696
+
697
+ ws.on("message", async (message) => {
698
+ try {
699
+ const data = JSON.parse(message.toString());
700
+ const { prompt, conversationId, metadata } = data;
701
+
702
+ if (!prompt) {
703
+ ws.send(
704
+ JSON.stringify({
705
+ error: "Prompt is required",
706
+ timestamp: new Date().toISOString(),
707
+ })
708
+ );
709
+ return;
710
+ }
711
+
712
+ // Process the prompt with LLM
713
+ const response = await this.processDirectPrompt(prompt, {
714
+ conversationId,
715
+ metadata,
716
+ clientId,
717
+ protocol: "websocket",
718
+ });
719
+
720
+ // Send response back
721
+ ws.send(
722
+ JSON.stringify({
723
+ response: response.content,
724
+ conversationId: conversationId || response.conversationId,
725
+ metadata: response.metadata,
726
+ timestamp: new Date().toISOString(),
727
+ })
728
+ );
729
+ } catch (error) {
730
+ logger.error("WebSocket message processing error:", error);
731
+ ws.send(
732
+ JSON.stringify({
733
+ error: "Failed to process prompt",
734
+ message: error.message,
735
+ timestamp: new Date().toISOString(),
736
+ })
737
+ );
738
+ }
739
+ });
740
+
741
+ ws.on("close", () => {
742
+ this.activeConnections--;
743
+ logger.info(
744
+ `WebSocket client disconnected: ${clientId} (${this.activeConnections}/${maxConnections})`
745
+ );
746
+ });
747
+
748
+ ws.on("error", (error) => {
749
+ logger.error(`WebSocket error for client ${clientId}:`, error);
750
+ });
751
+ });
752
+
753
+ logger.info(`🔌 WebSocket server listening on port ${port}`);
754
+ logger.info(`🔗 Direct prompting endpoint: ws://localhost:${port}`);
755
+ }
756
+
757
+ /**
758
+ * Setup HTTP endpoints for direct prompting
759
+ */
760
+ async setupHttpPromptingEndpoints() {
761
+ logger.info("🔧 Setting up HTTP prompting endpoints...");
762
+ const port = parseInt(process.env.DOCKER_PORT) || 3000;
763
+ logger.info(`Port: ${port}, httpApp exists: ${!!this.httpApp}`);
764
+
765
+ if (!this.httpApp) {
766
+ // Create a minimal HTTP app for prompting if main HTTP is disabled
767
+ logger.info("Creating new Express app for HTTP prompting");
768
+ this.httpApp = express();
769
+ this.httpApp.use(express.json({ limit: "10mb" }));
770
+ }
771
+
772
+ // Direct prompting endpoint
773
+ this.httpApp.post("/prompt", async (req, res) => {
774
+ try {
775
+ const { prompt, conversationId, metadata } = req.body;
776
+
777
+ if (!prompt) {
778
+ return res.status(400).json({
779
+ error: "Prompt is required",
780
+ timestamp: new Date().toISOString(),
781
+ });
782
+ }
783
+
784
+ const response = await this.processDirectPrompt(prompt, {
785
+ conversationId,
786
+ metadata,
787
+ protocol: "http",
788
+ clientIp: req.ip,
789
+ });
790
+
791
+ res.json({
792
+ response: response.content,
793
+ conversationId: conversationId || response.conversationId,
794
+ metadata: response.metadata,
795
+ timestamp: new Date().toISOString(),
796
+ });
797
+ } catch (error) {
798
+ logger.error("HTTP prompt processing error:", error);
799
+ res.status(500).json({
800
+ error: "Failed to process prompt",
801
+ message: error.message,
802
+ timestamp: new Date().toISOString(),
803
+ });
804
+ }
805
+ });
806
+
807
+ // Start the HTTP server if it's not already running
808
+ logger.info(
809
+ `HTTP server status: httpEnabled=${this.httpEnabled}, port=${port}`
810
+ );
811
+
812
+ if (!this.httpEnabled) {
813
+ // Only start if main HTTP server is not enabled
814
+ logger.info(`Starting HTTP direct prompting server on port ${port}...`);
815
+
816
+ try {
817
+ const server = this.httpApp.listen(port, "0.0.0.0", () => {
818
+ logger.info(
819
+ `🌐 HTTP direct prompting server listening on port ${port}`
820
+ );
821
+ logger.info(
822
+ `📡 Direct prompting endpoint: POST http://localhost:${port}/prompt`
823
+ );
824
+ });
825
+
826
+ server.on("error", (error) => {
827
+ logger.error(`HTTP server error:`, error);
828
+ });
829
+ } catch (error) {
830
+ logger.error(`Failed to start HTTP server:`, error);
831
+ }
832
+ } else {
833
+ logger.info(`📡 HTTP prompting endpoint added: POST /prompt`);
834
+ }
835
+ }
836
+
837
+ /**
838
+ * Process a direct prompt and emit events
839
+ */
840
+ async processDirectPrompt(prompt, context = {}) {
841
+ const startTime = Date.now();
842
+ const conversationId = context.conversationId || require("uuid").v4();
843
+
844
+ try {
845
+ // Emit request start event and allow handlers to modify the prompt
846
+ const startEventData = {
847
+ prompt,
848
+ conversationId,
849
+ context,
850
+ timestamp: new Date().toISOString(),
851
+ };
852
+
853
+ const modifiedData = this.emitEventWithResponse("request_output:start", startEventData);
854
+
855
+ // Use modified prompt if handlers returned one, otherwise use original
856
+ const finalPrompt = modifiedData?.prompt || prompt;
857
+
858
+ // Process with LLM
859
+ let response;
860
+ if (this.llmProvider === "openai") {
861
+ const completion = await this.llmClient.chat.completions.create({
862
+ model: this.llmModel,
863
+ messages: [
864
+ { role: "system", content: this.agentPrompt },
865
+ { role: "user", content: finalPrompt },
866
+ ],
867
+ temperature: parseFloat(process.env.LLM_TEMPERATURE) || 0.7,
868
+ max_tokens: parseInt(process.env.LLM_MAX_TOKENS) || 1000,
869
+ });
870
+
871
+ response = {
872
+ content: completion.choices[0].message.content,
873
+ usage: completion.usage,
874
+ model: completion.model,
875
+ };
876
+ } else {
877
+ // Fallback for other providers
878
+ response = {
879
+ content: `Echo: ${prompt} (LLM provider ${this.llmProvider} not fully implemented)`,
880
+ usage: { total_tokens: 0 },
881
+ model: this.llmModel,
882
+ };
883
+ }
884
+
885
+ const processingTime = Date.now() - startTime;
886
+
887
+ // Emit request output event
888
+ this.emitEvent("request_output", {
889
+ prompt,
890
+ finalPrompt, // Include the final prompt that was sent to LLM
891
+ response: response.content,
892
+ conversationId,
893
+ context,
894
+ usage: response.usage,
895
+ model: response.model,
896
+ processingTime,
897
+ promptModified: finalPrompt !== prompt, // Indicate if prompt was modified
898
+ timestamp: new Date().toISOString(),
899
+ });
900
+
901
+ // Emit end event and allow handlers to modify the response before returning
902
+ const endEventData = {
903
+ conversationId,
904
+ prompt,
905
+ finalPrompt,
906
+ response: response.content,
907
+ context,
908
+ usage: response.usage,
909
+ model: response.model,
910
+ processingTime,
911
+ promptModified: finalPrompt !== prompt,
912
+ success: true,
913
+ timestamp: new Date().toISOString(),
914
+ };
915
+
916
+ const modifiedEndData = this.emitEventWithResponse("request_output:end", endEventData);
917
+
918
+ // Use the final response (potentially modified by handlers)
919
+ const finalResponse = modifiedEndData.response || response.content;
920
+
921
+ return {
922
+ content: finalResponse,
923
+ conversationId,
924
+ metadata: {
925
+ usage: response.usage,
926
+ model: response.model,
927
+ processingTime,
928
+ responseModified: finalResponse !== response.content, // Indicate if response was modified
929
+ },
930
+ };
931
+ } catch (error) {
932
+ const processingTime = Date.now() - startTime;
933
+
934
+ // Emit error event
935
+ this.emitEvent("request_output:error", {
936
+ prompt,
937
+ finalPrompt,
938
+ conversationId,
939
+ context,
940
+ error: error.message,
941
+ processingTime,
942
+ promptModified: finalPrompt !== prompt,
943
+ success: false,
944
+ timestamp: new Date().toISOString(),
945
+ });
946
+
947
+ throw error;
948
+ }
949
+ }
950
+
951
+ /**
952
+ * Wrap HTTP handler to emit tool events
953
+ */
954
+ wrapHttpHandlerWithEvents(method, path, originalHandler) {
955
+ return async (req, res) => {
956
+ const startTime = Date.now();
957
+ const requestId = require("uuid").v4();
958
+
959
+ try {
960
+ // Emit call event
961
+ this.emitEvent("tool:http-server:call", {
962
+ requestId,
963
+ method: method.toUpperCase(),
964
+ path,
965
+ headers: req.headers,
966
+ body: req.body,
967
+ query: req.query,
968
+ params: req.params,
969
+ timestamp: new Date().toISOString(),
970
+ });
971
+
972
+ // Emit specific method call event
973
+ this.emitEvent(`tool:http-server:call:${method.toLowerCase()}`, {
974
+ requestId,
975
+ path,
976
+ headers: req.headers,
977
+ body: req.body,
978
+ query: req.query,
979
+ params: req.params,
980
+ timestamp: new Date().toISOString(),
981
+ });
982
+
983
+ // Capture response data
984
+ const originalSend = res.send;
985
+ const originalJson = res.json;
986
+ let responseData = null;
987
+ let statusCode = 200;
988
+
989
+ res.send = function (data) {
990
+ responseData = data;
991
+ statusCode = res.statusCode;
992
+ return originalSend.call(this, data);
993
+ };
994
+
995
+ res.json = function (data) {
996
+ responseData = data;
997
+ statusCode = res.statusCode;
998
+ return originalJson.call(this, data);
999
+ };
1000
+
1001
+ // Execute original handler
1002
+ const result = await originalHandler(req, res);
1003
+
1004
+ const processingTime = Date.now() - startTime;
1005
+
1006
+ // Emit response event
1007
+ this.emitEvent("tool:http-server:response", {
1008
+ requestId,
1009
+ method: method.toUpperCase(),
1010
+ path,
1011
+ statusCode,
1012
+ responseData,
1013
+ processingTime,
1014
+ timestamp: new Date().toISOString(),
1015
+ });
1016
+
1017
+ // Emit specific method response event
1018
+ this.emitEvent(`tool:http-server:response:${method.toLowerCase()}`, {
1019
+ requestId,
1020
+ path,
1021
+ statusCode,
1022
+ responseData,
1023
+ processingTime,
1024
+ timestamp: new Date().toISOString(),
1025
+ });
1026
+
1027
+ // Emit wildcard events
1028
+ this.emitEvent("tool:http-server:*", {
1029
+ type: "response",
1030
+ requestId,
1031
+ method: method.toUpperCase(),
1032
+ path,
1033
+ statusCode,
1034
+ responseData,
1035
+ processingTime,
1036
+ timestamp: new Date().toISOString(),
1037
+ });
1038
+
1039
+ return result;
1040
+ } catch (error) {
1041
+ const processingTime = Date.now() - startTime;
1042
+
1043
+ // Emit error event
1044
+ this.emitEvent("tool:http-server:error", {
1045
+ requestId,
1046
+ method: method.toUpperCase(),
1047
+ path,
1048
+ error: error.message,
1049
+ stack: error.stack,
1050
+ processingTime,
1051
+ timestamp: new Date().toISOString(),
1052
+ });
1053
+
1054
+ throw error;
1055
+ }
1056
+ };
1057
+ }
1058
+
1059
+ /**
1060
+ * Create tools proxy for agent runtime
1061
+ */
1062
+ createToolsProxy() {
1063
+ // Basic built-in tools that work in container environment
1064
+ return {
1065
+ httpRequest: async (params) => {
1066
+ const axios = require("axios");
1067
+ try {
1068
+ const response = await axios({
1069
+ url: params.url,
1070
+ method: params.method || "GET",
1071
+ headers: params.headers || {},
1072
+ data: params.data,
1073
+ timeout: params.timeout || 10000,
1074
+ });
1075
+
1076
+ return {
1077
+ status: response.status,
1078
+ headers: response.headers,
1079
+ data: response.data,
1080
+ success: response.status >= 200 && response.status < 300,
1081
+ };
1082
+ } catch (error) {
1083
+ throw new Error(`HTTP request failed: ${error.message}`);
1084
+ }
1085
+ },
1086
+
1087
+ getCurrentTime: (params = {}) => {
1088
+ const now = new Date();
1089
+ const format = params.format || "iso";
1090
+
1091
+ switch (format) {
1092
+ case "iso":
1093
+ return {
1094
+ formatted: now.toISOString(),
1095
+ timestamp: now.toISOString(),
1096
+ };
1097
+ case "unix":
1098
+ return {
1099
+ formatted: Math.floor(now.getTime() / 1000),
1100
+ timestamp: now.toISOString(),
1101
+ };
1102
+ case "readable":
1103
+ return {
1104
+ formatted: now.toLocaleString(),
1105
+ timestamp: now.toISOString(),
1106
+ };
1107
+ default:
1108
+ return {
1109
+ formatted: now.toISOString(),
1110
+ timestamp: now.toISOString(),
1111
+ };
1112
+ }
1113
+ },
1114
+
1115
+ analyzeText: (params) => {
1116
+ const text = params.text;
1117
+ const words = text.split(/\s+/).filter((word) => word.length > 0);
1118
+ const sentences = text
1119
+ .split(/[.!?]+/)
1120
+ .filter((s) => s.trim().length > 0);
1121
+
1122
+ const result = {
1123
+ length: text.length,
1124
+ stats: {
1125
+ characters: text.length,
1126
+ words: words.length,
1127
+ sentences: sentences.length,
1128
+ averageWordsPerSentence:
1129
+ sentences.length > 0
1130
+ ? Math.round((words.length / sentences.length) * 10) / 10
1131
+ : 0,
1132
+ },
1133
+ };
1134
+
1135
+ if (params.includeSentiment) {
1136
+ const positiveWords = [
1137
+ "good",
1138
+ "great",
1139
+ "excellent",
1140
+ "amazing",
1141
+ "wonderful",
1142
+ ];
1143
+ const negativeWords = [
1144
+ "bad",
1145
+ "terrible",
1146
+ "awful",
1147
+ "horrible",
1148
+ "disappointing",
1149
+ ];
1150
+
1151
+ const lowerText = text.toLowerCase();
1152
+ const positiveCount = positiveWords.filter((word) =>
1153
+ lowerText.includes(word)
1154
+ ).length;
1155
+ const negativeCount = negativeWords.filter((word) =>
1156
+ lowerText.includes(word)
1157
+ ).length;
1158
+
1159
+ result.sentiment = {
1160
+ score: positiveCount - negativeCount,
1161
+ label:
1162
+ positiveCount > negativeCount
1163
+ ? "positive"
1164
+ : negativeCount > positiveCount
1165
+ ? "negative"
1166
+ : "neutral",
1167
+ };
1168
+ }
1169
+
1170
+ return result;
1171
+ },
1172
+ };
1173
+ }
1174
+
1175
+ /**
1176
+ * Graceful shutdown
1177
+ */
1178
+ async shutdown() {
1179
+ logger.info("Shutting down agent...");
1180
+
1181
+ this.isRunning = false;
1182
+
1183
+ // Clean up keep-alive mechanisms
1184
+ if (this.heartbeatInterval) {
1185
+ clearInterval(this.heartbeatInterval);
1186
+ logger.debug("Heartbeat interval cleared");
1187
+ }
1188
+
1189
+ if (this.keepAliveTimeout) {
1190
+ clearTimeout(this.keepAliveTimeout);
1191
+ logger.debug("Keep-alive timeout cleared");
1192
+ }
1193
+
1194
+ // Call shutdown handlers if they exist
1195
+ const shutdownHandlers = this.handlers.get("shutdown") || [];
1196
+ for (const handler of shutdownHandlers) {
1197
+ try {
1198
+ await handler();
1199
+ } catch (error) {
1200
+ logger.error("Error in shutdown handler:", error);
1201
+ }
1202
+ }
1203
+
1204
+ logger.info("Agent shutdown complete");
1205
+ process.exit(0);
1206
+ }
1207
+ }
1208
+
1209
+ // Handle shutdown signals
1210
+ process.on("SIGTERM", async () => {
1211
+ if (runtime) {
1212
+ await runtime.shutdown();
1213
+ }
1214
+ });
1215
+
1216
+ process.on("SIGINT", async () => {
1217
+ if (runtime) {
1218
+ await runtime.shutdown();
1219
+ }
1220
+ });
1221
+
1222
+ // Start the agent runtime
1223
+ const runtime = new AgentRuntime();
1224
+ runtime.initialize().catch((error) => {
1225
+ logger.error("Failed to start agent:", error);
1226
+ process.exit(1);
1227
+ });