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.
- package/README.md +17 -7
- package/dist/cli/commands.d.ts +109 -0
- package/dist/cli/commands.js +2024 -0
- package/dist/cli/display.d.ts +124 -0
- package/dist/cli/display.js +381 -0
- package/dist/cli/hitl.d.ts +33 -0
- package/dist/cli/hitl.js +81 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/stream.d.ts +41 -0
- package/dist/cli/stream.js +222 -0
- package/dist/core/activity-tracker.d.ts +16 -0
- package/dist/core/activity-tracker.d.ts.map +1 -0
- package/dist/core/activity-tracker.js +91 -0
- package/dist/core/activity-tracker.js.map +1 -0
- package/dist/core/ai-commands.d.ts +16 -0
- package/dist/core/ai-commands.d.ts.map +1 -0
- package/dist/core/ai-commands.js +24 -0
- package/dist/core/ai-commands.js.map +1 -0
- package/dist/core/ai-sdk-patch.d.ts +24 -0
- package/dist/core/ai-sdk-patch.d.ts.map +1 -0
- package/dist/core/ai-sdk-patch.js +169 -0
- package/dist/core/ai-sdk-patch.js.map +1 -0
- package/dist/core/anthropic-direct.d.ts +52 -0
- package/dist/core/anthropic-direct.d.ts.map +1 -0
- package/dist/core/anthropic-direct.js +301 -0
- package/dist/core/anthropic-direct.js.map +1 -0
- package/dist/core/approval-cache.d.ts +104 -0
- package/dist/core/approval-cache.d.ts.map +1 -0
- package/dist/core/approval-cache.js +150 -0
- package/dist/core/approval-cache.js.map +1 -0
- package/dist/core/chat-constants.d.ts +20 -0
- package/dist/core/chat-constants.d.ts.map +1 -0
- package/dist/core/chat-constants.js +22 -0
- package/dist/core/chat-constants.js.map +1 -0
- package/dist/core/create-agent-tool.d.ts +66 -0
- package/dist/core/create-agent-tool.d.ts.map +1 -0
- package/dist/core/create-agent-tool.js +212 -0
- package/dist/core/create-agent-tool.js.map +1 -0
- package/dist/core/events/approval-checker.d.ts +61 -0
- package/dist/core/events/approval-checker.d.ts.map +1 -0
- package/dist/core/events/approval-checker.js +226 -0
- package/dist/core/events/approval-checker.js.map +1 -0
- package/dist/core/events/index.d.ts +25 -0
- package/dist/core/events/index.d.ts.map +1 -0
- package/dist/core/events/index.js +30 -0
- package/dist/core/events/index.js.map +1 -0
- package/dist/core/events/memory-manager.d.ts +73 -0
- package/dist/core/events/memory-manager.d.ts.map +1 -0
- package/dist/core/events/memory-manager.js +1218 -0
- package/dist/core/events/memory-manager.js.map +1 -0
- package/dist/core/events/mention-logic.d.ts +39 -0
- package/dist/core/events/mention-logic.d.ts.map +1 -0
- package/dist/core/events/mention-logic.js +163 -0
- package/dist/core/events/mention-logic.js.map +1 -0
- package/dist/core/events/orchestrator.d.ts +69 -0
- package/dist/core/events/orchestrator.d.ts.map +1 -0
- package/dist/core/events/orchestrator.js +883 -0
- package/dist/core/events/orchestrator.js.map +1 -0
- package/dist/core/events/persistence.d.ts +41 -0
- package/dist/core/events/persistence.d.ts.map +1 -0
- package/dist/core/events/persistence.js +296 -0
- package/dist/core/events/persistence.js.map +1 -0
- package/dist/core/events/publishers.d.ts +81 -0
- package/dist/core/events/publishers.d.ts.map +1 -0
- package/dist/core/events/publishers.js +272 -0
- package/dist/core/events/publishers.js.map +1 -0
- package/dist/core/events/subscribers.d.ts +45 -0
- package/dist/core/events/subscribers.d.ts.map +1 -0
- package/dist/core/events/subscribers.js +288 -0
- package/dist/core/events/subscribers.js.map +1 -0
- package/dist/core/events/tool-bridge-logging.d.ts +28 -0
- package/dist/core/events/tool-bridge-logging.d.ts.map +1 -0
- package/dist/core/events/tool-bridge-logging.js +94 -0
- package/dist/core/events/tool-bridge-logging.js.map +1 -0
- package/dist/core/events-metadata.d.ts +72 -0
- package/dist/core/events-metadata.d.ts.map +1 -0
- package/dist/core/events-metadata.js +167 -0
- package/dist/core/events-metadata.js.map +1 -0
- package/dist/core/events.d.ts +186 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +1248 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/export.d.ts +106 -0
- package/dist/core/export.d.ts.map +1 -0
- package/dist/core/export.js +705 -0
- package/dist/core/export.js.map +1 -0
- package/dist/core/file-tools.d.ts +114 -0
- package/dist/core/file-tools.d.ts.map +1 -0
- package/dist/core/file-tools.js +370 -0
- package/dist/core/file-tools.js.map +1 -0
- package/dist/core/google-direct.d.ts +58 -0
- package/dist/core/google-direct.d.ts.map +1 -0
- package/dist/core/google-direct.js +298 -0
- package/dist/core/google-direct.js.map +1 -0
- package/dist/core/hitl.d.ts +54 -0
- package/dist/core/hitl.d.ts.map +1 -0
- package/dist/core/hitl.js +153 -0
- package/dist/core/hitl.js.map +1 -0
- package/dist/core/index.d.ts +59 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +70 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/llm-config.d.ts +128 -0
- package/dist/core/llm-config.d.ts.map +1 -0
- package/dist/core/llm-config.js +164 -0
- package/dist/core/llm-config.js.map +1 -0
- package/dist/core/llm-manager.d.ts +163 -0
- package/dist/core/llm-manager.d.ts.map +1 -0
- package/dist/core/llm-manager.js +669 -0
- package/dist/core/llm-manager.js.map +1 -0
- package/dist/core/load-skill-tool.d.ts +55 -0
- package/dist/core/load-skill-tool.d.ts.map +1 -0
- package/dist/core/load-skill-tool.js +468 -0
- package/dist/core/load-skill-tool.js.map +1 -0
- package/dist/core/logger.d.ts +88 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +358 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/managers.d.ts +131 -0
- package/dist/core/managers.d.ts.map +1 -0
- package/dist/core/managers.js +1223 -0
- package/dist/core/managers.js.map +1 -0
- package/dist/core/mcp-server-registry.d.ts +304 -0
- package/dist/core/mcp-server-registry.d.ts.map +1 -0
- package/dist/core/mcp-server-registry.js +1769 -0
- package/dist/core/mcp-server-registry.js.map +1 -0
- package/dist/core/mcp-tools.d.ts +56 -0
- package/dist/core/mcp-tools.d.ts.map +1 -0
- package/dist/core/mcp-tools.js +186 -0
- package/dist/core/mcp-tools.js.map +1 -0
- package/dist/core/message-prep.d.ts +81 -0
- package/dist/core/message-prep.d.ts.map +1 -0
- package/dist/core/message-prep.js +223 -0
- package/dist/core/message-prep.js.map +1 -0
- package/dist/core/message-processing-control.d.ts +54 -0
- package/dist/core/message-processing-control.d.ts.map +1 -0
- package/dist/core/message-processing-control.js +139 -0
- package/dist/core/message-processing-control.js.map +1 -0
- package/dist/core/openai-direct.d.ts +80 -0
- package/dist/core/openai-direct.d.ts.map +1 -0
- package/dist/core/openai-direct.js +374 -0
- package/dist/core/openai-direct.js.map +1 -0
- package/dist/core/shell-cmd-tool.d.ts +235 -0
- package/dist/core/shell-cmd-tool.d.ts.map +1 -0
- package/dist/core/shell-cmd-tool.js +1157 -0
- package/dist/core/shell-cmd-tool.js.map +1 -0
- package/dist/core/shell-process-registry.d.ts +88 -0
- package/dist/core/shell-process-registry.d.ts.map +1 -0
- package/dist/core/shell-process-registry.js +309 -0
- package/dist/core/shell-process-registry.js.map +1 -0
- package/dist/core/skill-registry.d.ts +75 -0
- package/dist/core/skill-registry.d.ts.map +1 -0
- package/dist/core/skill-registry.js +369 -0
- package/dist/core/skill-registry.js.map +1 -0
- package/dist/core/skill-script-runner.d.ts +89 -0
- package/dist/core/skill-script-runner.d.ts.map +1 -0
- package/dist/core/skill-script-runner.js +274 -0
- package/dist/core/skill-script-runner.js.map +1 -0
- package/dist/core/skill-selector.d.ts +65 -0
- package/dist/core/skill-selector.d.ts.map +1 -0
- package/dist/core/skill-selector.js +190 -0
- package/dist/core/skill-selector.js.map +1 -0
- package/dist/core/skill-settings.d.ts +20 -0
- package/dist/core/skill-settings.d.ts.map +1 -0
- package/dist/core/skill-settings.js +40 -0
- package/dist/core/skill-settings.js.map +1 -0
- package/dist/core/storage/agent-storage.d.ts +134 -0
- package/dist/core/storage/agent-storage.d.ts.map +1 -0
- package/dist/core/storage/agent-storage.js +498 -0
- package/dist/core/storage/agent-storage.js.map +1 -0
- package/dist/core/storage/eventStorage/fileEventStorage.d.ts +100 -0
- package/dist/core/storage/eventStorage/fileEventStorage.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/fileEventStorage.js +494 -0
- package/dist/core/storage/eventStorage/fileEventStorage.js.map +1 -0
- package/dist/core/storage/eventStorage/index.d.ts +31 -0
- package/dist/core/storage/eventStorage/index.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/index.js +31 -0
- package/dist/core/storage/eventStorage/index.js.map +1 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.d.ts +87 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.js +244 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.js.map +1 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts +45 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.js +301 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.js.map +1 -0
- package/dist/core/storage/eventStorage/types.d.ts +142 -0
- package/dist/core/storage/eventStorage/types.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/types.js +43 -0
- package/dist/core/storage/eventStorage/types.js.map +1 -0
- package/dist/core/storage/eventStorage/validation.d.ts +30 -0
- package/dist/core/storage/eventStorage/validation.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/validation.js +68 -0
- package/dist/core/storage/eventStorage/validation.js.map +1 -0
- package/dist/core/storage/legacy-migrations.d.ts +45 -0
- package/dist/core/storage/legacy-migrations.d.ts.map +1 -0
- package/dist/core/storage/legacy-migrations.js +295 -0
- package/dist/core/storage/legacy-migrations.js.map +1 -0
- package/dist/core/storage/memory-storage.d.ts +105 -0
- package/dist/core/storage/memory-storage.d.ts.map +1 -0
- package/dist/core/storage/memory-storage.js +415 -0
- package/dist/core/storage/memory-storage.js.map +1 -0
- package/dist/core/storage/migration-runner.d.ts +96 -0
- package/dist/core/storage/migration-runner.d.ts.map +1 -0
- package/dist/core/storage/migration-runner.js +306 -0
- package/dist/core/storage/migration-runner.js.map +1 -0
- package/dist/core/storage/queue-storage.d.ts +147 -0
- package/dist/core/storage/queue-storage.d.ts.map +1 -0
- package/dist/core/storage/queue-storage.js +290 -0
- package/dist/core/storage/queue-storage.js.map +1 -0
- package/dist/core/storage/skill-storage.d.ts +136 -0
- package/dist/core/storage/skill-storage.d.ts.map +1 -0
- package/dist/core/storage/skill-storage.js +474 -0
- package/dist/core/storage/skill-storage.js.map +1 -0
- package/dist/core/storage/sqlite-schema.d.ts +95 -0
- package/dist/core/storage/sqlite-schema.d.ts.map +1 -0
- package/dist/core/storage/sqlite-schema.js +156 -0
- package/dist/core/storage/sqlite-schema.js.map +1 -0
- package/dist/core/storage/sqlite-storage.d.ts +146 -0
- package/dist/core/storage/sqlite-storage.d.ts.map +1 -0
- package/dist/core/storage/sqlite-storage.js +709 -0
- package/dist/core/storage/sqlite-storage.js.map +1 -0
- package/dist/core/storage/storage-factory.d.ts +61 -0
- package/dist/core/storage/storage-factory.d.ts.map +1 -0
- package/dist/core/storage/storage-factory.js +794 -0
- package/dist/core/storage/storage-factory.js.map +1 -0
- package/dist/core/storage/validation.d.ts +36 -0
- package/dist/core/storage/validation.d.ts.map +1 -0
- package/dist/core/storage/validation.js +79 -0
- package/dist/core/storage/validation.js.map +1 -0
- package/dist/core/storage/world-storage.d.ts +114 -0
- package/dist/core/storage/world-storage.d.ts.map +1 -0
- package/dist/core/storage/world-storage.js +378 -0
- package/dist/core/storage/world-storage.js.map +1 -0
- package/dist/core/subscription.d.ts +43 -0
- package/dist/core/subscription.d.ts.map +1 -0
- package/dist/core/subscription.js +227 -0
- package/dist/core/subscription.js.map +1 -0
- package/dist/core/tool-utils.d.ts +80 -0
- package/dist/core/tool-utils.d.ts.map +1 -0
- package/dist/core/tool-utils.js +273 -0
- package/dist/core/tool-utils.js.map +1 -0
- package/dist/core/types.d.ts +595 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +158 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/utils.d.ts +138 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/core/utils.js +478 -0
- package/dist/core/utils.js.map +1 -0
- package/dist/core/world-class.d.ts +43 -0
- package/dist/core/world-class.d.ts.map +1 -0
- package/dist/core/world-class.js +90 -0
- package/dist/core/world-class.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/public/assets/agent-sprites-DJFgj-zP.png +0 -0
- package/dist/public/assets/border-KHK37r8y.svg +83 -0
- package/dist/public/assets/index-C9kPXL6G.css +1 -0
- package/dist/public/assets/index-DOQEHGWt.js +96 -0
- package/dist/public/index.html +21 -0
- package/dist/server/api.d.ts +2 -0
- package/dist/server/api.js +1124 -0
- package/dist/server/index.d.ts +29 -0
- package/dist/server/sse-handler.d.ts +62 -0
- package/dist/server/sse-handler.js +234 -0
- package/package.json +15 -3
- 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;
|