agent-world 0.11.0 → 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 +55 -4
- 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 +21 -5
- package/scripts/launch-electron.js +0 -58
|
@@ -0,0 +1,1248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Events Module - World and Agent Event Functions
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Event-driven message publishing, agent response processing, and memory persistence
|
|
5
|
+
*
|
|
6
|
+
* Logging: Enable with LOG_EVENTS=debug or specific categories:
|
|
7
|
+
* - LOG_EVENTS_PUBLISH, LOG_EVENTS_AGENT, LOG_EVENTS_RESPONSE, LOG_EVENTS_MEMORY
|
|
8
|
+
* - LOG_EVENTS_AUTOMENTION, LOG_EVENTS_TURNLIMIT, LOG_EVENTS_CHATTITLE
|
|
9
|
+
*
|
|
10
|
+
* Core Features:
|
|
11
|
+
* - Event publishing/subscription via World.eventEmitter with type safety
|
|
12
|
+
* - Agent message filtering with mention detection and turn limits
|
|
13
|
+
* - Auto-mention logic with loop prevention and world tags (<world>STOP|TO:a,b</world>)
|
|
14
|
+
* - Message threading with replyToMessageId preservation
|
|
15
|
+
* - Event persistence with automatic chatId defaulting to world.currentChatId
|
|
16
|
+
* - Tool approval system with session/one-time approval tracking
|
|
17
|
+
* - Chat title generation on world idle events
|
|
18
|
+
*
|
|
19
|
+
* Recent Changes (2025-11):
|
|
20
|
+
* - Fixed tool event metadata validation errors: Added required ownerAgentId and
|
|
21
|
+
* triggeredByMessageId fields to toolHandler, added validation guards to reject
|
|
22
|
+
* tool events missing messageId or agentName
|
|
23
|
+
* - Removed unnecessary 'info' event emission from tool-utils.ts to prevent validation errors
|
|
24
|
+
* - Fixed approval response broadcast bug: Removed HUMAN check from shouldAutoMention to ensure
|
|
25
|
+
* agent responses to HUMAN approval messages include proper targeting mentions (@HUMAN),
|
|
26
|
+
* preventing unintended broadcast to all agents
|
|
27
|
+
* - Consolidated redundant logging and streamlined approval checking
|
|
28
|
+
* - Added tool_calls/tool_call_id persistence for approval messages
|
|
29
|
+
* - Pre-generate message IDs for agent responses
|
|
30
|
+
* - Fixed activity tracking to prevent premature idle signals
|
|
31
|
+
*/
|
|
32
|
+
import { SenderType, EventType } from './types.js';
|
|
33
|
+
import { generateId } from './utils.js';
|
|
34
|
+
import { generateAgentResponse } from './llm-manager.js';
|
|
35
|
+
import { beginWorldActivity } from './activity-tracker.js';
|
|
36
|
+
import { createStorageWithWrappers } from './storage/storage-factory.js';
|
|
37
|
+
import { getWorldTurnLimit, extractMentions, extractParagraphBeginningMentions, determineSenderType, prepareMessagesForLLM } from './utils.js';
|
|
38
|
+
import { parseMessageContent } from './message-prep.js';
|
|
39
|
+
import { createCategoryLogger } from './logger.js';
|
|
40
|
+
import { calculateOwnerAgentIds, calculateRecipientAgentId, calculateIsMemoryOnly, calculateIsCrossAgentMessage, calculateMessageDirection } from './events-metadata.js';
|
|
41
|
+
// Function-specific loggers for granular debugging control
|
|
42
|
+
const loggerPublish = createCategoryLogger('events.publish');
|
|
43
|
+
const loggerAgent = createCategoryLogger('events.agent');
|
|
44
|
+
const loggerResponse = createCategoryLogger('events.response');
|
|
45
|
+
const loggerMemory = createCategoryLogger('events.memory');
|
|
46
|
+
const loggerAutoMention = createCategoryLogger('events.automention');
|
|
47
|
+
const loggerTurnLimit = createCategoryLogger('events.turnlimit');
|
|
48
|
+
const loggerChatTitle = createCategoryLogger('events.chattitle');
|
|
49
|
+
// Global streaming control
|
|
50
|
+
let globalStreamingEnabled = true;
|
|
51
|
+
export function enableStreaming() { globalStreamingEnabled = true; }
|
|
52
|
+
export function disableStreaming() { globalStreamingEnabled = false; }
|
|
53
|
+
/**
|
|
54
|
+
* Publish CRUD event for agent, chat, or world configuration changes
|
|
55
|
+
* Allows subscribed clients to receive real-time updates for all CRUD operations
|
|
56
|
+
*/
|
|
57
|
+
export function publishCRUDEvent(world, operation, entityType, entityId, entityData) {
|
|
58
|
+
const event = {
|
|
59
|
+
operation,
|
|
60
|
+
entityType,
|
|
61
|
+
entityId,
|
|
62
|
+
entityData: operation === 'delete' ? null : entityData,
|
|
63
|
+
timestamp: new Date(),
|
|
64
|
+
chatId: world.currentChatId ?? null
|
|
65
|
+
};
|
|
66
|
+
world.eventEmitter.emit(EventType.CRUD, event);
|
|
67
|
+
loggerPublish.debug('CRUD event published', {
|
|
68
|
+
worldId: world.id,
|
|
69
|
+
operation,
|
|
70
|
+
entityType,
|
|
71
|
+
entityId
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Storage wrapper instance - initialized lazily
|
|
75
|
+
let storageWrappers = null;
|
|
76
|
+
async function getStorageWrappers() {
|
|
77
|
+
if (!storageWrappers) {
|
|
78
|
+
storageWrappers = await createStorageWithWrappers();
|
|
79
|
+
}
|
|
80
|
+
return storageWrappers;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Setup automatic event persistence listeners on World event emitter.
|
|
84
|
+
* Should be called once during World initialization.
|
|
85
|
+
*
|
|
86
|
+
* Events are persisted synchronously/awaitable for reliability.
|
|
87
|
+
* Failures are logged but don't block event emission.
|
|
88
|
+
* Returns a cleanup function to remove listeners.
|
|
89
|
+
*
|
|
90
|
+
* Environment variables:
|
|
91
|
+
* - DISABLE_EVENT_PERSISTENCE=true: Skip all persistence
|
|
92
|
+
*/
|
|
93
|
+
export function setupEventPersistence(world) {
|
|
94
|
+
if (process.env.DISABLE_EVENT_PERSISTENCE === 'true') {
|
|
95
|
+
loggerPublish.debug('Event persistence disabled by environment', { worldId: world.id });
|
|
96
|
+
return () => { }; // Return no-op cleanup
|
|
97
|
+
}
|
|
98
|
+
if (!world.eventStorage) {
|
|
99
|
+
loggerPublish.debug('Event storage not configured - events will not be persisted', { worldId: world.id });
|
|
100
|
+
return () => { }; // Return no-op cleanup
|
|
101
|
+
}
|
|
102
|
+
const storage = world.eventStorage;
|
|
103
|
+
// Helper to handle async persistence
|
|
104
|
+
const persistEvent = async (eventData) => {
|
|
105
|
+
try {
|
|
106
|
+
await storage.saveEvent(eventData);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
loggerPublish.error('Failed to persist event', {
|
|
110
|
+
worldId: world.id,
|
|
111
|
+
eventId: eventData.id,
|
|
112
|
+
eventType: eventData.type,
|
|
113
|
+
error: error instanceof Error ? error.message : error
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
// Message event persistence
|
|
118
|
+
const messageHandler = (event) => {
|
|
119
|
+
// Calculate enhanced metadata using helper functions
|
|
120
|
+
const ownerAgentIds = calculateOwnerAgentIds(world, event);
|
|
121
|
+
const recipientAgentId = calculateRecipientAgentId(world, event);
|
|
122
|
+
const messageDirection = calculateMessageDirection(world, event);
|
|
123
|
+
const isMemoryOnly = calculateIsMemoryOnly(world, event);
|
|
124
|
+
const isCrossAgentMessage = calculateIsCrossAgentMessage(world, event);
|
|
125
|
+
const isHumanMessage = event.sender === 'human' || event.sender === 'user';
|
|
126
|
+
// Calculate thread metadata (requires loading messages for accurate depth calculation)
|
|
127
|
+
// For now, use simplified version - can enhance later with full message history
|
|
128
|
+
const threadMetadata = event.replyToMessageId
|
|
129
|
+
? { threadRootId: event.replyToMessageId, threadDepth: 1, isReply: true }
|
|
130
|
+
: { threadRootId: null, threadDepth: 0, isReply: false };
|
|
131
|
+
// Get tool call information if present
|
|
132
|
+
const hasToolCalls = !!(event.tool_calls?.length);
|
|
133
|
+
const toolCallCount = event.tool_calls?.length || 0;
|
|
134
|
+
const eventData = {
|
|
135
|
+
id: event.messageId,
|
|
136
|
+
worldId: world.id,
|
|
137
|
+
chatId: event.chatId || null,
|
|
138
|
+
type: 'message',
|
|
139
|
+
payload: {
|
|
140
|
+
content: event.content,
|
|
141
|
+
sender: event.sender,
|
|
142
|
+
replyToMessageId: event.replyToMessageId,
|
|
143
|
+
// Preserve OpenAI protocol fields for tool calls and approvals
|
|
144
|
+
role: event.role,
|
|
145
|
+
tool_calls: event.tool_calls,
|
|
146
|
+
tool_call_id: event.tool_call_id
|
|
147
|
+
},
|
|
148
|
+
meta: {
|
|
149
|
+
// Core fields
|
|
150
|
+
sender: event.sender,
|
|
151
|
+
chatId: event.chatId || null,
|
|
152
|
+
// Agent Context
|
|
153
|
+
ownerAgentIds,
|
|
154
|
+
recipientAgentId,
|
|
155
|
+
originalSender: null, // Will be set for cross-agent forwarding in future
|
|
156
|
+
deliveredToAgents: ownerAgentIds, // Same as owner for now
|
|
157
|
+
// Message Classification
|
|
158
|
+
messageDirection,
|
|
159
|
+
isMemoryOnly,
|
|
160
|
+
isCrossAgentMessage,
|
|
161
|
+
isHumanMessage,
|
|
162
|
+
// Threading
|
|
163
|
+
threadRootId: threadMetadata.threadRootId,
|
|
164
|
+
threadDepth: threadMetadata.threadDepth,
|
|
165
|
+
isReply: threadMetadata.isReply,
|
|
166
|
+
hasReplies: false, // Will be updated async in future
|
|
167
|
+
// Tool Approval
|
|
168
|
+
requiresApproval: event.requiresApproval || false,
|
|
169
|
+
approvalScope: null, // Set when approval is granted
|
|
170
|
+
approvedAt: null,
|
|
171
|
+
approvedBy: null,
|
|
172
|
+
deniedAt: null,
|
|
173
|
+
denialReason: null,
|
|
174
|
+
// Performance (for agent messages with LLM usage)
|
|
175
|
+
llmTokensInput: event.usage?.inputTokens || null,
|
|
176
|
+
llmTokensOutput: event.usage?.outputTokens || null,
|
|
177
|
+
llmLatency: null, // Can be calculated from SSE start/end events
|
|
178
|
+
llmProvider: null, // Not available in message event
|
|
179
|
+
llmModel: null,
|
|
180
|
+
// UI State
|
|
181
|
+
hasToolCalls,
|
|
182
|
+
toolCallCount
|
|
183
|
+
},
|
|
184
|
+
createdAt: event.timestamp
|
|
185
|
+
};
|
|
186
|
+
return persistEvent(eventData);
|
|
187
|
+
};
|
|
188
|
+
// SSE event handler - persist only start and end events
|
|
189
|
+
const sseHandler = (event) => {
|
|
190
|
+
// Only persist start and end events, not chunk events
|
|
191
|
+
if (event.type !== 'start' && event.type !== 'end') {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Make ID unique by combining messageId with event type
|
|
195
|
+
const eventData = {
|
|
196
|
+
id: `${event.messageId}-sse-${event.type}`,
|
|
197
|
+
worldId: world.id,
|
|
198
|
+
chatId: world.currentChatId || null, // Default to current chat
|
|
199
|
+
type: 'sse',
|
|
200
|
+
payload: {
|
|
201
|
+
agentName: event.agentName,
|
|
202
|
+
type: event.type,
|
|
203
|
+
content: event.content,
|
|
204
|
+
error: event.error,
|
|
205
|
+
usage: event.usage,
|
|
206
|
+
logEvent: event.logEvent
|
|
207
|
+
},
|
|
208
|
+
meta: {
|
|
209
|
+
agentName: event.agentName,
|
|
210
|
+
sseType: event.type
|
|
211
|
+
},
|
|
212
|
+
createdAt: new Date()
|
|
213
|
+
};
|
|
214
|
+
return persistEvent(eventData);
|
|
215
|
+
};
|
|
216
|
+
// Tool event persistence (world channel)
|
|
217
|
+
// Handles WorldToolEvent (tool execution) and WorldActivityEventPayload (activity tracking)
|
|
218
|
+
const toolHandler = (event) => {
|
|
219
|
+
// Check event type category
|
|
220
|
+
const isActivityEvent = event.type && ['response-start', 'response-end', 'idle'].includes(event.type);
|
|
221
|
+
const isToolEvent = event.type && ['tool-start', 'tool-result', 'tool-error', 'tool-progress'].includes(event.type);
|
|
222
|
+
// Validate required fields for tool events only
|
|
223
|
+
if (isToolEvent) {
|
|
224
|
+
if (!event.messageId) {
|
|
225
|
+
loggerPublish.error('Tool event missing required messageId', {
|
|
226
|
+
worldId: world.id,
|
|
227
|
+
eventType: event.type,
|
|
228
|
+
agentName: event.agentName
|
|
229
|
+
});
|
|
230
|
+
return; // Skip persistence for invalid events
|
|
231
|
+
}
|
|
232
|
+
if (!event.agentName) {
|
|
233
|
+
loggerPublish.error('Tool event missing required agentName', {
|
|
234
|
+
worldId: world.id,
|
|
235
|
+
eventType: event.type,
|
|
236
|
+
messageId: event.messageId
|
|
237
|
+
});
|
|
238
|
+
return; // Skip persistence for invalid events
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Generate unique ID for tool events by combining messageId with tool type
|
|
242
|
+
// This prevents duplicate ID conflicts when multiple tool events (tool-start, tool-result, tool-error)
|
|
243
|
+
// share the same messageId
|
|
244
|
+
const eventId = isActivityEvent
|
|
245
|
+
? event.messageId // Activity events already have unique messageIds
|
|
246
|
+
: `${event.messageId}-tool-${event.type}`; // Tool events need type suffix for uniqueness
|
|
247
|
+
const eventData = {
|
|
248
|
+
id: eventId,
|
|
249
|
+
worldId: world.id,
|
|
250
|
+
chatId: world.currentChatId || null, // Default to current chat
|
|
251
|
+
type: isActivityEvent ? 'world' : 'tool',
|
|
252
|
+
payload: isActivityEvent ? {
|
|
253
|
+
activityType: event.type,
|
|
254
|
+
pendingOperations: event.pendingOperations,
|
|
255
|
+
activityId: event.activityId,
|
|
256
|
+
source: event.source,
|
|
257
|
+
activeSources: event.activeSources,
|
|
258
|
+
timestamp: event.timestamp
|
|
259
|
+
} : {
|
|
260
|
+
agentName: event.agentName,
|
|
261
|
+
type: event.type,
|
|
262
|
+
toolExecution: event.toolExecution
|
|
263
|
+
},
|
|
264
|
+
meta: isActivityEvent ? {
|
|
265
|
+
activityType: event.type,
|
|
266
|
+
source: event.source
|
|
267
|
+
} : {
|
|
268
|
+
agentName: event.agentName,
|
|
269
|
+
toolType: event.type,
|
|
270
|
+
// Required metadata for tool events
|
|
271
|
+
ownerAgentId: event.agentName,
|
|
272
|
+
triggeredByMessageId: event.messageId,
|
|
273
|
+
executionDuration: event.toolExecution?.duration ?? 0,
|
|
274
|
+
resultSize: event.toolExecution?.resultSize ?? 0,
|
|
275
|
+
wasApproved: false // Default, should be updated if approval tracking is needed
|
|
276
|
+
},
|
|
277
|
+
createdAt: isActivityEvent ? new Date(event.timestamp) : new Date()
|
|
278
|
+
};
|
|
279
|
+
return persistEvent(eventData);
|
|
280
|
+
};
|
|
281
|
+
// System event persistence
|
|
282
|
+
const systemHandler = (event) => {
|
|
283
|
+
const eventData = {
|
|
284
|
+
id: event.messageId,
|
|
285
|
+
worldId: world.id,
|
|
286
|
+
chatId: event.chatId !== undefined ? event.chatId : (world.currentChatId || null), // Default to current chat
|
|
287
|
+
type: 'system',
|
|
288
|
+
payload: event.content,
|
|
289
|
+
meta: {},
|
|
290
|
+
createdAt: event.timestamp
|
|
291
|
+
};
|
|
292
|
+
return persistEvent(eventData);
|
|
293
|
+
};
|
|
294
|
+
// CRUD event persistence
|
|
295
|
+
const crudHandler = (event) => {
|
|
296
|
+
const eventData = {
|
|
297
|
+
id: `crud-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
298
|
+
worldId: world.id,
|
|
299
|
+
chatId: event.chatId || null,
|
|
300
|
+
type: 'crud',
|
|
301
|
+
payload: {
|
|
302
|
+
operation: event.operation,
|
|
303
|
+
entityType: event.entityType,
|
|
304
|
+
entityId: event.entityId,
|
|
305
|
+
entityData: event.entityData,
|
|
306
|
+
timestamp: event.timestamp
|
|
307
|
+
},
|
|
308
|
+
meta: {
|
|
309
|
+
operation: event.operation,
|
|
310
|
+
entityType: event.entityType,
|
|
311
|
+
entityId: event.entityId
|
|
312
|
+
},
|
|
313
|
+
createdAt: event.timestamp
|
|
314
|
+
};
|
|
315
|
+
return persistEvent(eventData);
|
|
316
|
+
};
|
|
317
|
+
// Attach listeners
|
|
318
|
+
world.eventEmitter.on('message', messageHandler);
|
|
319
|
+
world.eventEmitter.on('sse', sseHandler);
|
|
320
|
+
world.eventEmitter.on('world', toolHandler);
|
|
321
|
+
world.eventEmitter.on('system', systemHandler);
|
|
322
|
+
world.eventEmitter.on(EventType.CRUD, crudHandler);
|
|
323
|
+
loggerPublish.debug('Event persistence setup complete', {
|
|
324
|
+
worldId: world.id
|
|
325
|
+
});
|
|
326
|
+
// Return cleanup function
|
|
327
|
+
return () => {
|
|
328
|
+
world.eventEmitter.off('message', messageHandler);
|
|
329
|
+
world.eventEmitter.off('sse', sseHandler);
|
|
330
|
+
world.eventEmitter.off('world', toolHandler);
|
|
331
|
+
world.eventEmitter.off('system', systemHandler);
|
|
332
|
+
world.eventEmitter.off(EventType.CRUD, crudHandler);
|
|
333
|
+
loggerPublish.debug('Event persistence listeners cleaned up', { worldId: world.id });
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Publish event to a specific channel using World.eventEmitter
|
|
338
|
+
*/
|
|
339
|
+
export function publishEvent(world, type, content) {
|
|
340
|
+
const event = {
|
|
341
|
+
content,
|
|
342
|
+
timestamp: new Date(),
|
|
343
|
+
messageId: generateId(),
|
|
344
|
+
chatId: world.currentChatId || null
|
|
345
|
+
};
|
|
346
|
+
world.eventEmitter.emit(type, event);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Message publishing using World.eventEmitter with chat session management
|
|
350
|
+
* Parses enhanced string protocol and automatically prepends @mention if agentId detected
|
|
351
|
+
* Returns the messageEvent so callers can access the generated messageId
|
|
352
|
+
*
|
|
353
|
+
* @param chatId - Optional chat ID. If not provided, uses world.currentChatId
|
|
354
|
+
* @param replyToMessageId - Optional parent message ID for threading
|
|
355
|
+
*/
|
|
356
|
+
export function publishMessage(world, content, sender, chatId, replyToMessageId) {
|
|
357
|
+
const messageId = generateId();
|
|
358
|
+
const targetChatId = chatId !== undefined ? chatId : world.currentChatId;
|
|
359
|
+
// Parse enhanced string protocol to extract targetAgentId
|
|
360
|
+
const { targetAgentId } = parseMessageContent(content, 'user');
|
|
361
|
+
// Prepend @mention if agentId is present in enhanced protocol
|
|
362
|
+
let finalContent = content;
|
|
363
|
+
if (targetAgentId) {
|
|
364
|
+
finalContent = `@${targetAgentId}, ${content}`;
|
|
365
|
+
loggerMemory.debug('[publishMessage] Prepended @mention from enhanced protocol', {
|
|
366
|
+
agentId: targetAgentId,
|
|
367
|
+
messageId
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const messageEvent = {
|
|
371
|
+
content: finalContent,
|
|
372
|
+
sender,
|
|
373
|
+
timestamp: new Date(),
|
|
374
|
+
messageId,
|
|
375
|
+
chatId: targetChatId,
|
|
376
|
+
replyToMessageId
|
|
377
|
+
};
|
|
378
|
+
loggerMemory.debug('[publishMessage] Generated messageId', {
|
|
379
|
+
messageId,
|
|
380
|
+
sender,
|
|
381
|
+
worldId: world.id,
|
|
382
|
+
chatId: targetChatId,
|
|
383
|
+
hasAgentId: !!targetAgentId,
|
|
384
|
+
contentPreview: finalContent.substring(0, 50)
|
|
385
|
+
});
|
|
386
|
+
world.eventEmitter.emit('message', messageEvent);
|
|
387
|
+
return messageEvent;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Message publishing with pre-generated messageId
|
|
391
|
+
* Used when messageId needs to be known before publishing (e.g., for agent responses)
|
|
392
|
+
*
|
|
393
|
+
* @param chatId - Optional chat ID. If not provided, uses world.currentChatId
|
|
394
|
+
* @param replyToMessageId - Optional parent message ID for threading
|
|
395
|
+
*/
|
|
396
|
+
export function publishMessageWithId(world, content, sender, messageId, chatId, replyToMessageId) {
|
|
397
|
+
const targetChatId = chatId !== undefined ? chatId : world.currentChatId;
|
|
398
|
+
const messageEvent = {
|
|
399
|
+
content,
|
|
400
|
+
sender,
|
|
401
|
+
timestamp: new Date(),
|
|
402
|
+
messageId,
|
|
403
|
+
chatId: targetChatId,
|
|
404
|
+
replyToMessageId
|
|
405
|
+
};
|
|
406
|
+
world.eventEmitter.emit('message', messageEvent);
|
|
407
|
+
return messageEvent;
|
|
408
|
+
}
|
|
409
|
+
export function subscribeToMessages(world, handler) {
|
|
410
|
+
world.eventEmitter.on('message', handler);
|
|
411
|
+
return () => world.eventEmitter.off('message', handler);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* SSE events using World.eventEmitter (for LLM streaming)
|
|
415
|
+
*/
|
|
416
|
+
export function publishSSE(world, data) {
|
|
417
|
+
const sseEvent = {
|
|
418
|
+
agentName: data.agentName,
|
|
419
|
+
type: data.type,
|
|
420
|
+
content: data.content,
|
|
421
|
+
error: data.error,
|
|
422
|
+
messageId: data.messageId || generateId(),
|
|
423
|
+
usage: data.usage,
|
|
424
|
+
logEvent: data.logEvent
|
|
425
|
+
};
|
|
426
|
+
world.eventEmitter.emit('sse', sseEvent);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Tool events using World.eventEmitter (for agent behavioral events)
|
|
430
|
+
*/
|
|
431
|
+
export function publishToolEvent(world, data) {
|
|
432
|
+
const toolEvent = {
|
|
433
|
+
agentName: data.agentName,
|
|
434
|
+
type: data.type,
|
|
435
|
+
messageId: data.messageId || generateId(),
|
|
436
|
+
toolExecution: data.toolExecution
|
|
437
|
+
};
|
|
438
|
+
world.eventEmitter.emit('world', toolEvent);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Publish approval request event
|
|
442
|
+
* Used when a tool requires approval before execution
|
|
443
|
+
* Note: This function is legacy - approval requests now use direct message events
|
|
444
|
+
* with OpenAI tool call protocol (see tool-utils.ts)
|
|
445
|
+
*/
|
|
446
|
+
export function publishApprovalRequest(world, approvalRequest, agentId, messageId) {
|
|
447
|
+
const approvalEvent = {
|
|
448
|
+
type: 'approval_request',
|
|
449
|
+
agentId,
|
|
450
|
+
messageId,
|
|
451
|
+
approvalRequest,
|
|
452
|
+
timestamp: new Date().toISOString()
|
|
453
|
+
};
|
|
454
|
+
// Emit as approval event for legacy compatibility
|
|
455
|
+
world.eventEmitter.emit('approval', approvalEvent);
|
|
456
|
+
// Note: SSE events are for streaming only, not for tool messages
|
|
457
|
+
// Approval requests should use message events with OpenAI tool call format
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* SSE subscription using World.eventEmitter
|
|
461
|
+
*/
|
|
462
|
+
export function subscribeToSSE(world, handler) {
|
|
463
|
+
world.eventEmitter.on('sse', handler);
|
|
464
|
+
return () => world.eventEmitter.off('sse', handler);
|
|
465
|
+
}
|
|
466
|
+
// Check if response has any mention at paragraph beginning (prevents auto-mention loops)
|
|
467
|
+
export function hasAnyMentionAtBeginning(response) {
|
|
468
|
+
if (!response?.trim())
|
|
469
|
+
return false;
|
|
470
|
+
const result = extractParagraphBeginningMentions(response).length > 0;
|
|
471
|
+
loggerAutoMention.debug('Checking for mentions at beginning', { response: response.substring(0, 100), hasMentions: result });
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
// Remove all mentions from paragraph beginnings (including commas and spaces)
|
|
475
|
+
export function removeMentionsFromParagraphBeginnings(text, specificMention) {
|
|
476
|
+
if (!text?.trim())
|
|
477
|
+
return text;
|
|
478
|
+
const lines = text.split('\n');
|
|
479
|
+
const processedLines = lines.map(line => {
|
|
480
|
+
const trimmed = line.trimStart();
|
|
481
|
+
let cleaned = trimmed;
|
|
482
|
+
if (specificMention) {
|
|
483
|
+
// For specific mentions, escape special regex characters and handle consecutive mentions
|
|
484
|
+
const escapedMention = specificMention.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
485
|
+
// Pattern to match @mention followed by optional comma/space combinations
|
|
486
|
+
const mentionPattern = new RegExp(`^@${escapedMention}(?:[,\\s]+|$)`, 'gi');
|
|
487
|
+
// Keep removing mentions from the beginning until no more are found
|
|
488
|
+
while (mentionPattern.test(cleaned)) {
|
|
489
|
+
cleaned = cleaned.replace(mentionPattern, '');
|
|
490
|
+
mentionPattern.lastIndex = 0; // Reset regex for next iteration
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
// For any mentions
|
|
495
|
+
const mentionPattern = /^@\w+(?:[-_]\w+)*(?:[,\s]+|$)/;
|
|
496
|
+
// Keep removing mentions from the beginning until no more are found
|
|
497
|
+
while (mentionPattern.test(cleaned)) {
|
|
498
|
+
cleaned = cleaned.replace(mentionPattern, '');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const leadingWhitespace = line.match(/^(\s*)/)?.[1] || '';
|
|
502
|
+
return leadingWhitespace + cleaned;
|
|
503
|
+
});
|
|
504
|
+
return processedLines.join('\n');
|
|
505
|
+
}
|
|
506
|
+
// Add auto-mention at beginning if no existing mentions (prevents loops)
|
|
507
|
+
// Supports world tags: <world>STOP|DONE|PASS</world> and <world>TO: a,b,c</world>
|
|
508
|
+
export function addAutoMention(response, sender) {
|
|
509
|
+
if (!response?.trim() || !sender) {
|
|
510
|
+
return response;
|
|
511
|
+
}
|
|
512
|
+
loggerAutoMention.debug('Processing auto-mention', { sender, responseStart: response.substring(0, 100) });
|
|
513
|
+
// Consolidated regex patterns for world tags (case insensitive)
|
|
514
|
+
const worldTagPattern = /<world>(STOP|DONE|PASS|TO:\s*([^<]*))<\/world>/gi;
|
|
515
|
+
let match;
|
|
516
|
+
let processedResponse = response;
|
|
517
|
+
while ((match = worldTagPattern.exec(response)) !== null) {
|
|
518
|
+
const [fullMatch, action, toRecipients] = match;
|
|
519
|
+
loggerAutoMention.debug('Found world tag', { action, toRecipients, fullMatch });
|
|
520
|
+
// Remove the world tag from response
|
|
521
|
+
processedResponse = processedResponse.replace(fullMatch, '');
|
|
522
|
+
const upperAction = action.toUpperCase();
|
|
523
|
+
if (upperAction === 'STOP' || upperAction === 'DONE' || upperAction === 'PASS') {
|
|
524
|
+
// Stop tags prevent auto-mention and remove ALL mentions at beginning of paragraphs
|
|
525
|
+
loggerAutoMention.debug('Processing STOP/DONE/PASS tag - removing mentions');
|
|
526
|
+
const cleanResponse = processedResponse.trim();
|
|
527
|
+
return removeMentionsFromParagraphBeginnings(cleanResponse).trim();
|
|
528
|
+
}
|
|
529
|
+
else if (upperAction.startsWith('TO:')) {
|
|
530
|
+
// TO tag with recipients - also remove existing mentions
|
|
531
|
+
const recipients = toRecipients?.split(',').map(name => name.trim()).filter(name => name) || [];
|
|
532
|
+
loggerAutoMention.debug('Processing TO tag', { recipients });
|
|
533
|
+
// Remove existing mentions from the response
|
|
534
|
+
const cleanResponse = removeMentionsFromParagraphBeginnings(processedResponse.trim()).trim();
|
|
535
|
+
if (recipients.length > 0) {
|
|
536
|
+
const mentions = recipients.map(recipient => `@${recipient}`).join('\n');
|
|
537
|
+
const result = `${mentions}\n\n${cleanResponse}`;
|
|
538
|
+
loggerAutoMention.debug('Added TO tag mentions', { mentions, result: result.substring(0, 100) });
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
// Empty TO tag - fall back to normal auto-mention behavior
|
|
543
|
+
loggerAutoMention.debug('Empty TO tag - falling back to normal auto-mention');
|
|
544
|
+
if (hasAnyMentionAtBeginning(cleanResponse)) {
|
|
545
|
+
return cleanResponse;
|
|
546
|
+
}
|
|
547
|
+
return `@${sender} ${cleanResponse}`;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} // Existing logic: add auto-mention if no existing mentions at beginning
|
|
551
|
+
if (hasAnyMentionAtBeginning(processedResponse)) {
|
|
552
|
+
loggerAutoMention.debug('Response already has mentions at beginning - no auto-mention needed');
|
|
553
|
+
return processedResponse;
|
|
554
|
+
}
|
|
555
|
+
const result = `@${sender} ${processedResponse.trim()}`;
|
|
556
|
+
loggerAutoMention.debug('Added auto-mention', { sender, result: result.substring(0, 100) });
|
|
557
|
+
return result;
|
|
558
|
+
}
|
|
559
|
+
// Get valid mentions excluding self-mentions (case-insensitive)
|
|
560
|
+
export function getValidMentions(response, agentId) {
|
|
561
|
+
if (!response?.trim() || !agentId)
|
|
562
|
+
return [];
|
|
563
|
+
return extractParagraphBeginningMentions(response)
|
|
564
|
+
.filter(mention => mention.toLowerCase() !== agentId.toLowerCase());
|
|
565
|
+
}
|
|
566
|
+
// Determine if agent should auto-mention sender (no valid mentions in response)
|
|
567
|
+
// Auto-mention is used to target responses and prevent unintended broadcasting
|
|
568
|
+
export function shouldAutoMention(response, sender, agentId) {
|
|
569
|
+
if (!response?.trim() || !sender || !agentId)
|
|
570
|
+
return false;
|
|
571
|
+
if (determineSenderType(sender) === SenderType.HUMAN)
|
|
572
|
+
return false;
|
|
573
|
+
if (sender.toLowerCase() === agentId.toLowerCase())
|
|
574
|
+
return false;
|
|
575
|
+
// Check if response already has valid mentions (excluding self)
|
|
576
|
+
return getValidMentions(response, agentId).length === 0;
|
|
577
|
+
}
|
|
578
|
+
// Remove consecutive self-mentions from response beginning (case-insensitive)
|
|
579
|
+
export function removeSelfMentions(response, agentId) {
|
|
580
|
+
if (!response || !agentId)
|
|
581
|
+
return response;
|
|
582
|
+
const trimmedResponse = response.trim();
|
|
583
|
+
if (!trimmedResponse)
|
|
584
|
+
return response;
|
|
585
|
+
loggerAutoMention.debug('Removing self-mentions', { agentId, responseStart: response.substring(0, 100) });
|
|
586
|
+
// Use the helper function to remove self-mentions
|
|
587
|
+
const result = removeMentionsFromParagraphBeginnings(trimmedResponse, agentId);
|
|
588
|
+
loggerAutoMention.debug('Self-mention removal result', {
|
|
589
|
+
agentId,
|
|
590
|
+
before: trimmedResponse.substring(0, 100),
|
|
591
|
+
after: result.substring(0, 100),
|
|
592
|
+
changed: trimmedResponse !== result
|
|
593
|
+
});
|
|
594
|
+
// Preserve original leading whitespace
|
|
595
|
+
const originalMatch = response.match(/^(\s*)/);
|
|
596
|
+
const originalLeadingWhitespace = originalMatch ? originalMatch[1] : '';
|
|
597
|
+
return originalLeadingWhitespace + result;
|
|
598
|
+
} /**
|
|
599
|
+
* Agent subscription with automatic message processing
|
|
600
|
+
*/
|
|
601
|
+
export function subscribeAgentToMessages(world, agent) {
|
|
602
|
+
const handler = async (messageEvent) => {
|
|
603
|
+
loggerAgent.debug('Agent received message', {
|
|
604
|
+
agentId: agent.id,
|
|
605
|
+
sender: messageEvent.sender,
|
|
606
|
+
messageId: messageEvent.messageId
|
|
607
|
+
});
|
|
608
|
+
if (!messageEvent.messageId) {
|
|
609
|
+
loggerAgent.error('Received message WITHOUT messageId', {
|
|
610
|
+
agentId: agent.id,
|
|
611
|
+
sender: messageEvent.sender,
|
|
612
|
+
worldId: world.id
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
// Check if this is an assistant message with tool_calls (approval request)
|
|
616
|
+
// These need to be saved to agent memory even though they're from the agent
|
|
617
|
+
const messageData = messageEvent;
|
|
618
|
+
if (messageData.role === 'assistant' && messageData.tool_calls && messageEvent.sender === agent.id) {
|
|
619
|
+
loggerMemory.debug('Saving approval request to agent memory', {
|
|
620
|
+
agentId: agent.id,
|
|
621
|
+
messageId: messageEvent.messageId,
|
|
622
|
+
toolCalls: messageData.tool_calls.length
|
|
623
|
+
});
|
|
624
|
+
const approvalMessage = {
|
|
625
|
+
role: 'assistant',
|
|
626
|
+
content: messageEvent.content || '',
|
|
627
|
+
sender: agent.id,
|
|
628
|
+
createdAt: messageEvent.timestamp,
|
|
629
|
+
chatId: world.currentChatId || null,
|
|
630
|
+
messageId: messageEvent.messageId,
|
|
631
|
+
replyToMessageId: messageData.replyToMessageId,
|
|
632
|
+
tool_calls: messageData.tool_calls,
|
|
633
|
+
agentId: agent.id
|
|
634
|
+
};
|
|
635
|
+
agent.memory.push(approvalMessage);
|
|
636
|
+
// Auto-save agent memory
|
|
637
|
+
try {
|
|
638
|
+
const storage = await getStorageWrappers();
|
|
639
|
+
await storage.saveAgent(world.id, agent);
|
|
640
|
+
loggerMemory.debug('Approval request saved to agent memory', {
|
|
641
|
+
agentId: agent.id,
|
|
642
|
+
messageId: messageEvent.messageId
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
loggerMemory.error('Failed to save approval request to memory', {
|
|
647
|
+
agentId: agent.id,
|
|
648
|
+
error: error instanceof Error ? error.message : error
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
return; // Don't process this message further
|
|
652
|
+
}
|
|
653
|
+
// Check if this is a tool result message (approval response)
|
|
654
|
+
// These need to be saved to agent memory for persistence
|
|
655
|
+
if (messageData.role === 'tool' && messageData.tool_call_id) {
|
|
656
|
+
loggerMemory.debug('Saving approval response to agent memory', {
|
|
657
|
+
agentId: agent.id,
|
|
658
|
+
messageId: messageEvent.messageId,
|
|
659
|
+
toolCallId: messageData.tool_call_id
|
|
660
|
+
});
|
|
661
|
+
const approvalResponse = {
|
|
662
|
+
role: 'tool',
|
|
663
|
+
content: messageEvent.content || '',
|
|
664
|
+
sender: messageEvent.sender || 'system',
|
|
665
|
+
createdAt: messageEvent.timestamp,
|
|
666
|
+
chatId: world.currentChatId || null,
|
|
667
|
+
messageId: messageEvent.messageId,
|
|
668
|
+
tool_call_id: messageData.tool_call_id,
|
|
669
|
+
agentId: agent.id
|
|
670
|
+
};
|
|
671
|
+
agent.memory.push(approvalResponse);
|
|
672
|
+
// Auto-save agent memory
|
|
673
|
+
try {
|
|
674
|
+
const storage = await getStorageWrappers();
|
|
675
|
+
await storage.saveAgent(world.id, agent);
|
|
676
|
+
loggerMemory.debug('Approval response saved to agent memory', {
|
|
677
|
+
agentId: agent.id,
|
|
678
|
+
messageId: messageEvent.messageId
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
loggerMemory.error('Failed to save approval response to memory', {
|
|
683
|
+
agentId: agent.id,
|
|
684
|
+
error: error instanceof Error ? error.message : error
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
return; // Don't process this message further
|
|
688
|
+
}
|
|
689
|
+
// Skip messages from this agent itself
|
|
690
|
+
if (messageEvent.sender === agent.id) {
|
|
691
|
+
loggerAgent.debug('Skipping own message in handler', { agentId: agent.id, sender: messageEvent.sender });
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
// Reset LLM call count if needed (for human/system messages)
|
|
695
|
+
await resetLLMCallCountIfNeeded(world, agent, messageEvent);
|
|
696
|
+
// Process message if agent should respond
|
|
697
|
+
loggerResponse.debug('Checking if agent should respond', { agentId: agent.id, sender: messageEvent.sender });
|
|
698
|
+
const shouldRespond = await shouldAgentRespond(world, agent, messageEvent);
|
|
699
|
+
if (shouldRespond) {
|
|
700
|
+
// Save incoming messages to agent memory only when they plan to respond
|
|
701
|
+
await saveIncomingMessageToMemory(world, agent, messageEvent);
|
|
702
|
+
loggerAgent.debug('Agent will respond - processing message', { agentId: agent.id, sender: messageEvent.sender });
|
|
703
|
+
await processAgentMessage(world, agent, messageEvent);
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
loggerAgent.debug('Agent will NOT respond - skipping memory save and SSE publishing', {
|
|
707
|
+
agentId: agent.id,
|
|
708
|
+
sender: messageEvent.sender
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
return subscribeToMessages(world, handler);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Save incoming message to agent memory with auto-save
|
|
716
|
+
*/
|
|
717
|
+
export async function saveIncomingMessageToMemory(world, agent, messageEvent) {
|
|
718
|
+
try {
|
|
719
|
+
if (messageEvent.sender?.toLowerCase() === agent.id.toLowerCase())
|
|
720
|
+
return;
|
|
721
|
+
if (!messageEvent.messageId) {
|
|
722
|
+
loggerMemory.error('Message missing messageId', {
|
|
723
|
+
agentId: agent.id,
|
|
724
|
+
sender: messageEvent.sender,
|
|
725
|
+
worldId: world.id
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
if (!world.currentChatId) {
|
|
729
|
+
loggerMemory.warn('Saving message without chatId', {
|
|
730
|
+
agentId: agent.id,
|
|
731
|
+
messageId: messageEvent.messageId
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
// Parse message content to detect enhanced format (e.g., tool results)
|
|
735
|
+
const { message: parsedMessage } = parseMessageContent(messageEvent.content, 'user');
|
|
736
|
+
const userMessage = {
|
|
737
|
+
...parsedMessage,
|
|
738
|
+
sender: messageEvent.sender,
|
|
739
|
+
createdAt: messageEvent.timestamp,
|
|
740
|
+
chatId: world.currentChatId || null,
|
|
741
|
+
messageId: messageEvent.messageId,
|
|
742
|
+
replyToMessageId: messageEvent.replyToMessageId,
|
|
743
|
+
agentId: agent.id
|
|
744
|
+
};
|
|
745
|
+
agent.memory.push(userMessage);
|
|
746
|
+
try {
|
|
747
|
+
const storage = await getStorageWrappers();
|
|
748
|
+
await storage.saveAgent(world.id, agent);
|
|
749
|
+
loggerMemory.debug('Agent saved successfully', {
|
|
750
|
+
agentId: agent.id,
|
|
751
|
+
messageId: messageEvent.messageId
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
loggerMemory.error('Failed to auto-save memory', { agentId: agent.id, error: error instanceof Error ? error.message : error });
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
loggerMemory.error('Could not save incoming message to memory', { agentId: agent.id, error: error instanceof Error ? error.message : error });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Agent message processing with LLM response generation and auto-mention logic
|
|
764
|
+
*/
|
|
765
|
+
export async function processAgentMessage(world, agent, messageEvent) {
|
|
766
|
+
const completeActivity = beginWorldActivity(world, `agent:${agent.id}`);
|
|
767
|
+
try {
|
|
768
|
+
// Load conversation history from storage for current chat (last 10 messages)
|
|
769
|
+
// NOTE: Don't save incoming message yet to avoid duplication in prepareMessagesForLLM
|
|
770
|
+
let conversationHistory = [];
|
|
771
|
+
try {
|
|
772
|
+
const storage = await getStorageWrappers();
|
|
773
|
+
const allMessages = await storage.getMemory(world.id, world.currentChatId);
|
|
774
|
+
conversationHistory = allMessages.slice(-10); // Get last 10 messages for current chat
|
|
775
|
+
}
|
|
776
|
+
catch (error) {
|
|
777
|
+
loggerMemory.error('Could not load conversation history from storage', { agentId: agent.id, chatId: world.currentChatId, error: error instanceof Error ? error.message : error });
|
|
778
|
+
}
|
|
779
|
+
// Prepare messages for LLM with history + current message
|
|
780
|
+
const messageData = {
|
|
781
|
+
id: messageEvent.messageId || generateId(),
|
|
782
|
+
name: 'message',
|
|
783
|
+
sender: messageEvent.sender,
|
|
784
|
+
content: messageEvent.content,
|
|
785
|
+
payload: {}
|
|
786
|
+
};
|
|
787
|
+
const messages = prepareMessagesForLLM(agent, messageData, conversationHistory);
|
|
788
|
+
// Note: Incoming message already saved in subscribeAgentToMessages handler
|
|
789
|
+
// Increment LLM call count and save agent state
|
|
790
|
+
agent.llmCallCount++;
|
|
791
|
+
agent.lastLLMCall = new Date();
|
|
792
|
+
try {
|
|
793
|
+
const storage = await getStorageWrappers();
|
|
794
|
+
await storage.saveAgent(world.id, agent);
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
loggerAgent.error('Failed to auto-save agent after LLM call increment', { agentId: agent.id, error: error instanceof Error ? error.message : error });
|
|
798
|
+
}
|
|
799
|
+
// Generate LLM response (streaming or non-streaming)
|
|
800
|
+
let response;
|
|
801
|
+
let messageId;
|
|
802
|
+
if (globalStreamingEnabled) {
|
|
803
|
+
const { streamAgentResponse } = await import('./llm-manager.js');
|
|
804
|
+
const result = await streamAgentResponse(world, agent, messages, publishSSE);
|
|
805
|
+
response = result.response;
|
|
806
|
+
messageId = result.messageId; // Use the same messageId from streaming
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
const { generateAgentResponse } = await import('./llm-manager.js');
|
|
810
|
+
response = await generateAgentResponse(world, agent, messages);
|
|
811
|
+
messageId = generateId(); // Generate new ID for non-streaming
|
|
812
|
+
}
|
|
813
|
+
if (!response) {
|
|
814
|
+
// Empty response could mean approval request was sent or actual error
|
|
815
|
+
// For approval requests, this is normal behavior - just return silently
|
|
816
|
+
loggerAgent.debug('LLM response is empty - could be approval request or error', { agentId: agent.id });
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
// Process auto-mention logic: remove self-mentions, then add auto-mention if needed
|
|
820
|
+
let finalResponse = removeSelfMentions(response, agent.id);
|
|
821
|
+
if (shouldAutoMention(finalResponse, messageEvent.sender, agent.id)) {
|
|
822
|
+
finalResponse = addAutoMention(finalResponse, messageEvent.sender);
|
|
823
|
+
}
|
|
824
|
+
if (!messageEvent.messageId) {
|
|
825
|
+
loggerMemory.error('messageEvent.messageId required for threading', {
|
|
826
|
+
agentId: agent.id,
|
|
827
|
+
sender: messageEvent.sender
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
// Save final response to memory with pre-generated ID and parent link
|
|
831
|
+
const assistantMessage = {
|
|
832
|
+
role: 'assistant',
|
|
833
|
+
content: finalResponse,
|
|
834
|
+
createdAt: new Date(),
|
|
835
|
+
chatId: world.currentChatId || null,
|
|
836
|
+
messageId: messageId,
|
|
837
|
+
replyToMessageId: messageEvent.messageId, // Link to message we're replying to
|
|
838
|
+
sender: agent.id, // Add sender field for consistency
|
|
839
|
+
agentId: agent.id
|
|
840
|
+
};
|
|
841
|
+
// Validate threading before saving
|
|
842
|
+
try {
|
|
843
|
+
const { validateMessageThreading } = await import('./types.js');
|
|
844
|
+
const validationContext = [...agent.memory, assistantMessage];
|
|
845
|
+
validateMessageThreading(assistantMessage, validationContext);
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
loggerMemory.error('Threading validation failed', {
|
|
849
|
+
agentId: agent.id,
|
|
850
|
+
messageId: assistantMessage.messageId,
|
|
851
|
+
error: error instanceof Error ? error.message : error
|
|
852
|
+
});
|
|
853
|
+
// Clear threading for critical errors (self-reference, circular, depth exceeded)
|
|
854
|
+
if (error instanceof Error &&
|
|
855
|
+
(error.message.includes('cannot reply to itself') ||
|
|
856
|
+
error.message.includes('Circular reference detected') ||
|
|
857
|
+
error.message.includes('Thread depth exceeds maximum'))) {
|
|
858
|
+
loggerMemory.warn('Clearing threading due to critical error', {
|
|
859
|
+
agentId: agent.id,
|
|
860
|
+
error: error.message
|
|
861
|
+
});
|
|
862
|
+
assistantMessage.replyToMessageId = undefined;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
agent.memory.push(assistantMessage);
|
|
866
|
+
// Publish final response with pre-generated messageId and threading info
|
|
867
|
+
if (finalResponse && typeof finalResponse === 'string') {
|
|
868
|
+
publishMessageWithId(world, finalResponse, agent.id, messageId, world.currentChatId, messageEvent.messageId);
|
|
869
|
+
}
|
|
870
|
+
// Auto-save memory after adding response (now with correct messageId)
|
|
871
|
+
try {
|
|
872
|
+
const storage = await getStorageWrappers();
|
|
873
|
+
await storage.saveAgent(world.id, agent);
|
|
874
|
+
}
|
|
875
|
+
catch (error) {
|
|
876
|
+
loggerMemory.error('Failed to auto-save memory after response', { agentId: agent.id, error: error instanceof Error ? error.message : error });
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
loggerAgent.error('Agent failed to process message', { agentId: agent.id, error: error instanceof Error ? error.message : error });
|
|
881
|
+
publishEvent(world, 'system', { message: `[Error] ${error.message}`, type: 'error' });
|
|
882
|
+
}
|
|
883
|
+
finally {
|
|
884
|
+
completeActivity();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Reset LLM call count for human/world messages with persistence
|
|
889
|
+
*/
|
|
890
|
+
export async function resetLLMCallCountIfNeeded(world, agent, messageEvent) {
|
|
891
|
+
const senderType = determineSenderType(messageEvent.sender);
|
|
892
|
+
if ((senderType === SenderType.HUMAN || senderType === SenderType.WORLD) && agent.llmCallCount > 0) {
|
|
893
|
+
loggerTurnLimit.debug('Resetting LLM call count', { agentId: agent.id, oldCount: agent.llmCallCount });
|
|
894
|
+
agent.llmCallCount = 0;
|
|
895
|
+
try {
|
|
896
|
+
const storage = await getStorageWrappers();
|
|
897
|
+
await storage.saveAgent(world.id, agent);
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
loggerTurnLimit.warn('Failed to auto-save agent after turn limit reset', { agentId: agent.id, error: error instanceof Error ? error.message : error });
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Enhanced message filtering logic with turn limits and mention detection
|
|
906
|
+
*/
|
|
907
|
+
export async function shouldAgentRespond(world, agent, messageEvent) {
|
|
908
|
+
// Never respond to own messages
|
|
909
|
+
if (messageEvent.sender?.toLowerCase() === agent.id.toLowerCase()) {
|
|
910
|
+
loggerResponse.debug('Skipping own message', { agentId: agent.id, sender: messageEvent.sender });
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
const content = messageEvent.content || '';
|
|
914
|
+
// Never respond to turn limit messages (prevents endless loops)
|
|
915
|
+
if (content.includes('Turn limit reached')) {
|
|
916
|
+
loggerTurnLimit.debug('Skipping turn limit message', { agentId: agent.id });
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
// Check turn limit based on LLM call count
|
|
920
|
+
const worldTurnLimit = getWorldTurnLimit(world);
|
|
921
|
+
loggerTurnLimit.debug('Checking turn limit', { agentId: agent.id, llmCallCount: agent.llmCallCount, worldTurnLimit });
|
|
922
|
+
if (agent.llmCallCount >= worldTurnLimit) {
|
|
923
|
+
loggerTurnLimit.debug('Turn limit reached, sending turn limit message', { agentId: agent.id, llmCallCount: agent.llmCallCount, worldTurnLimit });
|
|
924
|
+
const turnLimitMessage = `@human Turn limit reached (${worldTurnLimit} LLM calls). Please take control of the conversation.`;
|
|
925
|
+
publishMessage(world, turnLimitMessage, agent.id);
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
// Determine sender type for message handling logic
|
|
929
|
+
const senderType = determineSenderType(messageEvent.sender);
|
|
930
|
+
loggerResponse.debug('Determined sender type', { agentId: agent.id, sender: messageEvent.sender, senderType });
|
|
931
|
+
// Never respond to system messages
|
|
932
|
+
if (messageEvent.sender === 'system') {
|
|
933
|
+
loggerResponse.debug('Skipping system message', { agentId: agent.id });
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
// Always respond to world messages
|
|
937
|
+
if (messageEvent.sender === 'world') {
|
|
938
|
+
loggerResponse.debug('Responding to world message', { agentId: agent.id });
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
const anyMentions = extractMentions(messageEvent.content);
|
|
942
|
+
const mentions = extractParagraphBeginningMentions(messageEvent.content);
|
|
943
|
+
loggerResponse.debug('Extracted mentions', { mentions, anyMentions });
|
|
944
|
+
// For HUMAN messages
|
|
945
|
+
if (senderType === SenderType.HUMAN) {
|
|
946
|
+
if (mentions.length === 0) {
|
|
947
|
+
if (anyMentions.length > 0) {
|
|
948
|
+
loggerResponse.debug('Mentions exist but not at paragraph beginning', { agentId: agent.id });
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
loggerResponse.debug('No mentions - public message', { agentId: agent.id });
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
const shouldRespond = mentions.includes(agent.id.toLowerCase());
|
|
955
|
+
loggerResponse.debug('HUMAN message mention check', { agentId: agent.id, shouldRespond });
|
|
956
|
+
return shouldRespond;
|
|
957
|
+
}
|
|
958
|
+
// For agent messages, only respond if this agent has a paragraph-beginning mention
|
|
959
|
+
const shouldRespond = mentions.includes(agent.id.toLowerCase());
|
|
960
|
+
loggerResponse.debug('AGENT message mention check', { agentId: agent.id, shouldRespond });
|
|
961
|
+
return shouldRespond;
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Subscribe world to messages with cleanup function
|
|
965
|
+
*/
|
|
966
|
+
export function subscribeWorldToMessages(world) {
|
|
967
|
+
return subscribeToMessages(world, async (_event) => {
|
|
968
|
+
// No-op - title updates handled by setupWorldActivityListener on idle
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Setup world activity listener for chat title updates
|
|
973
|
+
* Triggers title generation when world becomes idle (pendingOperations === 0)
|
|
974
|
+
*/
|
|
975
|
+
export function setupWorldActivityListener(world) {
|
|
976
|
+
const handler = async (event) => {
|
|
977
|
+
// Only update title when world becomes idle (all agents done)
|
|
978
|
+
if (event.type === 'idle' && event.pendingOperations === 0) {
|
|
979
|
+
try {
|
|
980
|
+
if (!world.currentChatId)
|
|
981
|
+
return;
|
|
982
|
+
const chat = world.chats.get(world.currentChatId);
|
|
983
|
+
if (!chat)
|
|
984
|
+
return;
|
|
985
|
+
// Only update if still default title
|
|
986
|
+
if (chat.name === 'New Chat') {
|
|
987
|
+
const title = await generateChatTitleFromMessages(world, '');
|
|
988
|
+
if (title) {
|
|
989
|
+
chat.name = title;
|
|
990
|
+
const storage = await getStorageWrappers();
|
|
991
|
+
await storage.updateChatData(world.id, world.currentChatId, { name: title });
|
|
992
|
+
publishEvent(world, 'system', `chat-title-updated`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
catch (err) {
|
|
997
|
+
loggerChatTitle.warn('Activity-based title update failed', { error: err instanceof Error ? err.message : err });
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
world.eventEmitter.on('world', handler);
|
|
1002
|
+
return () => world.eventEmitter.off('world', handler);
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Generate chat title from message content with LLM support and fallback
|
|
1006
|
+
*/
|
|
1007
|
+
async function generateChatTitleFromMessages(world, content) {
|
|
1008
|
+
loggerChatTitle.debug('Generating chat title', { worldId: world.id, contentStart: content.substring(0, 50) });
|
|
1009
|
+
let title = '';
|
|
1010
|
+
let messages = [];
|
|
1011
|
+
const maxLength = 100; // Max title length
|
|
1012
|
+
try {
|
|
1013
|
+
const firstAgent = Array.from(world.agents.values())[0];
|
|
1014
|
+
const storage = await getStorageWrappers();
|
|
1015
|
+
// Load messages for current chat only, not all messages
|
|
1016
|
+
messages = await storage.getMemory(world.id, world.currentChatId);
|
|
1017
|
+
if (content)
|
|
1018
|
+
messages.push({ role: 'user', content });
|
|
1019
|
+
loggerChatTitle.debug('Calling LLM for title generation', {
|
|
1020
|
+
messageCount: messages.length,
|
|
1021
|
+
provider: world.chatLLMProvider || firstAgent?.provider,
|
|
1022
|
+
model: world.chatLLMModel || firstAgent?.model
|
|
1023
|
+
});
|
|
1024
|
+
const tempAgent = {
|
|
1025
|
+
provider: world.chatLLMProvider || firstAgent?.provider || 'openai',
|
|
1026
|
+
model: world.chatLLMModel || firstAgent?.model || 'gpt-4',
|
|
1027
|
+
systemPrompt: 'You are a helpful assistant that turns conversations into concise titles.',
|
|
1028
|
+
maxTokens: 20,
|
|
1029
|
+
};
|
|
1030
|
+
const userPrompt = {
|
|
1031
|
+
role: 'user',
|
|
1032
|
+
content: `Below is a conversation between a user and an assistant. Generate a short, punchy title (3–6 words) that captures its main topic.
|
|
1033
|
+
|
|
1034
|
+
${messages.filter(msg => msg.role !== 'tool').map(msg => `-${msg.role}: ${msg.content}`).join('\n')}
|
|
1035
|
+
`
|
|
1036
|
+
};
|
|
1037
|
+
title = await generateAgentResponse(world, tempAgent, [userPrompt], undefined, true); // skipTools = true for title generation
|
|
1038
|
+
loggerChatTitle.debug('LLM generated title', { rawTitle: title });
|
|
1039
|
+
}
|
|
1040
|
+
catch (error) {
|
|
1041
|
+
loggerChatTitle.warn('Failed to generate LLM title, using fallback', {
|
|
1042
|
+
error: error instanceof Error ? error.message : error
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
if (!title) {
|
|
1046
|
+
// Fallback: use content if provided, otherwise extract from first user message
|
|
1047
|
+
title = content.trim();
|
|
1048
|
+
if (!title && messages?.length > 0) {
|
|
1049
|
+
const firstUserMsg = messages.find((msg) => msg.role === 'user');
|
|
1050
|
+
title = firstUserMsg?.content?.substring(0, 50) || 'Chat';
|
|
1051
|
+
}
|
|
1052
|
+
if (!title)
|
|
1053
|
+
title = 'Chat';
|
|
1054
|
+
}
|
|
1055
|
+
title = title.trim().replace(/^["']|["']$/g, ''); // Remove quotes
|
|
1056
|
+
title = title.replace(/[\n\r\*]+/g, ' '); // Replace newlines with spaces
|
|
1057
|
+
title = title.replace(/\s+/g, ' '); // Normalize whitespace
|
|
1058
|
+
// Truncate if too long
|
|
1059
|
+
if (title.length > maxLength) {
|
|
1060
|
+
title = title.substring(0, maxLength - 3) + '...';
|
|
1061
|
+
}
|
|
1062
|
+
loggerChatTitle.debug('Final processed title', { title, originalLength: title.length });
|
|
1063
|
+
return title;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Check if a specific tool requires approval based on message history
|
|
1067
|
+
* Simplified: Only checks for session-wide approval, not one-time or denials
|
|
1068
|
+
*
|
|
1069
|
+
* Logic:
|
|
1070
|
+
* 1. Search for session approval → Execute immediately
|
|
1071
|
+
* 2. No session approval → Request approval
|
|
1072
|
+
*
|
|
1073
|
+
* @param context - Execution context with workingDirectory (CRITICAL FIX: AR Issue #1)
|
|
1074
|
+
*/
|
|
1075
|
+
export async function checkToolApproval(world, toolName, toolArgs, message, messages, context) {
|
|
1076
|
+
try {
|
|
1077
|
+
// Check for session-wide approval ONLY (matches name + directory + params)
|
|
1078
|
+
const workingDirectory = context?.workingDirectory || process.cwd();
|
|
1079
|
+
const sessionApproval = findSessionApproval(messages, toolName, toolArgs, workingDirectory);
|
|
1080
|
+
if (sessionApproval) {
|
|
1081
|
+
return {
|
|
1082
|
+
needsApproval: false,
|
|
1083
|
+
canExecute: true
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
// No session approval found - need to request approval
|
|
1087
|
+
return {
|
|
1088
|
+
needsApproval: true,
|
|
1089
|
+
canExecute: false,
|
|
1090
|
+
approvalRequest: {
|
|
1091
|
+
toolName,
|
|
1092
|
+
toolArgs,
|
|
1093
|
+
message,
|
|
1094
|
+
workingDirectory, // Include for session approval matching
|
|
1095
|
+
requestId: `approval-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
1096
|
+
options: ['deny', 'approve_once', 'approve_session']
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
loggerAgent.error('Error checking tool approval', {
|
|
1102
|
+
toolName,
|
|
1103
|
+
error: error instanceof Error ? error.message : error
|
|
1104
|
+
});
|
|
1105
|
+
return {
|
|
1106
|
+
needsApproval: true,
|
|
1107
|
+
canExecute: false,
|
|
1108
|
+
approvalRequest: {
|
|
1109
|
+
toolName,
|
|
1110
|
+
toolArgs,
|
|
1111
|
+
message,
|
|
1112
|
+
workingDirectory: context?.workingDirectory || process.cwd(), // Include even in error case
|
|
1113
|
+
requestId: `approval-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
1114
|
+
options: ['deny', 'approve_once', 'approve_session']
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Find session-wide approval for a tool in message history
|
|
1121
|
+
* Supports both enhanced string protocol (JSON) and legacy text parsing
|
|
1122
|
+
*
|
|
1123
|
+
* Session approval matches on:
|
|
1124
|
+
* - Tool name (required)
|
|
1125
|
+
* - Working directory (if provided)
|
|
1126
|
+
* - Parameters (exact match)
|
|
1127
|
+
*
|
|
1128
|
+
* Enhanced protocol format:
|
|
1129
|
+
* {
|
|
1130
|
+
* role: 'tool',
|
|
1131
|
+
* tool_call_id: 'approval_...',
|
|
1132
|
+
* content: '{"__type":"tool_result","content":"{\"decision\":\"approve\",\"scope\":\"session\",\"toolName\":\"...\",\"toolArgs\":{...},\"workingDirectory\":\"...\"}"}'
|
|
1133
|
+
* }
|
|
1134
|
+
*/
|
|
1135
|
+
export function findSessionApproval(messages, toolName, toolArgs, workingDirectory) {
|
|
1136
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1137
|
+
const msg = messages[i];
|
|
1138
|
+
// Primary: Enhanced string protocol (JSON tool result)
|
|
1139
|
+
if (msg.role === 'tool' && msg.tool_call_id && msg.content) {
|
|
1140
|
+
try {
|
|
1141
|
+
const parsed = JSON.parse(msg.content);
|
|
1142
|
+
if (parsed.__type === 'tool_result' && parsed.content) {
|
|
1143
|
+
const result = JSON.parse(parsed.content);
|
|
1144
|
+
if (result.decision === 'approve' &&
|
|
1145
|
+
result.scope === 'session' &&
|
|
1146
|
+
result.toolName?.toLowerCase() === toolName.toLowerCase()) {
|
|
1147
|
+
// Match working directory if provided in approval
|
|
1148
|
+
if (result.workingDirectory && workingDirectory) {
|
|
1149
|
+
if (result.workingDirectory !== workingDirectory) {
|
|
1150
|
+
continue; // Directory mismatch, keep searching
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// Match parameters (exact deep equality)
|
|
1154
|
+
if (result.toolArgs && toolArgs) {
|
|
1155
|
+
const argsMatch = JSON.stringify(result.toolArgs) === JSON.stringify(toolArgs);
|
|
1156
|
+
if (!argsMatch) {
|
|
1157
|
+
continue; // Parameters mismatch, keep searching
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return { decision: 'approve', scope: 'session', toolName };
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
catch (e) {
|
|
1165
|
+
// Not JSON or malformed, continue to fallback
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
// Fallback: Legacy text parsing (backwards compatibility)
|
|
1169
|
+
if (msg.content && typeof msg.content === 'string') {
|
|
1170
|
+
const content = msg.content.toLowerCase();
|
|
1171
|
+
if ((content.includes('approve') && content.includes(toolName.toLowerCase()) && content.includes('session')) ||
|
|
1172
|
+
(content.includes(`approve_session`) && content.includes(toolName.toLowerCase()))) {
|
|
1173
|
+
// ⚠️ Security warning: Legacy approvals don't check parameters
|
|
1174
|
+
loggerMemory.warn('Using legacy text-based approval (no parameter/directory check)', {
|
|
1175
|
+
toolName,
|
|
1176
|
+
security: 'UNSCOPED - all parameters and directories allowed for this tool'
|
|
1177
|
+
});
|
|
1178
|
+
return { decision: 'approve', scope: 'session', toolName };
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return undefined;
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* @deprecated This function is no longer used in approval checking logic.
|
|
1186
|
+
* One-time approvals are consumed after tool execution, checking for them is redundant.
|
|
1187
|
+
* Kept for backwards compatibility only.
|
|
1188
|
+
*/
|
|
1189
|
+
export function findRecentApproval(messages, toolName) {
|
|
1190
|
+
loggerMemory.warn('DEPRECATED: findRecentApproval() uses text parsing. Migrate to enhanced string protocol with __type: "tool_result"', {
|
|
1191
|
+
toolName,
|
|
1192
|
+
hint: 'Send JSON.stringify({__type:"tool_result",tool_call_id:"...",content:"..."})'
|
|
1193
|
+
});
|
|
1194
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
1195
|
+
let approvalIndex = -1;
|
|
1196
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1197
|
+
const msg = messages[i];
|
|
1198
|
+
if (msg.createdAt && msg.createdAt < fiveMinutesAgo)
|
|
1199
|
+
break;
|
|
1200
|
+
if (msg.content && typeof msg.content === 'string') {
|
|
1201
|
+
const content = msg.content.toLowerCase();
|
|
1202
|
+
if ((content.includes('approve') && content.includes(toolName.toLowerCase()) &&
|
|
1203
|
+
(content.includes('once') || (!content.includes('session')))) ||
|
|
1204
|
+
(content.includes(`approve_once`) && content.includes(toolName.toLowerCase()))) {
|
|
1205
|
+
approvalIndex = i;
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if (approvalIndex === -1)
|
|
1211
|
+
return undefined;
|
|
1212
|
+
// Check if approval has been consumed by subsequent tool execution
|
|
1213
|
+
for (let i = approvalIndex + 1; i < messages.length; i++) {
|
|
1214
|
+
const msg = messages[i];
|
|
1215
|
+
if (msg.content && typeof msg.content === 'string') {
|
|
1216
|
+
const content = msg.content.toLowerCase();
|
|
1217
|
+
if ((content.includes('tool') && content.includes(toolName.toLowerCase()) &&
|
|
1218
|
+
(content.includes('executed') || content.includes('completed') || content.includes('finished'))) ||
|
|
1219
|
+
(content.includes(toolName.toLowerCase()) && content.includes('successfully'))) {
|
|
1220
|
+
return undefined;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return { decision: 'approve', scope: 'once', toolName };
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* @deprecated This function is no longer used in approval checking logic.
|
|
1228
|
+
* Users should be allowed to change their mind about denials.
|
|
1229
|
+
* Kept for backwards compatibility only.
|
|
1230
|
+
*/
|
|
1231
|
+
export function findRecentDenial(messages, toolName) {
|
|
1232
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
1233
|
+
// Look for recent denial
|
|
1234
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1235
|
+
const msg = messages[i];
|
|
1236
|
+
if (msg.createdAt && msg.createdAt < fiveMinutesAgo) {
|
|
1237
|
+
break; // Stop if we've gone back more than 5 minutes
|
|
1238
|
+
}
|
|
1239
|
+
if (msg.content && typeof msg.content === 'string') {
|
|
1240
|
+
const content = msg.content.toLowerCase();
|
|
1241
|
+
if (content.includes('deny') && content.includes(toolName.toLowerCase())) {
|
|
1242
|
+
return { decision: 'deny', toolName };
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return undefined;
|
|
1247
|
+
}
|
|
1248
|
+
//# sourceMappingURL=events.js.map
|