agent-world 0.11.1 → 0.12.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.
Files changed (267) hide show
  1. package/README.md +17 -7
  2. package/dist/cli/commands.d.ts +109 -0
  3. package/dist/cli/commands.js +2024 -0
  4. package/dist/cli/display.d.ts +124 -0
  5. package/dist/cli/display.js +381 -0
  6. package/dist/cli/hitl.d.ts +33 -0
  7. package/dist/cli/hitl.js +81 -0
  8. package/dist/cli/index.d.ts +2 -0
  9. package/dist/cli/stream.d.ts +41 -0
  10. package/dist/cli/stream.js +222 -0
  11. package/dist/core/activity-tracker.d.ts +16 -0
  12. package/dist/core/activity-tracker.d.ts.map +1 -0
  13. package/dist/core/activity-tracker.js +91 -0
  14. package/dist/core/activity-tracker.js.map +1 -0
  15. package/dist/core/ai-commands.d.ts +16 -0
  16. package/dist/core/ai-commands.d.ts.map +1 -0
  17. package/dist/core/ai-commands.js +24 -0
  18. package/dist/core/ai-commands.js.map +1 -0
  19. package/dist/core/ai-sdk-patch.d.ts +24 -0
  20. package/dist/core/ai-sdk-patch.d.ts.map +1 -0
  21. package/dist/core/ai-sdk-patch.js +169 -0
  22. package/dist/core/ai-sdk-patch.js.map +1 -0
  23. package/dist/core/anthropic-direct.d.ts +52 -0
  24. package/dist/core/anthropic-direct.d.ts.map +1 -0
  25. package/dist/core/anthropic-direct.js +301 -0
  26. package/dist/core/anthropic-direct.js.map +1 -0
  27. package/dist/core/approval-cache.d.ts +104 -0
  28. package/dist/core/approval-cache.d.ts.map +1 -0
  29. package/dist/core/approval-cache.js +150 -0
  30. package/dist/core/approval-cache.js.map +1 -0
  31. package/dist/core/chat-constants.d.ts +20 -0
  32. package/dist/core/chat-constants.d.ts.map +1 -0
  33. package/dist/core/chat-constants.js +22 -0
  34. package/dist/core/chat-constants.js.map +1 -0
  35. package/dist/core/create-agent-tool.d.ts +66 -0
  36. package/dist/core/create-agent-tool.d.ts.map +1 -0
  37. package/dist/core/create-agent-tool.js +212 -0
  38. package/dist/core/create-agent-tool.js.map +1 -0
  39. package/dist/core/events/approval-checker.d.ts +61 -0
  40. package/dist/core/events/approval-checker.d.ts.map +1 -0
  41. package/dist/core/events/approval-checker.js +226 -0
  42. package/dist/core/events/approval-checker.js.map +1 -0
  43. package/dist/core/events/index.d.ts +25 -0
  44. package/dist/core/events/index.d.ts.map +1 -0
  45. package/dist/core/events/index.js +30 -0
  46. package/dist/core/events/index.js.map +1 -0
  47. package/dist/core/events/memory-manager.d.ts +73 -0
  48. package/dist/core/events/memory-manager.d.ts.map +1 -0
  49. package/dist/core/events/memory-manager.js +1218 -0
  50. package/dist/core/events/memory-manager.js.map +1 -0
  51. package/dist/core/events/mention-logic.d.ts +39 -0
  52. package/dist/core/events/mention-logic.d.ts.map +1 -0
  53. package/dist/core/events/mention-logic.js +163 -0
  54. package/dist/core/events/mention-logic.js.map +1 -0
  55. package/dist/core/events/orchestrator.d.ts +69 -0
  56. package/dist/core/events/orchestrator.d.ts.map +1 -0
  57. package/dist/core/events/orchestrator.js +883 -0
  58. package/dist/core/events/orchestrator.js.map +1 -0
  59. package/dist/core/events/persistence.d.ts +41 -0
  60. package/dist/core/events/persistence.d.ts.map +1 -0
  61. package/dist/core/events/persistence.js +296 -0
  62. package/dist/core/events/persistence.js.map +1 -0
  63. package/dist/core/events/publishers.d.ts +81 -0
  64. package/dist/core/events/publishers.d.ts.map +1 -0
  65. package/dist/core/events/publishers.js +272 -0
  66. package/dist/core/events/publishers.js.map +1 -0
  67. package/dist/core/events/subscribers.d.ts +45 -0
  68. package/dist/core/events/subscribers.d.ts.map +1 -0
  69. package/dist/core/events/subscribers.js +288 -0
  70. package/dist/core/events/subscribers.js.map +1 -0
  71. package/dist/core/events/tool-bridge-logging.d.ts +28 -0
  72. package/dist/core/events/tool-bridge-logging.d.ts.map +1 -0
  73. package/dist/core/events/tool-bridge-logging.js +94 -0
  74. package/dist/core/events/tool-bridge-logging.js.map +1 -0
  75. package/dist/core/events-metadata.d.ts +72 -0
  76. package/dist/core/events-metadata.d.ts.map +1 -0
  77. package/dist/core/events-metadata.js +167 -0
  78. package/dist/core/events-metadata.js.map +1 -0
  79. package/dist/core/events.d.ts +186 -0
  80. package/dist/core/events.d.ts.map +1 -0
  81. package/dist/core/events.js +1248 -0
  82. package/dist/core/events.js.map +1 -0
  83. package/dist/core/export.d.ts +106 -0
  84. package/dist/core/export.d.ts.map +1 -0
  85. package/dist/core/export.js +705 -0
  86. package/dist/core/export.js.map +1 -0
  87. package/dist/core/file-tools.d.ts +114 -0
  88. package/dist/core/file-tools.d.ts.map +1 -0
  89. package/dist/core/file-tools.js +370 -0
  90. package/dist/core/file-tools.js.map +1 -0
  91. package/dist/core/google-direct.d.ts +58 -0
  92. package/dist/core/google-direct.d.ts.map +1 -0
  93. package/dist/core/google-direct.js +298 -0
  94. package/dist/core/google-direct.js.map +1 -0
  95. package/dist/core/hitl.d.ts +54 -0
  96. package/dist/core/hitl.d.ts.map +1 -0
  97. package/dist/core/hitl.js +153 -0
  98. package/dist/core/hitl.js.map +1 -0
  99. package/dist/core/index.d.ts +59 -0
  100. package/dist/core/index.d.ts.map +1 -0
  101. package/dist/core/index.js +70 -0
  102. package/dist/core/index.js.map +1 -0
  103. package/dist/core/llm-config.d.ts +128 -0
  104. package/dist/core/llm-config.d.ts.map +1 -0
  105. package/dist/core/llm-config.js +164 -0
  106. package/dist/core/llm-config.js.map +1 -0
  107. package/dist/core/llm-manager.d.ts +163 -0
  108. package/dist/core/llm-manager.d.ts.map +1 -0
  109. package/dist/core/llm-manager.js +669 -0
  110. package/dist/core/llm-manager.js.map +1 -0
  111. package/dist/core/load-skill-tool.d.ts +55 -0
  112. package/dist/core/load-skill-tool.d.ts.map +1 -0
  113. package/dist/core/load-skill-tool.js +468 -0
  114. package/dist/core/load-skill-tool.js.map +1 -0
  115. package/dist/core/logger.d.ts +88 -0
  116. package/dist/core/logger.d.ts.map +1 -0
  117. package/dist/core/logger.js +358 -0
  118. package/dist/core/logger.js.map +1 -0
  119. package/dist/core/managers.d.ts +131 -0
  120. package/dist/core/managers.d.ts.map +1 -0
  121. package/dist/core/managers.js +1223 -0
  122. package/dist/core/managers.js.map +1 -0
  123. package/dist/core/mcp-server-registry.d.ts +304 -0
  124. package/dist/core/mcp-server-registry.d.ts.map +1 -0
  125. package/dist/core/mcp-server-registry.js +1769 -0
  126. package/dist/core/mcp-server-registry.js.map +1 -0
  127. package/dist/core/mcp-tools.d.ts +56 -0
  128. package/dist/core/mcp-tools.d.ts.map +1 -0
  129. package/dist/core/mcp-tools.js +186 -0
  130. package/dist/core/mcp-tools.js.map +1 -0
  131. package/dist/core/message-prep.d.ts +81 -0
  132. package/dist/core/message-prep.d.ts.map +1 -0
  133. package/dist/core/message-prep.js +223 -0
  134. package/dist/core/message-prep.js.map +1 -0
  135. package/dist/core/message-processing-control.d.ts +54 -0
  136. package/dist/core/message-processing-control.d.ts.map +1 -0
  137. package/dist/core/message-processing-control.js +139 -0
  138. package/dist/core/message-processing-control.js.map +1 -0
  139. package/dist/core/openai-direct.d.ts +80 -0
  140. package/dist/core/openai-direct.d.ts.map +1 -0
  141. package/dist/core/openai-direct.js +374 -0
  142. package/dist/core/openai-direct.js.map +1 -0
  143. package/dist/core/shell-cmd-tool.d.ts +235 -0
  144. package/dist/core/shell-cmd-tool.d.ts.map +1 -0
  145. package/dist/core/shell-cmd-tool.js +1157 -0
  146. package/dist/core/shell-cmd-tool.js.map +1 -0
  147. package/dist/core/shell-process-registry.d.ts +88 -0
  148. package/dist/core/shell-process-registry.d.ts.map +1 -0
  149. package/dist/core/shell-process-registry.js +309 -0
  150. package/dist/core/shell-process-registry.js.map +1 -0
  151. package/dist/core/skill-registry.d.ts +75 -0
  152. package/dist/core/skill-registry.d.ts.map +1 -0
  153. package/dist/core/skill-registry.js +369 -0
  154. package/dist/core/skill-registry.js.map +1 -0
  155. package/dist/core/skill-script-runner.d.ts +89 -0
  156. package/dist/core/skill-script-runner.d.ts.map +1 -0
  157. package/dist/core/skill-script-runner.js +274 -0
  158. package/dist/core/skill-script-runner.js.map +1 -0
  159. package/dist/core/skill-selector.d.ts +65 -0
  160. package/dist/core/skill-selector.d.ts.map +1 -0
  161. package/dist/core/skill-selector.js +190 -0
  162. package/dist/core/skill-selector.js.map +1 -0
  163. package/dist/core/skill-settings.d.ts +20 -0
  164. package/dist/core/skill-settings.d.ts.map +1 -0
  165. package/dist/core/skill-settings.js +40 -0
  166. package/dist/core/skill-settings.js.map +1 -0
  167. package/dist/core/storage/agent-storage.d.ts +134 -0
  168. package/dist/core/storage/agent-storage.d.ts.map +1 -0
  169. package/dist/core/storage/agent-storage.js +498 -0
  170. package/dist/core/storage/agent-storage.js.map +1 -0
  171. package/dist/core/storage/eventStorage/fileEventStorage.d.ts +100 -0
  172. package/dist/core/storage/eventStorage/fileEventStorage.d.ts.map +1 -0
  173. package/dist/core/storage/eventStorage/fileEventStorage.js +494 -0
  174. package/dist/core/storage/eventStorage/fileEventStorage.js.map +1 -0
  175. package/dist/core/storage/eventStorage/index.d.ts +31 -0
  176. package/dist/core/storage/eventStorage/index.d.ts.map +1 -0
  177. package/dist/core/storage/eventStorage/index.js +31 -0
  178. package/dist/core/storage/eventStorage/index.js.map +1 -0
  179. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts +87 -0
  180. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts.map +1 -0
  181. package/dist/core/storage/eventStorage/memoryEventStorage.js +244 -0
  182. package/dist/core/storage/eventStorage/memoryEventStorage.js.map +1 -0
  183. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts +45 -0
  184. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts.map +1 -0
  185. package/dist/core/storage/eventStorage/sqliteEventStorage.js +301 -0
  186. package/dist/core/storage/eventStorage/sqliteEventStorage.js.map +1 -0
  187. package/dist/core/storage/eventStorage/types.d.ts +142 -0
  188. package/dist/core/storage/eventStorage/types.d.ts.map +1 -0
  189. package/dist/core/storage/eventStorage/types.js +43 -0
  190. package/dist/core/storage/eventStorage/types.js.map +1 -0
  191. package/dist/core/storage/eventStorage/validation.d.ts +30 -0
  192. package/dist/core/storage/eventStorage/validation.d.ts.map +1 -0
  193. package/dist/core/storage/eventStorage/validation.js +68 -0
  194. package/dist/core/storage/eventStorage/validation.js.map +1 -0
  195. package/dist/core/storage/legacy-migrations.d.ts +45 -0
  196. package/dist/core/storage/legacy-migrations.d.ts.map +1 -0
  197. package/dist/core/storage/legacy-migrations.js +295 -0
  198. package/dist/core/storage/legacy-migrations.js.map +1 -0
  199. package/dist/core/storage/memory-storage.d.ts +105 -0
  200. package/dist/core/storage/memory-storage.d.ts.map +1 -0
  201. package/dist/core/storage/memory-storage.js +415 -0
  202. package/dist/core/storage/memory-storage.js.map +1 -0
  203. package/dist/core/storage/migration-runner.d.ts +96 -0
  204. package/dist/core/storage/migration-runner.d.ts.map +1 -0
  205. package/dist/core/storage/migration-runner.js +306 -0
  206. package/dist/core/storage/migration-runner.js.map +1 -0
  207. package/dist/core/storage/queue-storage.d.ts +147 -0
  208. package/dist/core/storage/queue-storage.d.ts.map +1 -0
  209. package/dist/core/storage/queue-storage.js +290 -0
  210. package/dist/core/storage/queue-storage.js.map +1 -0
  211. package/dist/core/storage/skill-storage.d.ts +136 -0
  212. package/dist/core/storage/skill-storage.d.ts.map +1 -0
  213. package/dist/core/storage/skill-storage.js +474 -0
  214. package/dist/core/storage/skill-storage.js.map +1 -0
  215. package/dist/core/storage/sqlite-schema.d.ts +95 -0
  216. package/dist/core/storage/sqlite-schema.d.ts.map +1 -0
  217. package/dist/core/storage/sqlite-schema.js +156 -0
  218. package/dist/core/storage/sqlite-schema.js.map +1 -0
  219. package/dist/core/storage/sqlite-storage.d.ts +146 -0
  220. package/dist/core/storage/sqlite-storage.d.ts.map +1 -0
  221. package/dist/core/storage/sqlite-storage.js +709 -0
  222. package/dist/core/storage/sqlite-storage.js.map +1 -0
  223. package/dist/core/storage/storage-factory.d.ts +61 -0
  224. package/dist/core/storage/storage-factory.d.ts.map +1 -0
  225. package/dist/core/storage/storage-factory.js +794 -0
  226. package/dist/core/storage/storage-factory.js.map +1 -0
  227. package/dist/core/storage/validation.d.ts +36 -0
  228. package/dist/core/storage/validation.d.ts.map +1 -0
  229. package/dist/core/storage/validation.js +79 -0
  230. package/dist/core/storage/validation.js.map +1 -0
  231. package/dist/core/storage/world-storage.d.ts +114 -0
  232. package/dist/core/storage/world-storage.d.ts.map +1 -0
  233. package/dist/core/storage/world-storage.js +378 -0
  234. package/dist/core/storage/world-storage.js.map +1 -0
  235. package/dist/core/subscription.d.ts +43 -0
  236. package/dist/core/subscription.d.ts.map +1 -0
  237. package/dist/core/subscription.js +227 -0
  238. package/dist/core/subscription.js.map +1 -0
  239. package/dist/core/tool-utils.d.ts +80 -0
  240. package/dist/core/tool-utils.d.ts.map +1 -0
  241. package/dist/core/tool-utils.js +273 -0
  242. package/dist/core/tool-utils.js.map +1 -0
  243. package/dist/core/types.d.ts +595 -0
  244. package/dist/core/types.d.ts.map +1 -0
  245. package/dist/core/types.js +158 -0
  246. package/dist/core/types.js.map +1 -0
  247. package/dist/core/utils.d.ts +138 -0
  248. package/dist/core/utils.d.ts.map +1 -0
  249. package/dist/core/utils.js +478 -0
  250. package/dist/core/utils.js.map +1 -0
  251. package/dist/core/world-class.d.ts +43 -0
  252. package/dist/core/world-class.d.ts.map +1 -0
  253. package/dist/core/world-class.js +90 -0
  254. package/dist/core/world-class.js.map +1 -0
  255. package/dist/index.d.ts +18 -0
  256. package/dist/public/assets/agent-sprites-DJFgj-zP.png +0 -0
  257. package/dist/public/assets/border-KHK37r8y.svg +83 -0
  258. package/dist/public/assets/index-C9kPXL6G.css +1 -0
  259. package/dist/public/assets/index-DOQEHGWt.js +96 -0
  260. package/dist/public/index.html +21 -0
  261. package/dist/server/api.d.ts +2 -0
  262. package/dist/server/api.js +1124 -0
  263. package/dist/server/index.d.ts +29 -0
  264. package/dist/server/sse-handler.d.ts +62 -0
  265. package/dist/server/sse-handler.js +234 -0
  266. package/package.json +15 -3
  267. package/scripts/launch-electron.js +0 -58
@@ -0,0 +1,1124 @@
1
+ /**
2
+ * Agent World API Routes
3
+ *
4
+ * REST API with Zod validation, SSE streaming for chat, and function-based world context.
5
+ * Supports world/agent/chat management with optimized serialization and error handling.
6
+ *
7
+ * Changes:
8
+ * - 2026-02-14: Added HITL option response endpoint `POST /worlds/:worldName/hitl/respond` for web/CLI approval submissions.
9
+ * - 2026-02-13: Added core-managed message edit endpoint `PUT /worlds/:worldName/messages/:messageId`
10
+ * - Delegates edit/remove/resubmit flow to `core.editUserMessage` for cross-client consistency
11
+ * - Streams edit-resubmission follow-up events over SSE by default (`stream: true`)
12
+ * - Keeps DELETE endpoint focused on removal-only behavior
13
+ * - 2026-02-11: Extended non-streaming timeout on tool-stream events to prevent premature timeout during long-running tools
14
+ * - Standardized world-scoped routes to use validateWorld middleware to load and attach worldCtx/world
15
+ * - Removed ad-hoc world loading and undefined getWorldOrError usage; handlers now use (req as any).worldCtx and (req as any).world
16
+ * - Chat endpoints now pass the normalized world id (worldCtx.id) to streaming/non-streaming handlers
17
+ * - Enhanced chat handlers with event-driven completion:
18
+ * - Non-streaming: Listens to world 'idle' event to complete response (with 60s timeout fallback)
19
+ * - Streaming: Ends SSE stream when world becomes 'idle' (with 60s timeout fallback)
20
+ * - Removed complex timer management (adaptive timeouts, agent tracking, tool tracking)
21
+ * - Simpler, more accurate completion based on actual world activity state
22
+ * - Better aligned with CLI event-driven approach
23
+ * - 2025-10-21: Refactored message edit to frontend-driven approach (DELETE removal only)
24
+ * - DELETE endpoint simplified: only accepts { chatId } (no newContent)
25
+ * - Calls removeMessagesFrom() directly (no resubmission)
26
+ * - Returns RemovalResult without resubmission status
27
+ * - Frontend handles resubmission via POST /messages (reuses SSE streaming)
28
+ * - Benefits: RESTful design, simpler server logic, automatic SSE streaming for responses
29
+ * - 2025-10-21: Fixed message event streaming to include messageId for frontend edit feature
30
+ * - Message events now streamed with complete data (sender, content, messageId, createdAt)
31
+ * - Enables frontend to track and edit user messages by server-generated messageId
32
+ * - 2025-10-30: Refactored to use direct world.eventEmitter subscription pattern
33
+ * - Eliminates ClientConnection.onWorldEvent forwarding (same pattern as CLI)
34
+ * - Attaches listeners directly to world.eventEmitter for better performance
35
+ * - Proper listener cleanup in both streaming and non-streaming handlers
36
+ * - 2025-10-30: Refactored to use event-driven completion instead of timers
37
+ * - Non-streaming: Waits for world 'idle' event to complete response
38
+ * - Streaming: Ends stream when world becomes 'idle'
39
+ * - Removed complex timer logic (resetTimer, activeAgents tracking, tool tracking)
40
+ * - Timeout only used as fallback (60s) instead of primary completion mechanism
41
+ * - 2025-11-10: Refactored SSE event handling into reusable sse-handler.ts module
42
+ * - Extracted common SSE logic (headers, listeners, cleanup, timeouts) into createSSEHandler()
43
+ * - /messages endpoint uses shared SSE handler utilities for consistent streaming behavior
44
+ * - Eliminates code duplication and ensures consistent SSE behavior
45
+ * - Simplified streaming handlers by ~150 lines each
46
+ * - 2026-02-08: Removed legacy manual intervention endpoint and related server handling
47
+ */
48
+ import express from 'express';
49
+ import { z } from 'zod';
50
+ import { createSSEHandler } from './sse-handler.js';
51
+ import { createWorld, listWorlds, createCategoryLogger, publishMessage, enableStreaming, disableStreaming,
52
+ // core managers (function-based)
53
+ getWorld, updateWorld, deleteWorld, createAgent, getAgent, updateAgent, deleteAgent, listChats, newChat, restoreChat, deleteChat as deleteChatCore, clearAgentMemory, listAgents as listAgentsCore, getMemory as coreGetMemory, exportWorldToMarkdown, removeMessagesFrom, editUserMessage, stopMessageProcessing, submitWorldOptionResponse, EventType } from '../core/index.js';
54
+ import { subscribeWorld } from '../core/index.js';
55
+ import { listMCPServers, restartMCPServer, getMCPSystemHealth, getMCPRegistryStats } from '../core/mcp-server-registry.js';
56
+ // Function-specific loggers for granular debugging control
57
+ const loggerWorld = createCategoryLogger('api.world');
58
+ const loggerAgent = createCategoryLogger('api.agent');
59
+ const loggerChat = createCategoryLogger('api.chat');
60
+ const loggerStream = createCategoryLogger('api.stream');
61
+ const loggerValidation = createCategoryLogger('api.validation');
62
+ const loggerMcp = createCategoryLogger('api.mcp');
63
+ const loggerExport = createCategoryLogger('api.export');
64
+ const DEFAULT_WORLD_NAME = 'Default World';
65
+ // World context factory - eliminates repetitive worldId passing
66
+ function createWorldContext(worldId) {
67
+ const id = toKebabCase(worldId);
68
+ const worldContext = {
69
+ id,
70
+ load: () => getWorld(id),
71
+ update: (updates) => updateWorld(id, updates),
72
+ delete: () => deleteWorld(id),
73
+ createAgent: (params) => createAgent(id, params),
74
+ getAgent: (agentName) => getAgent(id, toKebabCase(agentName)),
75
+ updateAgent: (agentName, updates) => updateAgent(id, toKebabCase(agentName), updates),
76
+ deleteAgent: (agentName) => deleteAgent(id, toKebabCase(agentName)),
77
+ listAgents: () => listAgentsCore(id),
78
+ clearAgentMemory: (agentName) => clearAgentMemory(id, toKebabCase(agentName)),
79
+ listChats: () => listChats(id),
80
+ newChat: () => newChat(id),
81
+ setChat: (chatId) => restoreChat(id, chatId),
82
+ deleteChat: (chatId) => deleteChatCore(id, chatId),
83
+ };
84
+ return worldContext;
85
+ }
86
+ // Serialization functions
87
+ function serializeWorld(world) {
88
+ return {
89
+ id: world.id,
90
+ name: world.name,
91
+ description: world.description,
92
+ turnLimit: world.turnLimit,
93
+ mainAgent: world.mainAgent || null,
94
+ chatLLMProvider: world.chatLLMProvider,
95
+ chatLLMModel: world.chatLLMModel,
96
+ currentChatId: world.currentChatId || null,
97
+ mcpConfig: world.mcpConfig || null,
98
+ variables: typeof world.variables === 'string' ? world.variables : '',
99
+ agents: Array.from(world.agents.values()).map(serializeAgent),
100
+ chats: Array.from(world.chats.values()).map(serializeChat)
101
+ };
102
+ }
103
+ function serializeAgent(agent) {
104
+ return {
105
+ id: agent.id,
106
+ name: agent.name,
107
+ autoReply: agent.autoReply !== false,
108
+ provider: agent.provider,
109
+ model: agent.model,
110
+ temperature: agent.temperature,
111
+ maxTokens: agent.maxTokens,
112
+ systemPrompt: agent.systemPrompt,
113
+ llmCallCount: agent.llmCallCount,
114
+ memory: agent.memory || [],
115
+ messageCount: agent.memory?.length || 0
116
+ };
117
+ }
118
+ function serializeChat(chat) {
119
+ return {
120
+ id: chat.id,
121
+ name: chat.name,
122
+ messageCount: chat.messageCount
123
+ };
124
+ }
125
+ // Utility functions
126
+ function sendError(res, status, message, code, details) {
127
+ const error = { error: message };
128
+ if (code)
129
+ error.code = code;
130
+ if (details)
131
+ error.details = details;
132
+ res.status(status).json(error);
133
+ }
134
+ function toKebabCase(name) {
135
+ return name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
136
+ }
137
+ async function isAgentNameUnique(worldCtx, agentName, excludeAgent) {
138
+ const normalizedAgentName = toKebabCase(agentName);
139
+ const normalizedExcludeAgent = excludeAgent ? toKebabCase(excludeAgent) : undefined;
140
+ if (normalizedExcludeAgent && normalizedAgentName === normalizedExcludeAgent)
141
+ return true;
142
+ const existingAgent = await worldCtx.getAgent(normalizedAgentName);
143
+ return !existingAgent;
144
+ }
145
+ async function chatExists(worldCtx, chatId) {
146
+ const chats = await worldCtx.listChats();
147
+ return chats.some(chat => chat.id === chatId);
148
+ }
149
+ // Validation middleware for world existence
150
+ function validateWorld(req, res, next) {
151
+ const worldName = req.params.worldName;
152
+ const worldCtx = createWorldContext(worldName);
153
+ worldCtx.load().then(world => {
154
+ if (!world) {
155
+ sendError(res, 404, 'World not found', 'WORLD_NOT_FOUND');
156
+ return;
157
+ }
158
+ // Attach worldCtx to request for downstream handlers
159
+ req.worldCtx = worldCtx;
160
+ req.world = world;
161
+ next();
162
+ }).catch(error => {
163
+ sendError(res, 500, 'Failed to validate world', 'WORLD_VALIDATE_ERROR', error);
164
+ });
165
+ }
166
+ // Validation schemas
167
+ const WorldCreateSchema = z.object({
168
+ name: z.string().min(1).max(100),
169
+ description: z.string().nullable().optional(),
170
+ turnLimit: z.number().min(1).optional(),
171
+ mainAgent: z.string().nullable().optional(),
172
+ chatLLMProvider: z.enum(['openai', 'anthropic', 'azure', 'google', 'xai', 'openai-compatible', 'ollama']).nullable().optional(),
173
+ chatLLMModel: z.string().nullable().optional(),
174
+ mcpConfig: z.string().nullable().optional(),
175
+ variables: z.string().nullable().optional()
176
+ });
177
+ const WorldUpdateSchema = z.object({
178
+ name: z.string().min(1).max(100).optional(),
179
+ description: z.string().nullable().optional(),
180
+ turnLimit: z.number().min(1).optional(),
181
+ mainAgent: z.string().nullable().optional(),
182
+ chatLLMProvider: z.enum(['openai', 'anthropic', 'azure', 'google', 'xai', 'openai-compatible', 'ollama']).nullable().optional(),
183
+ chatLLMModel: z.string().nullable().optional(),
184
+ mcpConfig: z.string().nullable().optional(),
185
+ variables: z.string().nullable().optional()
186
+ });
187
+ const AgentCreateSchema = z.object({
188
+ name: z.string().min(1).max(100),
189
+ type: z.string().optional().default('default'),
190
+ autoReply: z.boolean().optional().default(true),
191
+ provider: z.enum(['openai', 'anthropic', 'azure', 'google', 'xai', 'openai-compatible', 'ollama']).default('openai'),
192
+ model: z.string().default('gpt-4'),
193
+ systemPrompt: z.string().optional(),
194
+ apiKey: z.string().optional(),
195
+ baseUrl: z.string().optional(),
196
+ temperature: z.number().min(0).max(1).optional(),
197
+ maxTokens: z.number().min(1).optional()
198
+ });
199
+ const ChatMessageSchema = z.object({
200
+ message: z.string().min(1),
201
+ sender: z.string().default("human"),
202
+ stream: z.boolean().optional().default(true),
203
+ chatId: z.string().min(1).optional(),
204
+ messages: z.array(z.any()).optional()
205
+ });
206
+ const MessageEditSchema = z.object({
207
+ chatId: z.string().min(1),
208
+ newContent: z.string().min(1),
209
+ stream: z.boolean().optional().default(true)
210
+ });
211
+ const StopMessageProcessingSchema = z.object({
212
+ chatId: z.string().min(1)
213
+ });
214
+ const HitlResponseSchema = z.object({
215
+ requestId: z.string().min(1),
216
+ optionId: z.string().min(1),
217
+ chatId: z.string().nullable().optional()
218
+ });
219
+ const AgentUpdateSchema = z.object({
220
+ name: z.string().min(1).max(100).optional(),
221
+ type: z.string().optional(),
222
+ autoReply: z.boolean().optional(),
223
+ status: z.enum(["active", "inactive", "error"]).optional(),
224
+ provider: z.enum(['openai', 'anthropic', 'azure', 'google', 'xai', 'openai-compatible', 'ollama']).optional(),
225
+ model: z.string().optional(),
226
+ systemPrompt: z.string().optional(),
227
+ temperature: z.number().min(0).max(1).optional(),
228
+ maxTokens: z.number().min(1).optional(),
229
+ clearMemory: z.boolean().optional()
230
+ });
231
+ const router = express.Router();
232
+ // World Routes
233
+ router.get('/worlds', async (req, res) => {
234
+ try {
235
+ const worlds = await listWorlds();
236
+ if (!worlds?.length) {
237
+ const world = await createWorld({ name: DEFAULT_WORLD_NAME });
238
+ if (world) {
239
+ res.json([{ name: world.name, agentCount: 0 }]);
240
+ }
241
+ else {
242
+ sendError(res, 500, 'Failed to create world', 'WORLD_CREATE_ERROR');
243
+ }
244
+ }
245
+ else {
246
+ res.json(worlds.map(world => ({
247
+ name: world.name,
248
+ agentCount: world.totalAgents || 0,
249
+ id: world.id,
250
+ description: world.description
251
+ })));
252
+ }
253
+ }
254
+ catch (error) {
255
+ loggerWorld.error('Error listing worlds', { error: error instanceof Error ? error.message : error });
256
+ sendError(res, 500, 'Failed to list worlds', 'WORLD_LIST_ERROR');
257
+ }
258
+ });
259
+ router.get('/worlds/:worldName', validateWorld, async (req, res) => {
260
+ try {
261
+ // const worldCtx = (req as any).worldCtx as ReturnType<typeof createWorldContext>;
262
+ const world = req.world;
263
+ res.json(serializeWorld(world));
264
+ }
265
+ catch (error) {
266
+ loggerWorld.error('Error getting world', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName });
267
+ sendError(res, 500, 'Internal server error', 'INTERNAL_ERROR');
268
+ }
269
+ });
270
+ router.post('/worlds', async (req, res) => {
271
+ try {
272
+ const validation = WorldCreateSchema.safeParse(req.body);
273
+ if (!validation.success) {
274
+ sendError(res, 400, 'Invalid request body', 'VALIDATION_ERROR', validation.error.issues);
275
+ return;
276
+ }
277
+ const { name, description, turnLimit, mainAgent, chatLLMProvider, chatLLMModel, mcpConfig, variables } = validation.data;
278
+ const worldId = toKebabCase(name);
279
+ const world = await createWorld({
280
+ name,
281
+ description,
282
+ turnLimit,
283
+ mainAgent: mainAgent || null,
284
+ chatLLMProvider: (chatLLMProvider || undefined),
285
+ chatLLMModel: chatLLMModel || undefined,
286
+ mcpConfig: mcpConfig || null,
287
+ variables: variables || undefined
288
+ });
289
+ if (world) {
290
+ res.status(201).json({ name: world.name, id: worldId });
291
+ }
292
+ else {
293
+ sendError(res, 500, 'Failed to create world', 'WORLD_CREATE_ERROR');
294
+ }
295
+ }
296
+ catch (error) {
297
+ const errorMessage = error instanceof Error ? error.message : String(error);
298
+ if (errorMessage.includes('already exists')) {
299
+ sendError(res, 409, 'World with this name already exists', 'WORLD_EXISTS');
300
+ return;
301
+ }
302
+ loggerWorld.error('Error creating world', { error: errorMessage });
303
+ sendError(res, 500, 'Failed to create world', 'WORLD_CREATE_ERROR');
304
+ }
305
+ });
306
+ router.patch('/worlds/:worldName', validateWorld, async (req, res) => {
307
+ try {
308
+ const validation = WorldUpdateSchema.safeParse(req.body);
309
+ if (!validation.success) {
310
+ sendError(res, 400, 'Invalid request body', 'VALIDATION_ERROR', validation.error.issues);
311
+ return;
312
+ }
313
+ const worldCtx = req.worldCtx;
314
+ const currentWorld = req.world;
315
+ const { name, description, turnLimit, mainAgent, chatLLMProvider, chatLLMModel, mcpConfig, variables } = validation.data;
316
+ const updates = {};
317
+ if (name !== undefined)
318
+ updates.name = name;
319
+ if (description !== undefined)
320
+ updates.description = description;
321
+ if (turnLimit !== undefined)
322
+ updates.turnLimit = turnLimit;
323
+ if (mainAgent !== undefined)
324
+ updates.mainAgent = mainAgent;
325
+ if (chatLLMProvider !== undefined && chatLLMProvider !== null)
326
+ updates.chatLLMProvider = chatLLMProvider;
327
+ if (chatLLMModel !== undefined && chatLLMModel !== null)
328
+ updates.chatLLMModel = chatLLMModel;
329
+ if (mcpConfig !== undefined)
330
+ updates.mcpConfig = mcpConfig;
331
+ if (variables !== undefined)
332
+ updates.variables = variables;
333
+ let updatedWorld = currentWorld;
334
+ if (Object.keys(updates).length > 0) {
335
+ const updateResult = await worldCtx.update(updates);
336
+ if (!updateResult) {
337
+ sendError(res, 500, 'Failed to update world', 'WORLD_UPDATE_ERROR');
338
+ return;
339
+ }
340
+ updatedWorld = updateResult;
341
+ }
342
+ res.json(serializeWorld(updatedWorld));
343
+ }
344
+ catch (error) {
345
+ loggerWorld.error('Error updating world', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName });
346
+ sendError(res, 500, 'Failed to update world', 'WORLD_UPDATE_ERROR');
347
+ }
348
+ });
349
+ router.delete('/worlds/:worldName', validateWorld, async (req, res) => {
350
+ try {
351
+ const worldCtx = req.worldCtx;
352
+ if (!worldCtx) {
353
+ // Fallback if middleware not used (should not happen once standardized)
354
+ sendError(res, 404, 'World not found', 'WORLD_NOT_FOUND');
355
+ return;
356
+ }
357
+ const deleted = await worldCtx.delete();
358
+ if (!deleted) {
359
+ sendError(res, 500, 'Failed to delete world', 'WORLD_DELETE_ERROR');
360
+ return;
361
+ }
362
+ res.status(204).send();
363
+ }
364
+ catch (error) {
365
+ loggerWorld.error('Error deleting world', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName });
366
+ sendError(res, 500, 'Failed to delete world', 'WORLD_DELETE_ERROR');
367
+ }
368
+ });
369
+ // Agent Routes
370
+ router.post('/worlds/:worldName/agents', validateWorld, async (req, res) => {
371
+ try {
372
+ const validation = AgentCreateSchema.safeParse(req.body);
373
+ if (!validation.success) {
374
+ sendError(res, 400, 'Invalid request body', 'VALIDATION_ERROR', validation.error.issues);
375
+ return;
376
+ }
377
+ const agentData = validation.data;
378
+ const worldCtx = req.worldCtx;
379
+ const isUnique = await isAgentNameUnique(worldCtx, agentData.name);
380
+ if (!isUnique) {
381
+ sendError(res, 409, 'Agent with this name already exists', 'AGENT_EXISTS');
382
+ return;
383
+ }
384
+ const createdAgent = await worldCtx.createAgent(agentData);
385
+ if (!createdAgent) {
386
+ sendError(res, 500, 'Failed to create agent', 'AGENT_CREATE_ERROR');
387
+ return;
388
+ }
389
+ res.status(201).json(serializeAgent(createdAgent));
390
+ }
391
+ catch (error) {
392
+ loggerAgent.error('Error creating agent', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName });
393
+ sendError(res, 500, 'Failed to create agent', 'AGENT_CREATE_ERROR');
394
+ }
395
+ });
396
+ router.get('/worlds/:worldName/agents', validateWorld, async (req, res) => {
397
+ try {
398
+ const worldCtx = req.worldCtx;
399
+ const world = await worldCtx.load();
400
+ if (!world) {
401
+ sendError(res, 404, 'World not found', 'WORLD_NOT_FOUND');
402
+ return;
403
+ }
404
+ res.json(Array.from(world.agents.values()).map(serializeAgent));
405
+ }
406
+ catch (error) {
407
+ loggerAgent.error('Error listing agents', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName });
408
+ sendError(res, 500, 'Failed to list agents', 'AGENT_LIST_ERROR');
409
+ }
410
+ });
411
+ router.get('/worlds/:worldName/agents/:agentName', validateWorld, async (req, res) => {
412
+ try {
413
+ const { agentName } = req.params;
414
+ const worldCtx = req.worldCtx;
415
+ const agent = await worldCtx.getAgent(agentName);
416
+ if (!agent) {
417
+ sendError(res, 404, 'Agent not found', 'AGENT_NOT_FOUND');
418
+ return;
419
+ }
420
+ res.json(serializeAgent(agent));
421
+ }
422
+ catch (error) {
423
+ loggerAgent.error('Error getting agent', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName, agentName: req.params.agentName });
424
+ sendError(res, 500, 'Failed to get agent', 'AGENT_GET_ERROR');
425
+ }
426
+ });
427
+ router.get('/worlds/:worldName/export', validateWorld, async (req, res) => {
428
+ try {
429
+ const { worldName } = req.params;
430
+ const worldCtx = req.worldCtx;
431
+ const markdown = await exportWorldToMarkdown(worldCtx.id);
432
+ const now = new Date();
433
+ const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, -5);
434
+ const filename = `${worldName}-${timestamp}.md`;
435
+ res.setHeader('Content-Type', 'text/markdown');
436
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
437
+ res.setHeader('Content-Length', Buffer.byteLength(markdown, 'utf8'));
438
+ res.send(markdown);
439
+ }
440
+ catch (error) {
441
+ loggerExport.error('Error exporting world', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName });
442
+ sendError(res, 500, 'Failed to export world', 'WORLD_EXPORT_ERROR');
443
+ }
444
+ });
445
+ router.patch('/worlds/:worldName/agents/:agentName', validateWorld, async (req, res) => {
446
+ try {
447
+ const { agentName } = req.params;
448
+ const validation = AgentUpdateSchema.safeParse(req.body);
449
+ if (!validation.success) {
450
+ sendError(res, 400, 'Invalid request body', 'VALIDATION_ERROR', validation.error.issues);
451
+ return;
452
+ }
453
+ const { clearMemory } = validation.data;
454
+ const worldCtx = req.worldCtx;
455
+ const normalizedAgentName = toKebabCase(agentName);
456
+ const existingAgent = await worldCtx.getAgent(normalizedAgentName);
457
+ if (!existingAgent) {
458
+ sendError(res, 404, 'Agent not found', 'AGENT_NOT_FOUND');
459
+ return;
460
+ }
461
+ let updatedAgent = existingAgent;
462
+ if (clearMemory) {
463
+ const cleared = await worldCtx.clearAgentMemory(normalizedAgentName);
464
+ if (!cleared) {
465
+ sendError(res, 500, 'Failed to clear agent memory', 'MEMORY_CLEAR_ERROR');
466
+ return;
467
+ }
468
+ const refreshedAgent = await worldCtx.getAgent(normalizedAgentName);
469
+ if (refreshedAgent) {
470
+ updatedAgent = refreshedAgent;
471
+ }
472
+ }
473
+ const updates = { ...validation.data };
474
+ delete updates.clearMemory;
475
+ if ('memory' in updates)
476
+ delete updates.memory;
477
+ const updateKeys = Object.keys(updates).filter(k => k !== 'memory');
478
+ if (updateKeys.length > 0) {
479
+ const updateResult = await worldCtx.updateAgent(normalizedAgentName, updates);
480
+ if (!updateResult) {
481
+ sendError(res, 500, 'Failed to update agent', 'AGENT_UPDATE_ERROR');
482
+ return;
483
+ }
484
+ updatedAgent = updateResult;
485
+ }
486
+ res.json(serializeAgent(updatedAgent));
487
+ }
488
+ catch (error) {
489
+ loggerAgent.error('Error updating agent', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName, agentName: req.params.agentName });
490
+ sendError(res, 500, 'Failed to update agent', 'AGENT_UPDATE_ERROR');
491
+ }
492
+ });
493
+ router.delete('/worlds/:worldName/agents/:agentName', validateWorld, async (req, res) => {
494
+ try {
495
+ const { agentName } = req.params;
496
+ const worldCtx = req.worldCtx;
497
+ const normalizedAgentName = toKebabCase(agentName);
498
+ const existingAgent = await worldCtx.getAgent(normalizedAgentName);
499
+ if (!existingAgent) {
500
+ sendError(res, 404, 'Agent not found', 'AGENT_NOT_FOUND');
501
+ return;
502
+ }
503
+ const deleted = await worldCtx.deleteAgent(normalizedAgentName);
504
+ if (!deleted) {
505
+ sendError(res, 500, 'Failed to delete agent', 'AGENT_DELETE_ERROR');
506
+ return;
507
+ }
508
+ res.status(204).send();
509
+ }
510
+ catch (error) {
511
+ loggerAgent.error('Error deleting agent', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName, agentName: req.params.agentName });
512
+ sendError(res, 500, 'Failed to delete agent', 'AGENT_DELETE_ERROR');
513
+ }
514
+ });
515
+ router.delete('/worlds/:worldName/agents/:agentName/memory', validateWorld, async (req, res) => {
516
+ try {
517
+ const { agentName } = req.params;
518
+ const worldCtx = req.worldCtx;
519
+ const normalizedAgentName = toKebabCase(agentName);
520
+ const agent = await worldCtx.getAgent(normalizedAgentName);
521
+ if (!agent) {
522
+ sendError(res, 404, 'Agent not found', 'AGENT_NOT_FOUND');
523
+ return;
524
+ }
525
+ const clearedAgent = await worldCtx.clearAgentMemory(normalizedAgentName);
526
+ if (!clearedAgent) {
527
+ sendError(res, 500, 'Failed to clear agent memory', 'MEMORY_CLEAR_ERROR');
528
+ return;
529
+ }
530
+ res.status(204).send();
531
+ }
532
+ catch (error) {
533
+ loggerAgent.error('Error clearing agent memory', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName, agentName: req.params.agentName });
534
+ sendError(res, 500, 'Failed to clear agent memory', 'MEMORY_CLEAR_ERROR');
535
+ }
536
+ });
537
+ // Chat Helper Functions
538
+ /**
539
+ * Handles non-streaming chat requests by subscribing to world events and collecting all messages.
540
+ * Disables streaming, subscribes to world events, publishes the message, and waits for world idle
541
+ * event (with timeout fallback) before returning the aggregated response.
542
+ *
543
+ * @param res - Express response object
544
+ * @param worldName - Name of the world to send message to
545
+ * @param message - The message to send
546
+ * @param sender - Agent name sending the message
547
+ * @returns Promise that resolves when chat is complete
548
+ */
549
+ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
550
+ disableStreaming();
551
+ let subscription = null;
552
+ let listeners = new Map();
553
+ try {
554
+ let responseContent = '';
555
+ let isComplete = false;
556
+ let hasError = false;
557
+ let errorMessage = '';
558
+ let awaitingIdle = false;
559
+ const responsePromise = new Promise((resolve, reject) => {
560
+ let timeoutTimer = setTimeout(() => {
561
+ if (!isComplete) {
562
+ hasError = true;
563
+ errorMessage = 'Request timeout - no response received within 60 seconds';
564
+ loggerChat.debug('Non-streaming timeout', { awaitingIdle, hasError });
565
+ reject(new Error(errorMessage));
566
+ }
567
+ }, 60000); // Longer timeout as fallback since we rely on events
568
+ // Helper to reset the fallback timeout (called when tool-stream data arrives)
569
+ const resetTimeout = () => {
570
+ clearTimeout(timeoutTimer);
571
+ timeoutTimer = setTimeout(() => {
572
+ if (!isComplete) {
573
+ hasError = true;
574
+ errorMessage = 'Request timeout - no response received within 60 seconds';
575
+ loggerChat.debug('Non-streaming timeout', { awaitingIdle, hasError });
576
+ reject(new Error(errorMessage));
577
+ }
578
+ }, 60000);
579
+ };
580
+ // Subscribe with minimal client (no forwarding callbacks)
581
+ subscribeWorld(worldName, { isOpen: true }).then(sub => {
582
+ if (!sub) {
583
+ hasError = true;
584
+ errorMessage = 'Failed to subscribe to world';
585
+ reject(new Error(errorMessage));
586
+ return;
587
+ }
588
+ subscription = sub;
589
+ const world = subscription.world;
590
+ // Listen to world activity events to detect when all processing is complete
591
+ const worldActivityListener = (eventData) => {
592
+ if (eventData.type === 'response-start') {
593
+ awaitingIdle = true;
594
+ loggerChat.debug('Non-streaming: world processing started', {
595
+ activityId: eventData.activityId,
596
+ source: eventData.source
597
+ });
598
+ }
599
+ else if (eventData.type === 'idle' && awaitingIdle) {
600
+ loggerChat.debug('Non-streaming: world idle, completing response', {
601
+ activityId: eventData.activityId
602
+ });
603
+ clearTimeout(timeoutTimer);
604
+ isComplete = true;
605
+ resolve();
606
+ }
607
+ };
608
+ // Collect message events for response
609
+ const messageListener = (eventData) => {
610
+ responseContent = JSON.stringify({ type: 'message', data: eventData });
611
+ };
612
+ // Listen to activity events for completion detection
613
+ world.eventEmitter.on(EventType.WORLD, worldActivityListener);
614
+ listeners.set(EventType.WORLD, worldActivityListener);
615
+ // Listen to message events for response content
616
+ world.eventEmitter.on(EventType.MESSAGE, messageListener);
617
+ listeners.set(EventType.MESSAGE, messageListener);
618
+ // Listen to SSE events to extend timeout on tool-stream data
619
+ const sseListener = (eventData) => {
620
+ if (eventData.type === 'tool-stream') {
621
+ resetTimeout();
622
+ }
623
+ };
624
+ world.eventEmitter.on(EventType.SSE, sseListener);
625
+ listeners.set(EventType.SSE, sseListener);
626
+ // Publish message
627
+ publishMessage(world, message, sender, chatId);
628
+ }).catch(error => {
629
+ hasError = true;
630
+ errorMessage = `Failed to connect to world: ${error instanceof Error ? error.message : error}`;
631
+ reject(new Error(errorMessage));
632
+ });
633
+ });
634
+ await responsePromise;
635
+ if (hasError) {
636
+ sendError(res, 500, errorMessage, 'CHAT_ERROR');
637
+ return;
638
+ }
639
+ res.json({
640
+ success: true,
641
+ message: 'Message processed successfully',
642
+ data: {
643
+ content: responseContent || 'No response received',
644
+ timestamp: new Date().toISOString()
645
+ }
646
+ });
647
+ }
648
+ catch (error) {
649
+ sendError(res, 500, error instanceof Error ? error.message : 'Unknown error', 'CHAT_ERROR');
650
+ }
651
+ finally {
652
+ // Cleanup listeners
653
+ if (subscription && listeners.size > 0) {
654
+ try {
655
+ const world = subscription.world;
656
+ for (const [eventType, listener] of listeners.entries()) {
657
+ world.eventEmitter.removeListener(eventType, listener);
658
+ }
659
+ listeners.clear();
660
+ await subscription.unsubscribe();
661
+ }
662
+ catch (cleanupError) {
663
+ loggerChat.error('Error during cleanup', { error: cleanupError instanceof Error ? cleanupError.message : cleanupError });
664
+ }
665
+ }
666
+ enableStreaming();
667
+ }
668
+ }
669
+ /**
670
+ * Handles streaming chat requests using Server-Sent Events (SSE).
671
+ * Subscribes to world events and streams them to the client in real-time.
672
+ * Uses world activity events to determine when to end the stream (with timeout fallback).
673
+ *
674
+ * @param req - Express request object
675
+ * @param res - Express response object
676
+ * @param worldName - Name of the world to send message to
677
+ * @param message - The message to send
678
+ * @param sender - Agent name sending the message
679
+ * @returns Promise that resolves when stream is complete
680
+ */
681
+ async function handleStreamingChat(req, res, worldName, message, sender, chatId) {
682
+ // Subscribe to world to get the world instance
683
+ const subscription = await subscribeWorld(worldName, { isOpen: true });
684
+ if (!subscription) {
685
+ loggerStream.error('Unexpected: subscription is null after world existence check');
686
+ res.setHeader('Content-Type', 'text/event-stream');
687
+ res.write(`data: ${JSON.stringify({ type: 'error', message: 'Failed to subscribe to world' })}\n\n`);
688
+ res.end();
689
+ return;
690
+ }
691
+ const world = subscription.world;
692
+ // Create SSE handler - automatically sets up headers, listeners, and cleanup
693
+ const sseHandler = createSSEHandler(req, res, world, 'chat', chatId);
694
+ // Clean up subscription when the HTTP response finishes to prevent stale world
695
+ // instances from accumulating in activeSubscribedWorlds.
696
+ res.on('finish', () => {
697
+ subscription?.unsubscribe();
698
+ });
699
+ try {
700
+ // Publish message - events will be automatically streamed
701
+ publishMessage(world, message, sender, chatId);
702
+ }
703
+ catch (error) {
704
+ sseHandler.sendSSE({
705
+ type: 'error',
706
+ message: 'Failed to send message',
707
+ data: { error: error instanceof Error ? error.message : String(error) }
708
+ });
709
+ setTimeout(() => {
710
+ sseHandler.endResponse();
711
+ }, 1000);
712
+ }
713
+ }
714
+ // Chat Routes
715
+ router.post('/worlds/:worldName/messages', validateWorld, async (req, res) => {
716
+ try {
717
+ const worldCtx = req.worldCtx;
718
+ const validation = ChatMessageSchema.safeParse(req.body);
719
+ if (!validation.success) {
720
+ sendError(res, 400, 'Invalid request body', 'VALIDATION_ERROR', validation.error.issues);
721
+ return;
722
+ }
723
+ const { message, sender, stream, chatId } = validation.data;
724
+ if (chatId && !(await chatExists(worldCtx, chatId))) {
725
+ sendError(res, 404, 'Chat not found', 'CHAT_NOT_FOUND');
726
+ return;
727
+ }
728
+ if (stream === false) {
729
+ await handleNonStreamingChat(res, worldCtx.id, message, sender, chatId);
730
+ }
731
+ else {
732
+ await handleStreamingChat(req, res, worldCtx.id, message, sender, chatId);
733
+ }
734
+ }
735
+ catch (error) {
736
+ loggerChat.error('Error in chat endpoint', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName });
737
+ if (!res.headersSent) {
738
+ sendError(res, 500, 'Failed to process chat request', 'CHAT_ERROR');
739
+ }
740
+ }
741
+ });
742
+ router.post('/worlds/:worldName/messages/stop', validateWorld, async (req, res) => {
743
+ try {
744
+ const worldCtx = req.worldCtx;
745
+ const validation = StopMessageProcessingSchema.safeParse(req.body);
746
+ if (!validation.success) {
747
+ sendError(res, 400, 'Invalid request body', 'VALIDATION_ERROR', validation.error.issues);
748
+ return;
749
+ }
750
+ const { chatId } = validation.data;
751
+ const result = stopMessageProcessing(worldCtx.id, chatId);
752
+ res.json(result);
753
+ }
754
+ catch (error) {
755
+ loggerChat.error('Error stopping message processing', {
756
+ error: error instanceof Error ? error.message : error,
757
+ worldName: req.params.worldName
758
+ });
759
+ sendError(res, 500, 'Failed to stop message processing', 'MESSAGE_STOP_ERROR');
760
+ }
761
+ });
762
+ router.put('/worlds/:worldName/messages/:messageId', validateWorld, async (req, res) => {
763
+ try {
764
+ const { messageId } = req.params;
765
+ const worldCtx = req.worldCtx;
766
+ const validation = MessageEditSchema.safeParse(req.body);
767
+ if (!validation.success) {
768
+ sendError(res, 400, 'Invalid request body', 'VALIDATION_ERROR', validation.error.issues);
769
+ return;
770
+ }
771
+ const { chatId, stream } = validation.data;
772
+ const newContent = validation.data.newContent.trim();
773
+ if (!(await chatExists(worldCtx, chatId))) {
774
+ sendError(res, 404, 'Chat not found', 'CHAT_NOT_FOUND');
775
+ return;
776
+ }
777
+ if (!newContent) {
778
+ sendError(res, 400, 'Message content cannot be empty', 'VALIDATION_ERROR');
779
+ return;
780
+ }
781
+ if (stream === false) {
782
+ const result = await editUserMessage(worldCtx.id, messageId, newContent, chatId);
783
+ if (!result.success) {
784
+ sendError(res, 500, 'Failed to edit message', 'MESSAGE_EDIT_ERROR', result.failedAgents);
785
+ return;
786
+ }
787
+ res.json({
788
+ ...result,
789
+ message: `Successfully edited message in ${result.processedAgents.length} agent(s)`
790
+ });
791
+ return;
792
+ }
793
+ const subscription = await subscribeWorld(worldCtx.id, { isOpen: true });
794
+ if (!subscription?.world) {
795
+ res.setHeader('Content-Type', 'text/event-stream');
796
+ res.setHeader('Cache-Control', 'no-cache');
797
+ res.setHeader('Connection', 'keep-alive');
798
+ res.write(`data: ${JSON.stringify({ type: 'error', message: 'Failed to subscribe to world for edit streaming' })}\n\n`);
799
+ res.end();
800
+ return;
801
+ }
802
+ const sseHandler = createSSEHandler(req, res, subscription.world, 'edit', chatId);
803
+ const finalizeWithError = (message, data) => {
804
+ sseHandler.sendSSE({
805
+ type: 'error',
806
+ message,
807
+ data
808
+ });
809
+ setTimeout(() => {
810
+ sseHandler.endResponse();
811
+ subscription?.unsubscribe();
812
+ }, 500);
813
+ };
814
+ // Clean up subscription when the HTTP response finishes.
815
+ res.on('finish', () => {
816
+ subscription?.unsubscribe();
817
+ });
818
+ // Pass subscription.world so editUserMessage emits on the same eventEmitter
819
+ // that the SSE handler is listening on, avoiding stale-world mismatch.
820
+ const result = await editUserMessage(worldCtx.id, messageId, newContent, chatId, subscription.world);
821
+ if (!result.success) {
822
+ finalizeWithError('Failed to edit message', {
823
+ code: 'MESSAGE_EDIT_ERROR',
824
+ failedAgents: result.failedAgents
825
+ });
826
+ return;
827
+ }
828
+ if (result.resubmissionStatus !== 'success') {
829
+ finalizeWithError(`Messages removed but resubmission failed: ${String(result.resubmissionError || result.resubmissionStatus || 'unknown')}`, {
830
+ code: 'MESSAGE_RESUBMISSION_FAILED',
831
+ result
832
+ });
833
+ return;
834
+ }
835
+ }
836
+ catch (error) {
837
+ const errorMessage = error instanceof Error ? error.message : String(error);
838
+ if (errorMessage.includes('Cannot edit message while world is processing') ||
839
+ errorMessage.includes('Cannot edit message while target chat is processing')) {
840
+ sendError(res, 423, 'World is currently processing another message', 'WORLD_LOCKED');
841
+ return;
842
+ }
843
+ if (errorMessage.includes("World '") && errorMessage.includes('not found')) {
844
+ sendError(res, 404, 'World not found', 'WORLD_NOT_FOUND');
845
+ return;
846
+ }
847
+ loggerChat.error('Error editing message', {
848
+ error: errorMessage,
849
+ worldName: req.params.worldName,
850
+ messageId: req.params.messageId
851
+ });
852
+ if (!res.headersSent) {
853
+ sendError(res, 500, 'Failed to edit message', 'MESSAGE_EDIT_ERROR');
854
+ }
855
+ }
856
+ });
857
+ router.delete('/worlds/:worldName/messages/:messageId', validateWorld, async (req, res) => {
858
+ try {
859
+ const { messageId } = req.params;
860
+ const worldCtx = req.worldCtx;
861
+ const world = req.world;
862
+ // Validate request body - only chatId needed for removal
863
+ const validation = z.object({
864
+ chatId: z.string()
865
+ }).safeParse(req.body);
866
+ if (!validation.success) {
867
+ sendError(res, 400, 'Invalid request body', 'VALIDATION_ERROR', validation.error.issues);
868
+ return;
869
+ }
870
+ const { chatId } = validation.data;
871
+ // Check if world is processing
872
+ if (world.isProcessing) {
873
+ sendError(res, 423, 'World is currently processing another message', 'WORLD_LOCKED');
874
+ return;
875
+ }
876
+ // Verify message exists and get its details
877
+ const memory = await coreGetMemory(worldCtx.id, chatId);
878
+ if (!memory) {
879
+ sendError(res, 404, 'Chat not found', 'CHAT_NOT_FOUND');
880
+ return;
881
+ }
882
+ const targetMessage = memory.find(m => m.messageId === messageId);
883
+ if (!targetMessage) {
884
+ sendError(res, 404, 'Message not found', 'MESSAGE_NOT_FOUND');
885
+ return;
886
+ }
887
+ // Verify it's a user message (check role, not sender)
888
+ if (targetMessage.role !== 'user') {
889
+ sendError(res, 400, 'Can only edit user messages', 'INVALID_MESSAGE_TYPE');
890
+ return;
891
+ }
892
+ // Perform removal only (frontend will handle resubmission)
893
+ const result = await removeMessagesFrom(worldCtx.id, messageId, chatId);
894
+ // Return removal result
895
+ if (!result.success) {
896
+ sendError(res, 500, 'Failed to remove messages', 'REMOVAL_ERROR', result.failedAgents);
897
+ return;
898
+ }
899
+ res.json({
900
+ ...result,
901
+ message: `Successfully removed ${result.messagesRemovedTotal} message(s) from ${result.processedAgents.length} agent(s)`
902
+ });
903
+ }
904
+ catch (error) {
905
+ loggerChat.error('Error deleting message', {
906
+ error: error instanceof Error ? error.message : error,
907
+ worldName: req.params.worldName,
908
+ messageId: req.params.messageId
909
+ });
910
+ if (!res.headersSent) {
911
+ sendError(res, 500, 'Failed to edit message', 'MESSAGE_EDIT_ERROR');
912
+ }
913
+ }
914
+ });
915
+ router.post('/worlds/:worldName/hitl/respond', validateWorld, async (req, res) => {
916
+ try {
917
+ const validation = HitlResponseSchema.safeParse(req.body);
918
+ if (!validation.success) {
919
+ sendError(res, 400, 'Invalid request body', 'VALIDATION_ERROR', validation.error.issues);
920
+ return;
921
+ }
922
+ const worldCtx = req.worldCtx;
923
+ const { requestId, optionId } = validation.data;
924
+ const result = submitWorldOptionResponse({
925
+ worldId: worldCtx.id,
926
+ requestId,
927
+ optionId
928
+ });
929
+ res.json(result);
930
+ }
931
+ catch (error) {
932
+ loggerChat.error('Error submitting HITL response', {
933
+ error: error instanceof Error ? error.message : error,
934
+ worldName: req.params.worldName
935
+ });
936
+ sendError(res, 500, 'Failed to submit HITL response', 'HITL_RESPONSE_ERROR');
937
+ }
938
+ });
939
+ router.delete('/worlds/:worldName/chats/:chatId', validateWorld, async (req, res) => {
940
+ try {
941
+ const { chatId } = req.params;
942
+ const worldCtx = req.worldCtx;
943
+ const deleted = await worldCtx.deleteChat(chatId);
944
+ if (!deleted) {
945
+ sendError(res, 404, 'Chat not found', 'CHAT_NOT_FOUND');
946
+ return;
947
+ }
948
+ res.json({ message: 'Chat deleted successfully' });
949
+ }
950
+ catch (error) {
951
+ loggerChat.error('Error deleting chat', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName, chatId: req.params.chatId });
952
+ if (!res.headersSent) {
953
+ sendError(res, 500, 'Failed to delete chat', 'DELETE_CHAT_ERROR');
954
+ }
955
+ }
956
+ });
957
+ router.get('/worlds/:worldName/chats', validateWorld, async (req, res) => {
958
+ try {
959
+ const worldCtx = req.worldCtx;
960
+ const chats = await worldCtx.listChats();
961
+ res.json(chats.map(serializeChat));
962
+ }
963
+ catch (error) {
964
+ loggerChat.error('Error listing chats', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName });
965
+ sendError(res, 500, 'Failed to list chats', 'CHAT_LIST_ERROR');
966
+ }
967
+ });
968
+ // router.get('/worlds/:worldName/chats/:chatId', validateWorld, async (req: Request, res: Response): Promise<void> => {
969
+ // try {
970
+ // const { chatId } = req.params;
971
+ // const worldCtx = (req as any).worldCtx as ReturnType<typeof createWorldContext>;
972
+ // const world = await worldCtx.load();
973
+ // if (!world) {
974
+ // sendError(res, 404, 'World not found', 'WORLD_NOT_FOUND');
975
+ // return;
976
+ // }
977
+ // const chat = world.chats.get(chatId);
978
+ // if (!chat) {
979
+ // sendError(res, 404, 'Chat not found', 'CHAT_NOT_FOUND');
980
+ // return;
981
+ // }
982
+ // res.json(serializeChat(chat));
983
+ // } catch (error) {
984
+ // logger.error('Error getting chat', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName, chatId: req.params.chatId });
985
+ // sendError(res, 500, 'Failed to get chat', 'CHAT_GET_ERROR');
986
+ // }
987
+ // });
988
+ // router.get('/worlds/:worldName/chats/:chatId/messages', validateWorld, async (req: Request, res: Response): Promise<void> => {
989
+ // try {
990
+ // const { chatId } = req.params;
991
+ // const worldCtx = (req as any).worldCtx as ReturnType<typeof createWorldContext>;
992
+ // const messages = await worldCtx.getMemory(chatId);
993
+ // res.json(messages || []);
994
+ // } catch (error) {
995
+ // logger.error('Error getting chat messages', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName, chatId: req.params.chatId });
996
+ // sendError(res, 500, 'Failed to get chat messages', 'CHAT_MESSAGES_ERROR');
997
+ // }
998
+ // });
999
+ router.post('/worlds/:worldName/chats', validateWorld, async (req, res) => {
1000
+ try {
1001
+ const worldCtx = req.worldCtx;
1002
+ const updatedWorld = await worldCtx.newChat();
1003
+ if (!updatedWorld) {
1004
+ sendError(res, 400, 'Failed to create new chat', 'CHAT_CREATION_ERROR');
1005
+ return;
1006
+ }
1007
+ res.json({
1008
+ world: serializeWorld(updatedWorld),
1009
+ chatId: updatedWorld.currentChatId,
1010
+ success: true
1011
+ });
1012
+ }
1013
+ catch (error) {
1014
+ loggerChat.error('Error creating new chat', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName });
1015
+ sendError(res, 500, 'Failed to create new chat', 'NEW_CHAT_ERROR');
1016
+ }
1017
+ });
1018
+ router.post('/worlds/:worldName/setChat/:chatId', validateWorld, async (req, res) => {
1019
+ try {
1020
+ const { chatId } = req.params;
1021
+ const worldCtx = req.worldCtx;
1022
+ const currentWorld = req.world;
1023
+ if (!currentWorld) {
1024
+ sendError(res, 404, 'World not found', 'WORLD_NOT_FOUND');
1025
+ return;
1026
+ }
1027
+ const updatedWorld = await worldCtx.setChat(chatId);
1028
+ if (!updatedWorld) {
1029
+ res.json({
1030
+ world: serializeWorld(currentWorld),
1031
+ chatId: currentWorld.currentChatId,
1032
+ success: false
1033
+ });
1034
+ return;
1035
+ }
1036
+ res.json({
1037
+ world: serializeWorld(updatedWorld),
1038
+ chatId: updatedWorld.currentChatId,
1039
+ success: true
1040
+ });
1041
+ }
1042
+ catch (error) {
1043
+ loggerChat.error('Error loading chat', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName, chatId: req.params.chatId });
1044
+ if (!res.headersSent) {
1045
+ sendError(res, 500, 'Failed to load chat', 'LOAD_CHAT_ERROR');
1046
+ }
1047
+ }
1048
+ });
1049
+ // MCP Server Management Routes
1050
+ router.get('/mcp/servers', async (req, res) => {
1051
+ try {
1052
+ const servers = listMCPServers();
1053
+ const serversInfo = servers.map(server => ({
1054
+ id: server.id.slice(0, 8), // Truncated ID for display
1055
+ name: server.config.name,
1056
+ transport: server.config.transport,
1057
+ status: server.status,
1058
+ referenceCount: server.referenceCount,
1059
+ startedAt: server.startedAt,
1060
+ lastHealthCheck: server.lastHealthCheck,
1061
+ associatedWorlds: Array.from(server.associatedWorlds),
1062
+ error: server.error?.message
1063
+ }));
1064
+ const stats = getMCPRegistryStats();
1065
+ res.json({
1066
+ servers: serversInfo,
1067
+ stats
1068
+ });
1069
+ }
1070
+ catch (error) {
1071
+ loggerMcp.error('Error listing MCP servers', { error: error instanceof Error ? error.message : error });
1072
+ sendError(res, 500, 'Failed to list MCP servers', 'MCP_LIST_ERROR');
1073
+ }
1074
+ });
1075
+ router.post('/mcp/servers/:serverId/restart', async (req, res) => {
1076
+ try {
1077
+ const { serverId } = req.params;
1078
+ // Find full server ID from partial ID
1079
+ const servers = listMCPServers();
1080
+ const server = servers.find(s => s.id.startsWith(serverId) || s.id === serverId);
1081
+ if (!server) {
1082
+ sendError(res, 404, 'MCP server not found', 'MCP_SERVER_NOT_FOUND');
1083
+ return;
1084
+ }
1085
+ const success = await restartMCPServer(server.id);
1086
+ if (success) {
1087
+ res.json({
1088
+ success: true,
1089
+ message: `MCP server ${server.config.name} restarted successfully`,
1090
+ serverId: server.id.slice(0, 8)
1091
+ });
1092
+ }
1093
+ else {
1094
+ sendError(res, 500, 'Failed to restart MCP server', 'MCP_RESTART_ERROR');
1095
+ }
1096
+ }
1097
+ catch (error) {
1098
+ loggerMcp.error('Error restarting MCP server', {
1099
+ error: error instanceof Error ? error.message : error,
1100
+ serverId: req.params.serverId
1101
+ });
1102
+ sendError(res, 500, 'Failed to restart MCP server', 'MCP_RESTART_ERROR');
1103
+ }
1104
+ });
1105
+ router.get('/mcp/health', async (req, res) => {
1106
+ try {
1107
+ const health = getMCPSystemHealth();
1108
+ const stats = getMCPRegistryStats();
1109
+ res.json({
1110
+ ...health,
1111
+ timestamp: new Date().toISOString(),
1112
+ registry: stats
1113
+ });
1114
+ }
1115
+ catch (error) {
1116
+ loggerMcp.error('Error getting MCP health', { error: error instanceof Error ? error.message : error });
1117
+ res.status(500).json({
1118
+ status: 'unhealthy',
1119
+ timestamp: new Date().toISOString(),
1120
+ error: 'Failed to get MCP system health'
1121
+ });
1122
+ }
1123
+ });
1124
+ export default router;