@hailer/mcp 0.1.17 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/dist/app.js +27 -20
  2. package/dist/core.d.ts +33 -9
  3. package/dist/core.js +279 -147
  4. package/dist/mcp/UserContextCache.js +18 -0
  5. package/dist/mcp/hailer-clients.d.ts +9 -1
  6. package/dist/mcp/hailer-clients.js +13 -3
  7. package/dist/mcp/signal-handler.js +1 -1
  8. package/dist/mcp/tool-registry.d.ts +3 -1
  9. package/dist/mcp/tool-registry.js +4 -1
  10. package/dist/mcp/tools/activity.js +43 -34
  11. package/dist/mcp/tools/bot-config/constants.d.ts +23 -0
  12. package/dist/mcp/tools/bot-config/constants.js +94 -0
  13. package/dist/mcp/tools/{bot-config.d.ts → bot-config/core.d.ts} +6 -6
  14. package/dist/mcp/tools/{bot-config.js → bot-config/core.js} +15 -15
  15. package/dist/mcp/tools/bot-config/index.d.ts +10 -0
  16. package/dist/mcp/tools/bot-config/index.js +59 -0
  17. package/dist/mcp/tools/bot-config/tools.d.ts +7 -0
  18. package/dist/mcp/tools/bot-config/tools.js +15 -0
  19. package/dist/mcp/tools/bot-config/types.d.ts +50 -0
  20. package/dist/mcp/tools/bot-config/types.js +6 -0
  21. package/dist/mcp/tools/bug-fixer-tools.d.ts +21 -0
  22. package/dist/mcp/tools/{giuseppe-tools.js → bug-fixer-tools.js} +61 -61
  23. package/dist/mcp/tools/user.js +10 -29
  24. package/dist/mcp/tools/workflow.js +36 -2
  25. package/dist/mcp/utils/data-transformers.d.ts +0 -8
  26. package/dist/mcp/utils/data-transformers.js +0 -28
  27. package/dist/mcp/utils/index.d.ts +4 -1
  28. package/dist/mcp/utils/index.js +17 -3
  29. package/dist/mcp/utils/pagination.d.ts +40 -0
  30. package/dist/mcp/utils/pagination.js +55 -0
  31. package/dist/mcp/utils/response-builder.d.ts +53 -0
  32. package/dist/mcp/utils/response-builder.js +110 -0
  33. package/dist/mcp/utils/tool-helpers.d.ts +0 -8
  34. package/dist/mcp/utils/tool-helpers.js +0 -24
  35. package/dist/mcp/utils/types.d.ts +1 -33
  36. package/dist/mcp-server.d.ts +2 -2
  37. package/dist/mcp-server.js +161 -139
  38. package/package.json +1 -1
  39. package/REFACTOR_STATUS.md +0 -127
  40. package/dist/agents/bot-manager.d.ts +0 -48
  41. package/dist/agents/bot-manager.js +0 -254
  42. package/dist/agents/factory.d.ts +0 -150
  43. package/dist/agents/factory.js +0 -650
  44. package/dist/agents/giuseppe/ai.d.ts +0 -83
  45. package/dist/agents/giuseppe/ai.js +0 -466
  46. package/dist/agents/giuseppe/bot.d.ts +0 -110
  47. package/dist/agents/giuseppe/bot.js +0 -780
  48. package/dist/agents/giuseppe/config.d.ts +0 -25
  49. package/dist/agents/giuseppe/config.js +0 -227
  50. package/dist/agents/giuseppe/files.d.ts +0 -52
  51. package/dist/agents/giuseppe/files.js +0 -338
  52. package/dist/agents/giuseppe/git.d.ts +0 -48
  53. package/dist/agents/giuseppe/git.js +0 -298
  54. package/dist/agents/giuseppe/index.d.ts +0 -97
  55. package/dist/agents/giuseppe/index.js +0 -258
  56. package/dist/agents/giuseppe/lsp.d.ts +0 -113
  57. package/dist/agents/giuseppe/lsp.js +0 -485
  58. package/dist/agents/giuseppe/monitor.d.ts +0 -118
  59. package/dist/agents/giuseppe/monitor.js +0 -621
  60. package/dist/agents/giuseppe/prompt.d.ts +0 -5
  61. package/dist/agents/giuseppe/prompt.js +0 -94
  62. package/dist/agents/giuseppe/registries/pending-classification.d.ts +0 -28
  63. package/dist/agents/giuseppe/registries/pending-classification.js +0 -50
  64. package/dist/agents/giuseppe/registries/pending-fix.d.ts +0 -30
  65. package/dist/agents/giuseppe/registries/pending-fix.js +0 -42
  66. package/dist/agents/giuseppe/registries/pending.d.ts +0 -27
  67. package/dist/agents/giuseppe/registries/pending.js +0 -49
  68. package/dist/agents/giuseppe/specialist.d.ts +0 -47
  69. package/dist/agents/giuseppe/specialist.js +0 -237
  70. package/dist/agents/giuseppe/types.d.ts +0 -123
  71. package/dist/agents/giuseppe/types.js +0 -9
  72. package/dist/agents/hailer-expert/index.d.ts +0 -8
  73. package/dist/agents/hailer-expert/index.js +0 -14
  74. package/dist/agents/hal/daemon.d.ts +0 -142
  75. package/dist/agents/hal/daemon.js +0 -1103
  76. package/dist/agents/hal/definitions.d.ts +0 -55
  77. package/dist/agents/hal/definitions.js +0 -263
  78. package/dist/agents/hal/index.d.ts +0 -3
  79. package/dist/agents/hal/index.js +0 -8
  80. package/dist/agents/index.d.ts +0 -18
  81. package/dist/agents/index.js +0 -48
  82. package/dist/agents/shared/base.d.ts +0 -216
  83. package/dist/agents/shared/base.js +0 -846
  84. package/dist/agents/shared/services/agent-registry.d.ts +0 -107
  85. package/dist/agents/shared/services/agent-registry.js +0 -629
  86. package/dist/agents/shared/services/conversation-manager.d.ts +0 -50
  87. package/dist/agents/shared/services/conversation-manager.js +0 -136
  88. package/dist/agents/shared/services/mcp-client.d.ts +0 -56
  89. package/dist/agents/shared/services/mcp-client.js +0 -124
  90. package/dist/agents/shared/services/message-classifier.d.ts +0 -37
  91. package/dist/agents/shared/services/message-classifier.js +0 -187
  92. package/dist/agents/shared/services/message-formatter.d.ts +0 -89
  93. package/dist/agents/shared/services/message-formatter.js +0 -371
  94. package/dist/agents/shared/services/session-logger.d.ts +0 -106
  95. package/dist/agents/shared/services/session-logger.js +0 -446
  96. package/dist/agents/shared/services/tool-executor.d.ts +0 -41
  97. package/dist/agents/shared/services/tool-executor.js +0 -169
  98. package/dist/agents/shared/services/workspace-schema-cache.d.ts +0 -125
  99. package/dist/agents/shared/services/workspace-schema-cache.js +0 -578
  100. package/dist/agents/shared/specialist.d.ts +0 -91
  101. package/dist/agents/shared/specialist.js +0 -399
  102. package/dist/agents/shared/tool-schema-loader.d.ts +0 -62
  103. package/dist/agents/shared/tool-schema-loader.js +0 -232
  104. package/dist/agents/shared/types.d.ts +0 -327
  105. package/dist/agents/shared/types.js +0 -121
  106. package/dist/client/agents/base.d.ts +0 -207
  107. package/dist/client/agents/base.js +0 -744
  108. package/dist/client/agents/definitions.d.ts +0 -53
  109. package/dist/client/agents/definitions.js +0 -263
  110. package/dist/client/agents/orchestrator.d.ts +0 -141
  111. package/dist/client/agents/orchestrator.js +0 -1062
  112. package/dist/client/agents/specialist.d.ts +0 -86
  113. package/dist/client/agents/specialist.js +0 -340
  114. package/dist/client/bot-entrypoint.d.ts +0 -7
  115. package/dist/client/bot-entrypoint.js +0 -103
  116. package/dist/client/bot-manager.d.ts +0 -44
  117. package/dist/client/bot-manager.js +0 -173
  118. package/dist/client/bot-runner.d.ts +0 -35
  119. package/dist/client/bot-runner.js +0 -188
  120. package/dist/client/chat-agent-daemon.d.ts +0 -464
  121. package/dist/client/chat-agent-daemon.js +0 -1774
  122. package/dist/client/daemon-factory.d.ts +0 -106
  123. package/dist/client/daemon-factory.js +0 -301
  124. package/dist/client/factory.d.ts +0 -111
  125. package/dist/client/factory.js +0 -314
  126. package/dist/client/index.d.ts +0 -17
  127. package/dist/client/index.js +0 -38
  128. package/dist/client/multi-bot-manager.d.ts +0 -42
  129. package/dist/client/multi-bot-manager.js +0 -161
  130. package/dist/client/orchestrator-daemon.d.ts +0 -87
  131. package/dist/client/orchestrator-daemon.js +0 -444
  132. package/dist/client/server.d.ts +0 -8
  133. package/dist/client/server.js +0 -251
  134. package/dist/client/services/agent-registry.d.ts +0 -108
  135. package/dist/client/services/agent-registry.js +0 -630
  136. package/dist/client/services/conversation-manager.d.ts +0 -50
  137. package/dist/client/services/conversation-manager.js +0 -136
  138. package/dist/client/services/mcp-client.d.ts +0 -48
  139. package/dist/client/services/mcp-client.js +0 -105
  140. package/dist/client/services/message-classifier.d.ts +0 -37
  141. package/dist/client/services/message-classifier.js +0 -187
  142. package/dist/client/services/message-formatter.d.ts +0 -84
  143. package/dist/client/services/message-formatter.js +0 -353
  144. package/dist/client/services/session-logger.d.ts +0 -106
  145. package/dist/client/services/session-logger.js +0 -446
  146. package/dist/client/services/tool-executor.d.ts +0 -41
  147. package/dist/client/services/tool-executor.js +0 -169
  148. package/dist/client/services/workspace-schema-cache.d.ts +0 -149
  149. package/dist/client/services/workspace-schema-cache.js +0 -732
  150. package/dist/client/specialist-daemon.d.ts +0 -77
  151. package/dist/client/specialist-daemon.js +0 -197
  152. package/dist/client/specialists.d.ts +0 -53
  153. package/dist/client/specialists.js +0 -178
  154. package/dist/client/tool-schema-loader.d.ts +0 -62
  155. package/dist/client/tool-schema-loader.js +0 -232
  156. package/dist/client/types.d.ts +0 -327
  157. package/dist/client/types.js +0 -121
  158. package/dist/commands/seed-config.d.ts +0 -9
  159. package/dist/commands/seed-config.js +0 -372
  160. package/dist/lib/context-manager.d.ts +0 -111
  161. package/dist/lib/context-manager.js +0 -431
  162. package/dist/lib/prompt-length-manager.d.ts +0 -81
  163. package/dist/lib/prompt-length-manager.js +0 -457
  164. package/dist/mcp/tools/giuseppe-tools.d.ts +0 -21
  165. package/dist/modules/bug-reports/bug-config.d.ts +0 -25
  166. package/dist/modules/bug-reports/bug-config.js +0 -187
  167. package/dist/modules/bug-reports/bug-monitor.d.ts +0 -108
  168. package/dist/modules/bug-reports/bug-monitor.js +0 -510
  169. package/dist/modules/bug-reports/giuseppe-agent.d.ts +0 -58
  170. package/dist/modules/bug-reports/giuseppe-agent.js +0 -467
  171. package/dist/modules/bug-reports/giuseppe-ai.d.ts +0 -83
  172. package/dist/modules/bug-reports/giuseppe-ai.js +0 -466
  173. package/dist/modules/bug-reports/giuseppe-bot.d.ts +0 -110
  174. package/dist/modules/bug-reports/giuseppe-bot.js +0 -804
  175. package/dist/modules/bug-reports/giuseppe-daemon.d.ts +0 -80
  176. package/dist/modules/bug-reports/giuseppe-daemon.js +0 -617
  177. package/dist/modules/bug-reports/giuseppe-files.d.ts +0 -64
  178. package/dist/modules/bug-reports/giuseppe-files.js +0 -375
  179. package/dist/modules/bug-reports/giuseppe-git.d.ts +0 -48
  180. package/dist/modules/bug-reports/giuseppe-git.js +0 -298
  181. package/dist/modules/bug-reports/giuseppe-lsp.d.ts +0 -113
  182. package/dist/modules/bug-reports/giuseppe-lsp.js +0 -485
  183. package/dist/modules/bug-reports/giuseppe-prompt.d.ts +0 -5
  184. package/dist/modules/bug-reports/giuseppe-prompt.js +0 -94
  185. package/dist/modules/bug-reports/index.d.ts +0 -77
  186. package/dist/modules/bug-reports/index.js +0 -215
  187. package/dist/modules/bug-reports/pending-classification-registry.d.ts +0 -28
  188. package/dist/modules/bug-reports/pending-classification-registry.js +0 -50
  189. package/dist/modules/bug-reports/pending-fix-registry.d.ts +0 -30
  190. package/dist/modules/bug-reports/pending-fix-registry.js +0 -42
  191. package/dist/modules/bug-reports/pending-registry.d.ts +0 -27
  192. package/dist/modules/bug-reports/pending-registry.js +0 -49
  193. package/dist/modules/bug-reports/types.d.ts +0 -123
  194. package/dist/modules/bug-reports/types.js +0 -9
  195. package/dist/routes/agents.d.ts +0 -44
  196. package/dist/routes/agents.js +0 -311
  197. package/dist/services/agent-credential-store.d.ts +0 -73
  198. package/dist/services/agent-credential-store.js +0 -212
  199. package/dist/services/bug-monitor.d.ts +0 -23
  200. package/dist/services/bug-monitor.js +0 -275
@@ -1,1774 +0,0 @@
1
- "use strict";
2
- /**
3
- * Chat Agent Daemon
4
- *
5
- * A persistent LLM conversation that monitors all workspace chats.
6
- * The LLM maintains context across messages and decides what to respond to.
7
- *
8
- * Architecture:
9
- * - One daemon per bot client
10
- * - Subscribes to ALL messenger.new signals (not filtered)
11
- * - LLM sees every message with priority markers
12
- * - LLM decides: RESPOND / IGNORE / ACTION
13
- */
14
- var __importDefault = (this && this.__importDefault) || function (mod) {
15
- return (mod && mod.__esModule) ? mod : { "default": mod };
16
- };
17
- Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.ChatAgentDaemon = exports.MCP_CONFIG = exports.TOOL_REGISTRY = exports.TEAMS = exports.POSITIONS = exports.SESSION_LOG = exports.AGENT_DIRECTORY = void 0;
19
- const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
20
- const logger_1 = require("../lib/logger");
21
- const tool_schema_loader_1 = require("./tool-schema-loader");
22
- const tool_registry_1 = require("../mcp/tool-registry");
23
- // ===== AGENT REGISTRY & SESSION LOG CONSTANTS =====
24
- // These IDs reference the workspace workflows for agent tracking
25
- /** Agent Directory workflow - registry of all bot agents */
26
- exports.AGENT_DIRECTORY = {
27
- workflowId: "694a35b5b8b5c5f8030788e3",
28
- phases: {
29
- agentBuilder: "694a35b5b8b5c5f8030788f7",
30
- deployed: "694a35b5b8b5c5f8030788fb",
31
- retired: "694a35b5b8b5c5f8030788fc",
32
- },
33
- fields: {
34
- firstName: "694a35b5b8b5c5f8030788ea", // Etunimi (required)
35
- lastName: "694a35b5b8b5c5f8030788eb", // Sukunimi (required)
36
- description: "694a35b5b8b5c5f8030788f6", // System prompt/behavior
37
- hailerProfile: "694a35b5b8b5c5f8030788f2", // Agent Hailer profile (users)
38
- email: "694a35b5b8b5c5f8030788f3", // Email of Hailer profile
39
- password: "694a35b5b8b5c5f8030788f4", // Password
40
- team: "694a35b5b8b5c5f8030788e7", // Tiimi (activitylink)
41
- supervisor: "694a35b5b8b5c5f8030788e8", // Esimies (activitylink to self)
42
- memory: "694a35b5b8b5c5f8030788f5", // Memory
43
- position: "694a35b5b8b5c5f8030788e5", // Työpositio (activitylink)
44
- startDate: "694a35b5b8b5c5f8030788ec", // Aloituspäivämäärä
45
- },
46
- };
47
- /** Session Log workflow - tracks agent actions */
48
- exports.SESSION_LOG = {
49
- workflowId: "694a35b5b8b5c5f803078915",
50
- phases: {
51
- active: "694a35b5b8b5c5f803078941",
52
- archive: "694a35b5b8b5c5f803078942",
53
- },
54
- fields: {
55
- contextSummary: "694a35b5b8b5c5f80307893c", // What happened in this session
56
- previousLog: "694a35b5b8b5c5f80307893d", // Chain to previous log entry
57
- linkedWork: "694a35b5b8b5c5f80307893e", // Activity being worked on
58
- cost: "694a35b5b8b5c5f80307893f", // Token/API cost (numericunit)
59
- madeBy: "694a35b5b8b5c5f803078940", // Link to Agent Directory
60
- },
61
- };
62
- /** Positions workflow - job descriptions/roles for agents */
63
- exports.POSITIONS = {
64
- workflowId: "694a35b5b8b5c5f8030788e4",
65
- phases: {
66
- active: "694a35b5b8b5c5f80307890f", // Positiot
67
- },
68
- fields: {
69
- purpose: "694a35b5b8b5c5f803078901", // What the agent is for
70
- personaTone: "694a35b5b8b5c5f803078903", // Personality, voice, tone
71
- coreCapabilities: "694a35b5b8b5c5f803078904", // What the agent can do
72
- contextAwareness: "694a35b5b8b5c5f803078905", // Context access
73
- taskHandling: "694a35b5b8b5c5f803078906", // How to handle tasks
74
- infoRetrieval: "694a35b5b8b5c5f803078907", // Rules for searching/asking
75
- boundaries: "694a35b5b8b5c5f803078908", // What agent must NOT do
76
- userInteraction: "694a35b5b8b5c5f803078909", // How to interact with users
77
- errorHandling: "694a35b5b8b5c5f80307890a", // What to do when things go wrong
78
- learning: "694a35b5b8b5c5f80307890b", // Learning/adaptation rules
79
- escalation: "694a35b5b8b5c5f80307890c", // When to hand off to human
80
- auditLogging: "694a35b5b8b5c5f80307890d", // What to log
81
- agentCount: "694a35b5b8b5c5f803078902", // Number of agents in position
82
- multiAllowed: "694a35b5b8b5c5f80307890e", // >1 allowed in role (Yes/No)
83
- },
84
- };
85
- /** Teams workflow - groups of agents */
86
- exports.TEAMS = {
87
- workflowId: "694a35b5b8b5c5f8030788e6",
88
- phases: {
89
- active: "694a35b5b8b5c5f80307891a", // Tiimit
90
- },
91
- fields: {
92
- info: "694a35b5b8b5c5f803078919", // Tiimi-info (team description)
93
- leader: "694a35b5b8b5c5f8030788e9", // Tiimin johtaja → Agent Directory
94
- },
95
- };
96
- /** Tool Registry workflow - MCP server configurations */
97
- exports.TOOL_REGISTRY = {
98
- workflowId: "694a35b5b8b5c5f803078922",
99
- phases: {
100
- active: "694a35b5b8b5c5f803078927", // New Phase
101
- },
102
- fields: {
103
- baseUrl: "694a35b5b8b5c5f803078923", // mcp_base_url
104
- protocolVersion: "694a35b5b8b5c5f803078924", // protocol_version
105
- toolRegistry: "694a35b5b8b5c5f803078925", // tool_registry (JSON list)
106
- logEntryBy: "694a35b5b8b5c5f803078926", // Log entry by → Agent Directory
107
- },
108
- };
109
- /** MCP Config workflow - per-agent MCP configuration */
110
- exports.MCP_CONFIG = {
111
- workflowId: "694a35b5b8b5c5f803078928",
112
- phases: {
113
- active: "694a35b5b8b5c5f80307893b", // New Phase
114
- },
115
- fields: {
116
- agentName: "694a35b5b8b5c5f803078929", // agent_name → Agent Directory
117
- agentId: "694a35b5b8b5c5f80307892a", // Agent_ID (unique identifier)
118
- publicKey: "694a35b5b8b5c5f80307892c", // public_key
119
- accessTo: "694a35b5b8b5c5f80307892b", // Access to → Tool Registry
120
- authToken: "694a35b5b8b5c5f80307892d", // auth_token
121
- apiKey: "694a35b5b8b5c5f80307892e", // api_key
122
- workspaceId: "694a35b5b8b5c5f803078934", // workspace_id
123
- permissions: "694a35b5b8b5c5f803078935", // Permissions (list)
124
- sessionId: "694a35b5b8b5c5f803078936", // session_id
125
- resourceLimits: "694a35b5b8b5c5f803078937", // resource_limits
126
- loggingEndpoint: "694a35b5b8b5c5f803078938", // logging_endpoint
127
- callbackUrl: "694a35b5b8b5c5f803078939", // callback_url
128
- customContext: "694a35b5b8b5c5f80307893a", // custom_context
129
- // CRUD permissions (numeric)
130
- permCreate: "694a35b5b8b5c5f803078930",
131
- permRead: "694a35b5b8b5c5f803078931",
132
- permUpdate: "694a35b5b8b5c5f803078932",
133
- permDelete: "694a35b5b8b5c5f803078933",
134
- },
135
- };
136
- /** Idle timeout before flushing a session (ms) */
137
- const SESSION_IDLE_TIMEOUT = 60_000; // 60 seconds
138
- class ChatAgentDaemon {
139
- logger;
140
- client;
141
- botClient;
142
- config;
143
- toolSchemaLoader = new tool_schema_loader_1.ToolSchemaLoader();
144
- // Persistent conversation state
145
- conversationMessages = [];
146
- isProcessing = false;
147
- messageQueue = [];
148
- processedMessageIds = new Set();
149
- // Tool schemas (loaded once)
150
- toolIndex = [];
151
- minimalTools = [];
152
- loadedToolSchemas = new Map();
153
- // ===== AGENT REGISTRY & SESSION LOGGING STATE =====
154
- /** This agent's activity ID in the Agent Directory */
155
- agentDirectoryId = null;
156
- /** This agent's Position activity ID */
157
- positionId = null;
158
- /** Shared Team activity ID for AI agents */
159
- teamId = null;
160
- /** MCP Server Tool Registry activity ID (shared) */
161
- toolRegistryId = null;
162
- /** This agent's MCP Config activity ID */
163
- mcpConfigId = null;
164
- /** Per-activity session tracking (activityId -> session) */
165
- activitySessions = new Map();
166
- /** Global session log chain ID (for overall continuity) */
167
- lastGlobalLogId = null;
168
- /** Timer for checking idle sessions */
169
- idleCheckTimer = null;
170
- /** Current discussion context for tracking */
171
- currentDiscussionId = null;
172
- currentLinkedActivityId = null;
173
- constructor(config) {
174
- this.config = config;
175
- this.botClient = config.botClient;
176
- this.logger = (0, logger_1.createLogger)({
177
- component: "ChatAgentDaemon",
178
- botId: config.botClient.userId
179
- });
180
- this.client = new sdk_1.default({
181
- apiKey: config.anthropicApiKey,
182
- });
183
- }
184
- /**
185
- * Initialize the daemon - load tools and subscribe to signals
186
- */
187
- async initialize() {
188
- this.logger.info("Initializing Chat Agent Daemon", {
189
- botId: this.botClient.userId,
190
- email: this.botClient.config.email,
191
- });
192
- // Load tool index once
193
- const allowedGroups = [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE];
194
- this.toolIndex = await this.toolSchemaLoader.loadToolIndex({
195
- mcpServerUrl: this.config.mcpServerUrl,
196
- mcpServerApiKey: this.botClient.config.mcpServerApiKey,
197
- allowedGroups,
198
- });
199
- this.minimalTools = this.toolSchemaLoader.toMinimalToolDefinitions(this.toolIndex);
200
- this.logger.info("Tools loaded", { toolCount: this.toolIndex.length });
201
- // Register in all agent workflows
202
- await this.registerAllAgentData();
203
- // Subscribe to ALL messenger.new signals (no filtering)
204
- this.botClient.signalHandler.subscribe(`daemon-${this.botClient.userId}`, ["messenger.new"], this.handleSignal.bind(this));
205
- // Start idle session check timer (every 30 seconds)
206
- this.idleCheckTimer = setInterval(() => {
207
- this.checkAndFlushIdleSessions().catch(err => {
208
- this.logger.warn("Error checking idle sessions", { error: err });
209
- });
210
- }, 30_000);
211
- this.logger.info("Chat Agent Daemon initialized and listening", {
212
- agentDirectoryId: this.agentDirectoryId,
213
- positionId: this.positionId,
214
- teamId: this.teamId,
215
- toolRegistryId: this.toolRegistryId,
216
- mcpConfigId: this.mcpConfigId,
217
- });
218
- }
219
- /**
220
- * Handle incoming signal from Hailer
221
- */
222
- async handleSignal(signal) {
223
- try {
224
- const message = await this.extractIncomingMessage(signal);
225
- if (!message)
226
- return;
227
- // Dedupe
228
- if (this.processedMessageIds.has(message.id)) {
229
- return;
230
- }
231
- this.processedMessageIds.add(message.id);
232
- // Clean up old IDs (keep last 500)
233
- if (this.processedMessageIds.size > 500) {
234
- const ids = Array.from(this.processedMessageIds);
235
- this.processedMessageIds = new Set(ids.slice(-250));
236
- }
237
- this.logger.info("Incoming message", {
238
- from: message.senderName,
239
- discussion: message.discussionId,
240
- priority: message.priority,
241
- reason: message.priorityReason,
242
- preview: message.content.substring(0, 50),
243
- });
244
- // Queue the message
245
- this.messageQueue.push(message);
246
- // Process if not already processing
247
- if (!this.isProcessing) {
248
- await this.processQueue();
249
- }
250
- }
251
- catch (error) {
252
- this.logger.error("Failed to handle signal", error);
253
- }
254
- }
255
- /**
256
- * Extract and classify incoming message from signal
257
- * Override in subclasses to customize message filtering
258
- */
259
- async extractIncomingMessage(signal) {
260
- const { discussion, msg_id, uid } = signal.data;
261
- // Skip our own messages
262
- if (uid === this.botClient.userId) {
263
- return null;
264
- }
265
- // Fetch the actual message content
266
- let messageContent = "";
267
- let senderName = "Unknown";
268
- try {
269
- const response = await this.botClient.client.socket.request("v3.discussion.message.latest", [discussion]);
270
- const messages = response?.messages || [];
271
- const targetMessage = messages.find((msg) => msg._id === msg_id);
272
- if (!targetMessage) {
273
- return null;
274
- }
275
- messageContent = targetMessage.msg || targetMessage.content || "";
276
- // Get sender name: try workspace cache first, then message field
277
- senderName = this.getUserDisplayName(uid) || targetMessage.userName || "Unknown";
278
- // Skip system messages
279
- if (targetMessage.type && targetMessage.type !== "user") {
280
- return null;
281
- }
282
- }
283
- catch (error) {
284
- this.logger.warn("Could not fetch message content", { error });
285
- return null;
286
- }
287
- // Classify priority
288
- const isMention = this.checkMention(messageContent);
289
- const isReplyToBot = await this.checkReplyToBot(discussion, msg_id);
290
- const isDirectMessage = await this.checkDirectMessage(discussion, uid);
291
- // Get linked activity ID and name if this discussion belongs to an activity
292
- let linkedActivityId;
293
- let linkedActivityName;
294
- try {
295
- const discussionResponse = await this.botClient.client.socket.request("messenger.load_discussions", [[discussion]]);
296
- if (discussionResponse && discussionResponse[discussion]) {
297
- const discData = discussionResponse[discussion];
298
- linkedActivityId = discData.linked_activity;
299
- linkedActivityName = discData.name; // Discussion name is usually the activity name
300
- }
301
- }
302
- catch {
303
- // Ignore errors - linkedActivityId is optional
304
- }
305
- let priority = "normal";
306
- let priorityReason = "General chat message";
307
- if (isDirectMessage) {
308
- priority = "high";
309
- priorityReason = "Direct message (1:1 conversation)";
310
- }
311
- else if (isMention) {
312
- priority = "high";
313
- priorityReason = "Bot mentioned in message";
314
- }
315
- else if (isReplyToBot) {
316
- priority = "high";
317
- priorityReason = "Reply to bot's message";
318
- }
319
- return {
320
- id: msg_id,
321
- discussionId: discussion,
322
- linkedActivityId,
323
- linkedActivityName,
324
- senderId: uid,
325
- senderName,
326
- content: messageContent,
327
- timestamp: signal.timestamp,
328
- priority,
329
- priorityReason,
330
- isReplyToBot,
331
- isMention,
332
- isDirectMessage,
333
- };
334
- }
335
- /**
336
- * Check if message mentions this bot
337
- */
338
- checkMention(content) {
339
- const pattern = new RegExp(`\\[hailerTag\\|[^\\]]+\\]\\(${this.botClient.userId}\\)`, "i");
340
- return pattern.test(content);
341
- }
342
- /**
343
- * Check if message is a reply to one of our messages
344
- */
345
- async checkReplyToBot(discussionId, messageId) {
346
- try {
347
- const response = await this.botClient.client.socket.request("v3.discussion.message.latest", [discussionId]);
348
- const messages = response?.messages || [];
349
- const targetMessage = messages.find((msg) => msg._id === messageId);
350
- if (targetMessage?.replyTo) {
351
- const repliedToMessage = messages.find((msg) => msg._id === targetMessage.replyTo);
352
- return repliedToMessage?.uid === this.botClient.userId;
353
- }
354
- }
355
- catch {
356
- // Ignore errors
357
- }
358
- return false;
359
- }
360
- /**
361
- * Check if this is a 1:1 DM with the bot
362
- */
363
- async checkDirectMessage(discussionId, senderId) {
364
- try {
365
- const response = await this.botClient.client.socket.request("messenger.load_discussions", [[discussionId]]);
366
- if (response && response[discussionId]) {
367
- const discussion = response[discussionId];
368
- const participants = discussion.participants || [];
369
- // 1:1 = exactly 2 participants: sender and bot
370
- if (participants.length === 2) {
371
- return participants.includes(this.botClient.userId) &&
372
- participants.includes(senderId);
373
- }
374
- }
375
- }
376
- catch {
377
- // Ignore errors
378
- }
379
- return false;
380
- }
381
- // ===== USER LOOKUP & HAILER TAG FORMATTING =====
382
- /**
383
- * Get user's display name from workspace cache
384
- */
385
- getUserDisplayName(userId) {
386
- const cache = this.botClient.workspaceCache;
387
- if (!cache)
388
- return null;
389
- const user = cache.usersById[userId];
390
- if (!user)
391
- return null;
392
- return user.fullName || `${user.firstname || ''} ${user.lastname || ''}`.trim() || null;
393
- }
394
- /**
395
- * Format a Hailer tag for mentioning a user or activity
396
- * Hailer requires ZWNBSP (U+FEFF) around the tag
397
- */
398
- formatHailerTag(targetId, displayName) {
399
- const ZWNBSP = '\uFEFF';
400
- return `${ZWNBSP}[hailerTag|${displayName}](${targetId})${ZWNBSP}`;
401
- }
402
- /**
403
- * Extract link metadata from hailerTag formatted content
404
- * Returns array of links for the messenger.send API
405
- */
406
- extractTagLinks(content) {
407
- const links = [];
408
- // Match all hailerTag patterns: [hailerTag|Name](id)
409
- const tagPattern = /\[hailerTag\|[^\]]+\]\(([a-f0-9]{24})\)/gi;
410
- const matches = content.matchAll(tagPattern);
411
- for (const match of matches) {
412
- const targetId = match[1];
413
- // Determine if it's a user or activity based on context
414
- // For now, check if it's in the user cache
415
- const isUser = this.botClient.workspaceCache?.usersById[targetId];
416
- links.push({
417
- target: targetId,
418
- targetType: isUser ? 'user' : 'activity',
419
- type: 'linked-to'
420
- });
421
- }
422
- return links;
423
- }
424
- /**
425
- * Look up user by ID and create a Hailer tag
426
- */
427
- createUserTagById(userId) {
428
- const cache = this.botClient.workspaceCache;
429
- if (!cache)
430
- return null;
431
- const user = cache.usersById[userId];
432
- if (!user)
433
- return null;
434
- const displayName = user.fullName || `${user.firstname} ${user.lastname}`.trim();
435
- return this.formatHailerTag(userId, displayName);
436
- }
437
- /**
438
- * Look up user by name and create a Hailer tag
439
- */
440
- createUserTagByName(name) {
441
- const cache = this.botClient.workspaceCache;
442
- if (!cache)
443
- return null;
444
- const nameLower = name.toLowerCase();
445
- const user = cache.users.find(u => {
446
- const fullName = u.fullName || `${u.firstname} ${u.lastname}`.trim();
447
- return fullName.toLowerCase() === nameLower ||
448
- u.firstname?.toLowerCase() === nameLower ||
449
- u.lastname?.toLowerCase() === nameLower;
450
- });
451
- if (!user)
452
- return null;
453
- const displayName = user.fullName || `${user.firstname} ${user.lastname}`.trim();
454
- return this.formatHailerTag(user.id, displayName);
455
- }
456
- /**
457
- * Convert @mentions and #activity tags in response to Hailer tags
458
- * Supports:
459
- * - @userId (24-char hex MongoDB ID) → user tag
460
- * - @"Full Name" (quoted name lookup) → user tag
461
- * - @FirstName (single word name lookup) → user tag
462
- * - #activityId (24-char hex) → activity tag
463
- * - #"Activity Name" (quoted) → activity tag (requires lookup)
464
- */
465
- convertMentionsToTags(content) {
466
- // User mentions (from cache)
467
- const cache = this.botClient.workspaceCache;
468
- if (cache) {
469
- // Pattern 1: @userId (24-char hex)
470
- content = content.replace(/@([a-f0-9]{24})\b/gi, (match, userId) => {
471
- const tag = this.createUserTagById(userId);
472
- return tag || match;
473
- });
474
- // Pattern 2: @"Full Name" (quoted)
475
- content = content.replace(/@"([^"]+)"/g, (match, name) => {
476
- const tag = this.createUserTagByName(name);
477
- return tag || match;
478
- });
479
- // Pattern 3: @FirstName (single word, capitalized)
480
- content = content.replace(/@([A-Z][a-z]+)\b/g, (match, name) => {
481
- const tag = this.createUserTagByName(name);
482
- return tag || match;
483
- });
484
- }
485
- // Activity tags (#activityId and #"Name") are handled by
486
- // resolveActivityTags (async) which runs before this in postResponse
487
- return content;
488
- }
489
- /**
490
- * Resolve user tags - look up names for IDs via API when not in cache
491
- * Handles @userId (24-char hex) format
492
- */
493
- async resolveUserTags(content) {
494
- const pattern = /@([a-f0-9]{24})\b/gi;
495
- const matches = [...content.matchAll(pattern)];
496
- for (const match of matches) {
497
- const userId = match[1];
498
- // Try cache first
499
- const cachedTag = this.createUserTagById(userId);
500
- if (cachedTag) {
501
- content = content.replace(match[0], cachedTag);
502
- this.logger.debug("User tag from cache", { userId });
503
- continue;
504
- }
505
- // Fallback to API
506
- try {
507
- this.logger.info("Resolving user tag via API", { userId });
508
- const result = await this.callMcpTool("search_workspace_users", { query: userId });
509
- const resultText = result?.content?.[0]?.text;
510
- if (resultText) {
511
- // Parse the JSON array from the response
512
- // Response format: "Found X users:\n\n```json\n[...]\n```"
513
- const jsonMatch = resultText.match(/```json\s*([\s\S]*?)\s*```/);
514
- if (jsonMatch) {
515
- const users = JSON.parse(jsonMatch[1]);
516
- const user = users.find((u) => u._id === userId);
517
- if (user) {
518
- const displayName = `${user.firstname || ''} ${user.lastname || ''}`.trim() || user.fullName || userId;
519
- const tag = this.formatHailerTag(userId, displayName);
520
- content = content.replace(match[0], tag);
521
- this.logger.info("Created user tag from API", { userId, displayName });
522
- }
523
- }
524
- }
525
- }
526
- catch (error) {
527
- this.logger.warn("Failed to resolve user tag", { userId, error });
528
- // Leave as @userId - won't be clickable but visible
529
- }
530
- }
531
- return content;
532
- }
533
- /**
534
- * Resolve activity tags - look up names for IDs and IDs for names
535
- * Handles both #activityId and #"Activity Name" formats
536
- */
537
- async resolveActivityTags(content) {
538
- // Pattern 1: #activityId (24-char hex) - need to look up the name
539
- const activityIdPattern = /#([a-f0-9]{24})\b/gi;
540
- const idMatches = [...content.matchAll(activityIdPattern)];
541
- for (const match of idMatches) {
542
- const activityId = match[1];
543
- try {
544
- this.logger.info("Resolving activity tag by ID", { activityId });
545
- // Fetch activity by ID to get its name
546
- const result = await this.callMcpTool("show_activity_by_id", {
547
- activityId: activityId,
548
- });
549
- const resultText = result?.content?.[0]?.text;
550
- this.logger.debug("show_activity_by_id response", {
551
- hasResult: !!resultText,
552
- preview: resultText?.substring(0, 200)
553
- });
554
- if (resultText) {
555
- // Response format: "✅ Loaded activity with ID "xxx":\n\n{ JSON }"
556
- // Extract the JSON part after the header
557
- const jsonMatch = resultText.match(/\{[\s\S]*\}/);
558
- if (jsonMatch) {
559
- const parsed = JSON.parse(jsonMatch[0]);
560
- const activityName = parsed.name;
561
- this.logger.info("Extracted activity name", { activityId, activityName });
562
- if (activityName) {
563
- const tag = this.formatHailerTag(activityId, activityName);
564
- content = content.replace(match[0], tag);
565
- this.logger.info("Created activity tag", { tag });
566
- }
567
- }
568
- else {
569
- this.logger.warn("Could not extract JSON from response", { preview: resultText.substring(0, 100) });
570
- }
571
- }
572
- }
573
- catch (error) {
574
- this.logger.warn("Failed to resolve activity ID tag", { activityId, error });
575
- // Leave as #id - won't be clickable but at least visible
576
- }
577
- }
578
- // Pattern 2: #"Activity Name" - need to look up the ID
579
- // This is harder because list_activities requires workflowId/phaseId
580
- // For now, search in each workflow until we find a match
581
- const activityNamePattern = /#"([^"]+)"/g;
582
- const nameMatches = [...content.matchAll(activityNamePattern)];
583
- for (const match of nameMatches) {
584
- const activityName = match[1];
585
- try {
586
- // Get list of workflows first
587
- const workflowsResult = await this.callMcpTool("list_workflows_minimal", {});
588
- const workflowsText = workflowsResult?.content?.[0]?.text;
589
- if (workflowsText) {
590
- // Extract workflow IDs from the response (format varies)
591
- const workflowIdMatches = workflowsText.matchAll(/"_id":\s*"([a-f0-9]{24})"/gi);
592
- const workflowIds = [...workflowIdMatches].map(m => m[1]);
593
- // Search each workflow for the activity
594
- for (const workflowId of workflowIds.slice(0, 5)) { // Limit to first 5 workflows
595
- try {
596
- const phasesResult = await this.callMcpTool("list_workflow_phases", { workflowId });
597
- const phasesText = phasesResult?.content?.[0]?.text;
598
- if (phasesText) {
599
- const phaseIdMatches = phasesText.matchAll(/"_id":\s*"([a-f0-9]{24})"/gi);
600
- const phaseIds = [...phaseIdMatches].map(m => m[1]);
601
- for (const phaseId of phaseIds.slice(0, 3)) { // Limit to first 3 phases
602
- const activitiesResult = await this.callMcpTool("list_activities", {
603
- workflowId,
604
- phaseId,
605
- search: activityName,
606
- limit: 1,
607
- });
608
- const activitiesText = activitiesResult?.content?.[0]?.text;
609
- if (activitiesText) {
610
- // Look for activity ID and name in the response
611
- const activityIdMatch = activitiesText.match(/"_id":\s*"([a-f0-9]{24})"/i);
612
- if (activityIdMatch) {
613
- const foundId = activityIdMatch[1];
614
- const tag = this.formatHailerTag(foundId, activityName);
615
- content = content.replace(match[0], tag);
616
- break; // Found it, stop searching
617
- }
618
- }
619
- }
620
- }
621
- }
622
- catch {
623
- // Continue to next workflow
624
- }
625
- }
626
- }
627
- }
628
- catch (error) {
629
- this.logger.warn("Failed to resolve activity name tag", { activityName, error });
630
- // Leave the original #"Name" in place
631
- }
632
- }
633
- return content;
634
- }
635
- /**
636
- * Process queued messages through the LLM
637
- */
638
- async processQueue() {
639
- if (this.isProcessing || this.messageQueue.length === 0) {
640
- return;
641
- }
642
- this.isProcessing = true;
643
- try {
644
- // Sort by priority (high first) then by timestamp
645
- this.messageQueue.sort((a, b) => {
646
- const priorityOrder = { high: 0, normal: 1, low: 2 };
647
- if (priorityOrder[a.priority] !== priorityOrder[b.priority]) {
648
- return priorityOrder[a.priority] - priorityOrder[b.priority];
649
- }
650
- return a.timestamp - b.timestamp;
651
- });
652
- // Process one message at a time for now
653
- while (this.messageQueue.length > 0) {
654
- const message = this.messageQueue.shift();
655
- await this.processMessage(message);
656
- }
657
- }
658
- finally {
659
- this.isProcessing = false;
660
- }
661
- }
662
- /**
663
- * Process a single message through the persistent LLM conversation
664
- */
665
- async processMessage(message) {
666
- const startTime = Date.now();
667
- // Update current context for session tracking
668
- this.currentDiscussionId = message.discussionId;
669
- this.currentLinkedActivityId = message.linkedActivityId || null;
670
- // Get or create activity session
671
- const session = this.getOrCreateActivitySession(message);
672
- session.lastActivityTime = Date.now();
673
- session.metrics.messagesProcessed++;
674
- // Add user message to conversation log (compact version)
675
- const userSnippet = message.content.length > 100
676
- ? message.content.substring(0, 100) + "..."
677
- : message.content;
678
- session.conversation.push(`${message.senderName}: ${userSnippet}`);
679
- // Format incoming message for LLM
680
- const incomingContent = this.formatIncomingMessage(message);
681
- // Append to conversation
682
- this.conversationMessages.push({
683
- role: "user",
684
- content: incomingContent,
685
- });
686
- // Check context size and summarize if needed
687
- await this.manageContextSize();
688
- try {
689
- // Call LLM with current conversation
690
- const response = await this.client.messages.create({
691
- model: this.config.model || "claude-haiku-4-5-20251001",
692
- max_tokens: 2000,
693
- system: this.getSystemPrompt(),
694
- messages: this.conversationMessages,
695
- tools: this.minimalTools,
696
- });
697
- // Track token usage in the activity session
698
- if (response.usage) {
699
- session.metrics.inputTokens += response.usage.input_tokens;
700
- session.metrics.outputTokens += response.usage.output_tokens;
701
- }
702
- // Handle response
703
- await this.handleLlmResponse(response, message);
704
- const duration = Date.now() - startTime;
705
- this.logger.info("Message processed", {
706
- discussion: message.discussionId,
707
- duration,
708
- stopReason: response.stop_reason,
709
- sessionActivity: session.activityName,
710
- });
711
- // Update last activity time (idle checker will flush when needed)
712
- session.lastActivityTime = Date.now();
713
- }
714
- catch (error) {
715
- this.logger.error("LLM processing failed", error);
716
- // On error, remove the message from conversation to avoid poisoning context
717
- this.conversationMessages.pop();
718
- }
719
- }
720
- /**
721
- * Format incoming message for LLM consumption
722
- */
723
- formatIncomingMessage(message) {
724
- const priorityTag = message.priority === "high" ? " priority=\"high\"" : "";
725
- const reasonAttr = message.priority === "high" ? ` reason="${message.priorityReason}"` : "";
726
- const activityAttr = message.linkedActivityId ? ` activity_id="${message.linkedActivityId}"` : "";
727
- return `<incoming discussion="${message.discussionId}"${activityAttr} from="${message.senderName}" user_id="${message.senderId}" timestamp="${new Date(message.timestamp).toISOString()}"${priorityTag}${reasonAttr}>
728
- ${message.content}
729
- </incoming>`;
730
- }
731
- /**
732
- * Execute tool calls and continue the conversation
733
- * Simple passthrough - just execute tools and return results to LLM
734
- */
735
- async executeToolsAndContinue(toolUseBlocks, originalMessage) {
736
- const toolResults = [];
737
- // Get current activity session
738
- const sessionKey = this.currentLinkedActivityId || this.currentDiscussionId || "default";
739
- const session = this.activitySessions.get(sessionKey);
740
- for (const toolUse of toolUseBlocks) {
741
- this.logger.info("Executing tool", { tool: toolUse.name });
742
- // Track tool call in session
743
- if (session) {
744
- session.metrics.toolCalls++;
745
- session.actions.push(`Tool: ${toolUse.name}`);
746
- session.lastActivityTime = Date.now();
747
- }
748
- try {
749
- // Load full schema if needed
750
- if (!this.loadedToolSchemas.has(toolUse.name)) {
751
- const schema = await this.fetchToolSchema(toolUse.name);
752
- this.loadedToolSchemas.set(toolUse.name, schema);
753
- }
754
- const result = await this.callMcpTool(toolUse.name, toolUse.input);
755
- const resultContent = JSON.stringify(result?.content || {});
756
- toolResults.push({
757
- type: "tool_result",
758
- tool_use_id: toolUse.id,
759
- content: resultContent,
760
- });
761
- }
762
- catch (error) {
763
- const errorMessage = error instanceof Error ? error.message : String(error);
764
- this.logger.warn("Tool execution failed", {
765
- tool: toolUse.name,
766
- error: errorMessage,
767
- });
768
- toolResults.push({
769
- type: "tool_result",
770
- tool_use_id: toolUse.id,
771
- content: `Error: ${errorMessage}`,
772
- is_error: true,
773
- });
774
- }
775
- }
776
- // Add tool results to conversation
777
- this.conversationMessages.push({
778
- role: "user",
779
- content: toolResults,
780
- });
781
- // Continue with LLM
782
- const response = await this.client.messages.create({
783
- model: this.config.model || "claude-haiku-4-5-20251001",
784
- max_tokens: 2000,
785
- system: this.getSystemPrompt(),
786
- messages: this.conversationMessages,
787
- tools: this.minimalTools,
788
- });
789
- // Track token usage in session
790
- if (session && response.usage) {
791
- session.metrics.inputTokens += response.usage.input_tokens;
792
- session.metrics.outputTokens += response.usage.output_tokens;
793
- session.lastActivityTime = Date.now();
794
- }
795
- // Recursively handle (might need more tools or finally respond)
796
- await this.handleLlmResponse(response, originalMessage);
797
- }
798
- /**
799
- * Handle LLM response
800
- * Override in subclasses to customize response handling
801
- */
802
- async handleLlmResponse(response, originalMessage) {
803
- // Add assistant response to conversation
804
- this.conversationMessages.push({
805
- role: "assistant",
806
- content: response.content,
807
- });
808
- // Check for tool calls
809
- const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
810
- if (toolUseBlocks.length > 0) {
811
- // Execute tools and continue conversation
812
- await this.executeToolsAndContinue(toolUseBlocks, originalMessage);
813
- return;
814
- }
815
- // Check for text response
816
- const textBlocks = response.content.filter((block) => block.type === "text");
817
- if (textBlocks.length > 0) {
818
- const responseText = textBlocks.map(b => b.text).join("\n").trim();
819
- this.logger.info("LLM raw response", {
820
- discussion: originalMessage.discussionId,
821
- priority: originalMessage.priority,
822
- responseLength: responseText.length,
823
- fullResponse: responseText.substring(0, 500),
824
- hasIgnoreTag: responseText.includes("<ignore"),
825
- hasRespondTag: responseText.includes("<respond"),
826
- });
827
- // Check for IGNORE decision
828
- if (responseText.includes("<decision>IGNORE</decision>") ||
829
- responseText.includes("<ignore")) {
830
- this.logger.debug("LLM decided to ignore message - removing from context", {
831
- discussion: originalMessage.discussionId,
832
- });
833
- // Remove both the assistant's ignore response AND the original incoming message
834
- // to keep the context clean from irrelevant chatter
835
- this.conversationMessages.pop(); // Remove assistant response we just added
836
- this.conversationMessages.pop(); // Remove the incoming message
837
- return;
838
- }
839
- // Check for explicit response directive
840
- const responseMatch = responseText.match(/<respond discussion="([^"]+)">([\s\S]*?)<\/respond>/);
841
- if (responseMatch) {
842
- const targetDiscussion = responseMatch[1];
843
- const content = responseMatch[2].trim();
844
- await this.postResponse(targetDiscussion, content);
845
- return;
846
- }
847
- // For high priority messages, always send substantive responses
848
- if (originalMessage.priority === "high" && responseText &&
849
- !responseText.includes("<thinking>") &&
850
- !responseText.startsWith("I'll") &&
851
- responseText.length > 10) {
852
- await this.postResponse(originalMessage.discussionId, responseText);
853
- return;
854
- }
855
- // For normal priority, ONLY respond if LLM used explicit <respond> tag
856
- // This was already handled above - if we reach here, don't post
857
- // (LLM should use <respond> or <ignore> for normal priority messages)
858
- if (originalMessage.priority === "normal") {
859
- this.logger.debug("Normal priority without <respond> tag - removing from context", {
860
- discussion: originalMessage.discussionId,
861
- responsePreview: responseText.substring(0, 100),
862
- });
863
- // Remove from context - we didn't respond so don't need this in history
864
- this.conversationMessages.pop(); // Remove assistant response
865
- this.conversationMessages.pop(); // Remove incoming message
866
- }
867
- }
868
- }
869
- /**
870
- * Post a response to a discussion
871
- * Automatically converts @mentions and #activity tags to Hailer tags
872
- * Includes links metadata required for tags to work
873
- */
874
- async postResponse(discussionId, content) {
875
- try {
876
- // Resolve tags that need API lookup
877
- let formattedContent = await this.resolveUserTags(content); // @userId → [hailerTag|Name](id)
878
- formattedContent = await this.resolveActivityTags(formattedContent); // #activityId → [hailerTag|Name](id)
879
- // Then convert any remaining @mentions from cache (fallback)
880
- formattedContent = this.convertMentionsToTags(formattedContent);
881
- // Remove redundant "(Name)" after tags - LLM sometimes adds these
882
- formattedContent = formattedContent.replace(/(\[hailerTag\|[^\]]+\]\([a-f0-9]{24}\)\uFEFF?)\s*\([^)]+\)/gi, '$1');
883
- // Extract link metadata for any tags in the content
884
- const links = this.extractTagLinks(formattedContent);
885
- // Build message object with links if we have any
886
- const messageData = {
887
- msg: formattedContent
888
- };
889
- if (links.length > 0) {
890
- messageData.links = links;
891
- }
892
- await this.botClient.client.socket.request("messenger.send", [
893
- messageData,
894
- discussionId,
895
- ]);
896
- // Track response in activity session
897
- const sessionKey = this.currentLinkedActivityId || this.currentDiscussionId || "default";
898
- const session = this.activitySessions.get(sessionKey);
899
- if (session) {
900
- session.metrics.responsesPosted++;
901
- session.actions.push(`Responded in discussion`);
902
- session.lastActivityTime = Date.now();
903
- // Add bot response to conversation log (compact version)
904
- const botSnippet = content.length > 100 ? content.substring(0, 100) + "..." : content;
905
- session.conversation.push(`Bot: ${botSnippet}`);
906
- }
907
- this.logger.info("Response posted", {
908
- discussion: discussionId,
909
- length: formattedContent.length,
910
- hadTags: formattedContent !== content,
911
- linkCount: links.length,
912
- });
913
- }
914
- catch (error) {
915
- this.logger.error("Failed to post response", error);
916
- }
917
- }
918
- /**
919
- * Manage conversation context size - summarize if too large
920
- */
921
- async manageContextSize() {
922
- const maxMessages = this.config.maxContextMessages || 50;
923
- if (this.conversationMessages.length > maxMessages) {
924
- this.logger.info("Context size limit reached, summarizing", {
925
- messageCount: this.conversationMessages.length,
926
- });
927
- // Keep last 10 messages, summarize the rest
928
- const toSummarize = this.conversationMessages.slice(0, -10);
929
- const toKeep = this.conversationMessages.slice(-10);
930
- // Create summary using LLM
931
- const summaryResponse = await this.client.messages.create({
932
- model: "claude-haiku-4-5-20251001",
933
- max_tokens: 1000,
934
- messages: [
935
- {
936
- role: "user",
937
- content: `Summarize this conversation context concisely, preserving key information:\n\n${JSON.stringify(toSummarize)}`,
938
- },
939
- ],
940
- });
941
- const summary = summaryResponse.content
942
- .filter((b) => b.type === "text")
943
- .map(b => b.text)
944
- .join("\n");
945
- // Replace conversation with summary + recent messages
946
- this.conversationMessages = [
947
- {
948
- role: "user",
949
- content: `<context_summary>${summary}</context_summary>`,
950
- },
951
- ...toKeep,
952
- ];
953
- this.logger.info("Context summarized", {
954
- originalCount: toSummarize.length + toKeep.length,
955
- newCount: this.conversationMessages.length,
956
- });
957
- }
958
- }
959
- /**
960
- * Get the system prompt for the daemon
961
- * MUST be overridden in subclasses
962
- */
963
- getSystemPrompt() {
964
- throw new Error("getSystemPrompt() must be implemented by subclass");
965
- }
966
- /**
967
- * Fetch tool schema from MCP server
968
- */
969
- async fetchToolSchema(toolName) {
970
- const url = `${this.config.mcpServerUrl}?apiKey=${this.botClient.config.mcpServerApiKey}`;
971
- const response = await fetch(url, {
972
- method: "POST",
973
- headers: { "Content-Type": "application/json" },
974
- body: JSON.stringify({
975
- jsonrpc: "2.0",
976
- id: Math.random().toString(36).substring(2),
977
- method: "tools/get_schema",
978
- params: { name: toolName },
979
- }),
980
- });
981
- const text = await response.text();
982
- const lines = text.split("\n");
983
- for (const line of lines) {
984
- if (line.startsWith("data: ")) {
985
- const data = JSON.parse(line.substring(6));
986
- if (data.result) {
987
- return {
988
- name: data.result.name,
989
- description: data.result.description,
990
- input_schema: data.result.inputSchema,
991
- };
992
- }
993
- }
994
- }
995
- throw new Error(`Failed to load schema for ${toolName}`);
996
- }
997
- /**
998
- * Call MCP tool
999
- */
1000
- async callMcpTool(name, args) {
1001
- const url = `${this.config.mcpServerUrl}?apiKey=${this.botClient.config.mcpServerApiKey}`;
1002
- const response = await fetch(url, {
1003
- method: "POST",
1004
- headers: { "Content-Type": "application/json" },
1005
- body: JSON.stringify({
1006
- jsonrpc: "2.0",
1007
- id: Math.random().toString(36).substring(2),
1008
- method: "tools/call",
1009
- params: { name, arguments: args },
1010
- }),
1011
- });
1012
- const text = await response.text();
1013
- const lines = text.split("\n");
1014
- for (const line of lines) {
1015
- if (line.startsWith("data: ")) {
1016
- const data = JSON.parse(line.substring(6));
1017
- if (data.error) {
1018
- throw new Error(data.error.message || data.error);
1019
- }
1020
- return data.result;
1021
- }
1022
- }
1023
- throw new Error("Failed to parse MCP response");
1024
- }
1025
- /**
1026
- * Stop the daemon
1027
- * Flushes all pending activity sessions before stopping
1028
- */
1029
- async stop() {
1030
- // Clear idle check timer
1031
- if (this.idleCheckTimer) {
1032
- clearInterval(this.idleCheckTimer);
1033
- this.idleCheckTimer = null;
1034
- }
1035
- // Flush all pending activity sessions
1036
- await this.flushAllSessions();
1037
- this.botClient.signalHandler.unsubscribe(`daemon-${this.botClient.userId}`);
1038
- this.logger.info("Chat Agent Daemon stopped", {
1039
- agentDirectoryId: this.agentDirectoryId,
1040
- activeSessions: this.activitySessions.size,
1041
- });
1042
- }
1043
- /**
1044
- * Get current conversation state (for debugging)
1045
- */
1046
- getConversationState() {
1047
- // Get last 5 messages with preview
1048
- const lastMessages = this.conversationMessages.slice(-5).map(msg => {
1049
- let preview = "";
1050
- if (typeof msg.content === "string") {
1051
- preview = msg.content.substring(0, 100);
1052
- }
1053
- else if (Array.isArray(msg.content)) {
1054
- const textBlock = msg.content.find((b) => b.type === "text");
1055
- preview = textBlock?.text?.substring(0, 100) || "[tool call/result]";
1056
- }
1057
- return {
1058
- role: msg.role,
1059
- preview: preview + (preview.length >= 100 ? "..." : ""),
1060
- };
1061
- });
1062
- return {
1063
- messageCount: this.conversationMessages.length,
1064
- queueLength: this.messageQueue.length,
1065
- lastMessages,
1066
- isProcessing: this.isProcessing,
1067
- };
1068
- }
1069
- /**
1070
- * Get full conversation for deep debugging
1071
- */
1072
- getFullConversation() {
1073
- return [...this.conversationMessages];
1074
- }
1075
- // ===== AGENT REGISTRY & SESSION LOGGING METHODS =====
1076
- /**
1077
- * Get agent's display name (override in subclass for custom names)
1078
- */
1079
- getAgentName() {
1080
- // Default: use email prefix as first name, "Bot" as last name
1081
- const email = this.botClient.config.email;
1082
- const emailPrefix = email.split("@")[0];
1083
- // Capitalize first letter
1084
- const firstName = emailPrefix.charAt(0).toUpperCase() + emailPrefix.slice(1);
1085
- return { firstName, lastName: "Bot" };
1086
- }
1087
- /**
1088
- * Get agent's description/system prompt (override in subclass)
1089
- */
1090
- getAgentDescription() {
1091
- return "Chat Agent Daemon - handles general workspace conversations";
1092
- }
1093
- /**
1094
- * Get default team ID from workspace cache
1095
- * Returns the first available team, or undefined if no teams exist
1096
- *
1097
- * Teams structure in init is: { teams: { workspaceId: { teamId: teamData, ... } } }
1098
- */
1099
- getDefaultTeamId() {
1100
- const rawInit = this.botClient.workspaceCache?.rawInit;
1101
- if (!rawInit?.teams) {
1102
- this.logger.debug("No teams in workspace cache");
1103
- return undefined;
1104
- }
1105
- // Teams are nested under workspace ID
1106
- // Structure: { workspaceId: { teamId1: {...}, teamId2: {...} } }
1107
- const workspaceTeams = Object.values(rawInit.teams)[0];
1108
- if (!workspaceTeams || typeof workspaceTeams !== 'object') {
1109
- this.logger.debug("No workspace teams found");
1110
- return undefined;
1111
- }
1112
- const teamIds = Object.keys(workspaceTeams);
1113
- if (teamIds.length === 0) {
1114
- this.logger.debug("No teams available in workspace");
1115
- return undefined;
1116
- }
1117
- const defaultTeamId = teamIds[0];
1118
- const teamName = workspaceTeams[defaultTeamId]?.name || 'unknown';
1119
- this.logger.debug("Using default team", { teamId: defaultTeamId, teamName, teamCount: teamIds.length });
1120
- return defaultTeamId;
1121
- }
1122
- /**
1123
- * Register this agent in the Agent Directory
1124
- * Called during initialize() - finds existing entry or creates new one
1125
- */
1126
- async registerAgentInDirectory() {
1127
- const email = this.botClient.config.email;
1128
- const userId = this.botClient.userId;
1129
- this.logger.info("Registering agent in directory", { email, userId });
1130
- try {
1131
- // First, try to find existing agent by email
1132
- const existingAgent = await this.findAgentByEmail(email);
1133
- if (existingAgent) {
1134
- this.agentDirectoryId = existingAgent._id;
1135
- this.logger.info("Found existing agent in directory", {
1136
- agentDirectoryId: this.agentDirectoryId,
1137
- name: existingAgent.name,
1138
- });
1139
- // Update the agent's Hailer profile field if not set
1140
- if (!existingAgent.fields?.[exports.AGENT_DIRECTORY.fields.hailerProfile]) {
1141
- await this.updateAgentProfile(existingAgent._id, userId);
1142
- }
1143
- return;
1144
- }
1145
- // Create new agent entry
1146
- const { firstName, lastName } = this.getAgentName();
1147
- const description = this.getAgentDescription();
1148
- const teamId = this.getDefaultTeamId();
1149
- this.logger.info("Creating new agent in directory", { firstName, lastName, teamId });
1150
- const createParams = {
1151
- workflowId: exports.AGENT_DIRECTORY.workflowId,
1152
- name: `${firstName} ${lastName}`,
1153
- phaseId: exports.AGENT_DIRECTORY.phases.deployed, // Start as deployed
1154
- fields: {
1155
- [exports.AGENT_DIRECTORY.fields.firstName]: firstName,
1156
- [exports.AGENT_DIRECTORY.fields.lastName]: lastName,
1157
- [exports.AGENT_DIRECTORY.fields.description]: description,
1158
- [exports.AGENT_DIRECTORY.fields.email]: email,
1159
- [exports.AGENT_DIRECTORY.fields.hailerProfile]: userId, // users field expects string (single user ID)
1160
- [exports.AGENT_DIRECTORY.fields.startDate]: Date.now(), // date field expects Unix timestamp (ms)
1161
- },
1162
- };
1163
- // Add teamId if available (required by some workflows)
1164
- if (teamId) {
1165
- createParams.teamId = teamId;
1166
- }
1167
- const result = await this.callMcpTool("create_activity", createParams);
1168
- // Parse result to get the created activity ID
1169
- const resultText = result?.content?.[0]?.text;
1170
- if (resultText) {
1171
- // Response format varies, try to extract ID
1172
- const idMatch = resultText.match(/"_id":\s*"([a-f0-9]{24})"/i) ||
1173
- resultText.match(/ID[:\s]+([a-f0-9]{24})/i) ||
1174
- resultText.match(/`([a-f0-9]{24})`/);
1175
- if (idMatch) {
1176
- this.agentDirectoryId = idMatch[1];
1177
- this.logger.info("Agent registered in directory", {
1178
- agentDirectoryId: this.agentDirectoryId,
1179
- });
1180
- }
1181
- }
1182
- }
1183
- catch (error) {
1184
- this.logger.warn("Failed to register agent in directory", {
1185
- error: error instanceof Error ? error.message : String(error),
1186
- });
1187
- // Non-fatal - agent can still function without directory entry
1188
- }
1189
- }
1190
- /**
1191
- * Find existing agent entry by email
1192
- */
1193
- async findAgentByEmail(email) {
1194
- try {
1195
- // Search deployed phase first
1196
- const result = await this.callMcpTool("list_activities", {
1197
- workflowId: exports.AGENT_DIRECTORY.workflowId,
1198
- phaseId: exports.AGENT_DIRECTORY.phases.deployed,
1199
- fields: [
1200
- exports.AGENT_DIRECTORY.fields.email,
1201
- exports.AGENT_DIRECTORY.fields.hailerProfile,
1202
- ],
1203
- filters: {
1204
- [exports.AGENT_DIRECTORY.fields.email]: {
1205
- operator: "text_search",
1206
- value: email,
1207
- },
1208
- },
1209
- limit: 1,
1210
- });
1211
- const resultText = result?.content?.[0]?.text;
1212
- if (resultText) {
1213
- // Try to parse the activities from the response
1214
- const jsonMatch = resultText.match(/```json\s*([\s\S]*?)\s*```/) ||
1215
- resultText.match(/\[[\s\S]*\]/);
1216
- if (jsonMatch) {
1217
- const jsonStr = jsonMatch[1] || jsonMatch[0];
1218
- const activities = JSON.parse(jsonStr);
1219
- if (Array.isArray(activities) && activities.length > 0) {
1220
- return activities[0];
1221
- }
1222
- }
1223
- }
1224
- }
1225
- catch (error) {
1226
- this.logger.debug("Error searching for agent", { error });
1227
- }
1228
- return null;
1229
- }
1230
- /**
1231
- * Update agent's Hailer profile reference
1232
- */
1233
- async updateAgentProfile(activityId, userId) {
1234
- try {
1235
- await this.callMcpTool("update_activity", {
1236
- activityId,
1237
- fields: {
1238
- [exports.AGENT_DIRECTORY.fields.hailerProfile]: userId, // string, not array
1239
- },
1240
- });
1241
- this.logger.debug("Updated agent profile reference", { activityId, userId });
1242
- }
1243
- catch (error) {
1244
- this.logger.warn("Failed to update agent profile", { error });
1245
- }
1246
- }
1247
- // ===== FULL AGENT REGISTRATION =====
1248
- /**
1249
- * Register all agent data across all workflows
1250
- * Orchestrates: Agent Directory, Position, Team, Tool Registry, MCP Config
1251
- */
1252
- async registerAllAgentData() {
1253
- this.logger.info("Starting full agent registration");
1254
- try {
1255
- // 1. Register in Agent Directory first (required for other links)
1256
- await this.registerAgentInDirectory();
1257
- // 2. Create/find shared Team for AI agents
1258
- await this.registerTeam();
1259
- // 3. Register MCP server in Tool Registry (shared)
1260
- await this.registerToolRegistry();
1261
- // 4. Create Position for this agent
1262
- await this.registerPosition();
1263
- // 5. Create MCP Config for this agent
1264
- await this.registerMcpConfig();
1265
- // 6. Update Agent Directory with Position and Team links
1266
- await this.linkAgentToPositionAndTeam();
1267
- this.logger.info("Full agent registration completed", {
1268
- agentDirectoryId: this.agentDirectoryId,
1269
- positionId: this.positionId,
1270
- teamId: this.teamId,
1271
- toolRegistryId: this.toolRegistryId,
1272
- mcpConfigId: this.mcpConfigId,
1273
- });
1274
- }
1275
- catch (error) {
1276
- this.logger.warn("Agent registration incomplete", {
1277
- error: error instanceof Error ? error.message : String(error),
1278
- agentDirectoryId: this.agentDirectoryId,
1279
- });
1280
- }
1281
- }
1282
- /**
1283
- * Get agent's Position details (override in subclass for custom positions)
1284
- */
1285
- getPositionDetails() {
1286
- return {
1287
- name: "Chat Agent",
1288
- purpose: "General workspace assistant that monitors discussions and responds to user queries.",
1289
- personaTone: "Professional, helpful, and concise. Responds directly without unnecessary filler.",
1290
- coreCapabilities: "- Monitor workspace discussions\n- Answer questions about workflows and activities\n- Execute MCP tools to read/write data\n- Tag users and link activities",
1291
- boundaries: "- Never fabricate data - always use tools\n- Don't respond to off-topic messages\n- Don't share sensitive credentials",
1292
- };
1293
- }
1294
- /**
1295
- * Register/find shared Team for AI agents
1296
- */
1297
- async registerTeam() {
1298
- const teamName = "AI Agents";
1299
- try {
1300
- // Search for existing team
1301
- const result = await this.callMcpTool("list_activities", {
1302
- workflowId: exports.TEAMS.workflowId,
1303
- phaseId: exports.TEAMS.phases.active,
1304
- fields: [exports.TEAMS.fields.info],
1305
- search: teamName,
1306
- limit: 1,
1307
- });
1308
- const resultText = result?.content?.[0]?.text;
1309
- const existingId = this.extractActivityId(resultText);
1310
- if (existingId) {
1311
- this.teamId = existingId;
1312
- this.logger.info("Found existing AI Agents team", { teamId: this.teamId });
1313
- return;
1314
- }
1315
- // Create new team
1316
- const wsTeamId = this.getDefaultTeamId();
1317
- const createParams = {
1318
- workflowId: exports.TEAMS.workflowId,
1319
- name: teamName,
1320
- phaseId: exports.TEAMS.phases.active,
1321
- fields: {
1322
- [exports.TEAMS.fields.info]: "Team of AI agent bots that assist with workspace operations.",
1323
- },
1324
- };
1325
- if (wsTeamId)
1326
- createParams.teamId = wsTeamId;
1327
- const createResult = await this.callMcpTool("create_activity", createParams);
1328
- this.teamId = this.extractActivityId(createResult?.content?.[0]?.text);
1329
- this.logger.info("Created AI Agents team", { teamId: this.teamId });
1330
- }
1331
- catch (error) {
1332
- this.logger.warn("Failed to register team", { error });
1333
- }
1334
- }
1335
- /**
1336
- * Register MCP server in Tool Registry
1337
- */
1338
- async registerToolRegistry() {
1339
- const serverName = "Hailer MCP Server";
1340
- try {
1341
- // Search for existing registry entry
1342
- const result = await this.callMcpTool("list_activities", {
1343
- workflowId: exports.TOOL_REGISTRY.workflowId,
1344
- phaseId: exports.TOOL_REGISTRY.phases.active,
1345
- fields: [exports.TOOL_REGISTRY.fields.baseUrl],
1346
- search: serverName,
1347
- limit: 1,
1348
- });
1349
- const resultText = result?.content?.[0]?.text;
1350
- const existingId = this.extractActivityId(resultText);
1351
- if (existingId) {
1352
- this.toolRegistryId = existingId;
1353
- this.logger.info("Found existing Tool Registry entry", { toolRegistryId: this.toolRegistryId });
1354
- return;
1355
- }
1356
- // Create new tool registry entry
1357
- const wsTeamId = this.getDefaultTeamId();
1358
- const toolNames = this.toolIndex.map((t) => t.name).join(", ");
1359
- const createParams = {
1360
- workflowId: exports.TOOL_REGISTRY.workflowId,
1361
- name: serverName,
1362
- phaseId: exports.TOOL_REGISTRY.phases.active,
1363
- fields: {
1364
- [exports.TOOL_REGISTRY.fields.baseUrl]: this.config.mcpServerUrl,
1365
- [exports.TOOL_REGISTRY.fields.protocolVersion]: "2024-11-05",
1366
- [exports.TOOL_REGISTRY.fields.toolRegistry]: `Available tools (${this.toolIndex.length}):\n${toolNames}`,
1367
- },
1368
- };
1369
- if (wsTeamId)
1370
- createParams.teamId = wsTeamId;
1371
- const createResult = await this.callMcpTool("create_activity", createParams);
1372
- this.toolRegistryId = this.extractActivityId(createResult?.content?.[0]?.text);
1373
- this.logger.info("Created Tool Registry entry", { toolRegistryId: this.toolRegistryId });
1374
- }
1375
- catch (error) {
1376
- this.logger.warn("Failed to register tool registry", { error });
1377
- }
1378
- }
1379
- /**
1380
- * Register Position for this agent
1381
- */
1382
- async registerPosition() {
1383
- const { firstName, lastName } = this.getAgentName();
1384
- const positionName = `${firstName} ${lastName} Position`;
1385
- const details = this.getPositionDetails();
1386
- try {
1387
- // Search for existing position by name
1388
- const result = await this.callMcpTool("list_activities", {
1389
- workflowId: exports.POSITIONS.workflowId,
1390
- phaseId: exports.POSITIONS.phases.active,
1391
- fields: [exports.POSITIONS.fields.purpose],
1392
- search: positionName,
1393
- limit: 1,
1394
- });
1395
- const resultText = result?.content?.[0]?.text;
1396
- const existingId = this.extractActivityId(resultText);
1397
- if (existingId) {
1398
- this.positionId = existingId;
1399
- this.logger.info("Found existing Position", { positionId: this.positionId });
1400
- return;
1401
- }
1402
- // Create new position
1403
- const wsTeamId = this.getDefaultTeamId();
1404
- const createParams = {
1405
- workflowId: exports.POSITIONS.workflowId,
1406
- name: positionName,
1407
- phaseId: exports.POSITIONS.phases.active,
1408
- fields: {
1409
- [exports.POSITIONS.fields.purpose]: details.purpose,
1410
- [exports.POSITIONS.fields.personaTone]: details.personaTone,
1411
- [exports.POSITIONS.fields.coreCapabilities]: details.coreCapabilities,
1412
- [exports.POSITIONS.fields.boundaries]: details.boundaries,
1413
- [exports.POSITIONS.fields.multiAllowed]: "Yes",
1414
- [exports.POSITIONS.fields.agentCount]: 1,
1415
- },
1416
- };
1417
- if (wsTeamId)
1418
- createParams.teamId = wsTeamId;
1419
- const createResult = await this.callMcpTool("create_activity", createParams);
1420
- this.positionId = this.extractActivityId(createResult?.content?.[0]?.text);
1421
- this.logger.info("Created Position", { positionId: this.positionId });
1422
- }
1423
- catch (error) {
1424
- this.logger.warn("Failed to register position", { error });
1425
- }
1426
- }
1427
- /**
1428
- * Register MCP Config for this agent
1429
- */
1430
- async registerMcpConfig() {
1431
- if (!this.agentDirectoryId) {
1432
- this.logger.debug("Skipping MCP config - no agent directory ID");
1433
- return;
1434
- }
1435
- const { firstName, lastName } = this.getAgentName();
1436
- const configName = `${firstName} ${lastName} Config`;
1437
- try {
1438
- // Search for existing config
1439
- const result = await this.callMcpTool("list_activities", {
1440
- workflowId: exports.MCP_CONFIG.workflowId,
1441
- phaseId: exports.MCP_CONFIG.phases.active,
1442
- fields: [exports.MCP_CONFIG.fields.agentId],
1443
- search: configName,
1444
- limit: 1,
1445
- });
1446
- const resultText = result?.content?.[0]?.text;
1447
- const existingId = this.extractActivityId(resultText);
1448
- if (existingId) {
1449
- this.mcpConfigId = existingId;
1450
- this.logger.info("Found existing MCP Config", { mcpConfigId: this.mcpConfigId });
1451
- return;
1452
- }
1453
- // Create new MCP config
1454
- const wsTeamId = this.getDefaultTeamId();
1455
- const workspaceId = this.botClient.workspaceCache?.currentWorkspace?._id || "";
1456
- const createParams = {
1457
- workflowId: exports.MCP_CONFIG.workflowId,
1458
- name: configName,
1459
- phaseId: exports.MCP_CONFIG.phases.active,
1460
- fields: {
1461
- [exports.MCP_CONFIG.fields.agentName]: this.agentDirectoryId,
1462
- [exports.MCP_CONFIG.fields.agentId]: this.botClient.userId,
1463
- [exports.MCP_CONFIG.fields.workspaceId]: workspaceId,
1464
- [exports.MCP_CONFIG.fields.permissions]: "read, write",
1465
- [exports.MCP_CONFIG.fields.permCreate]: 1,
1466
- [exports.MCP_CONFIG.fields.permRead]: 1,
1467
- [exports.MCP_CONFIG.fields.permUpdate]: 1,
1468
- [exports.MCP_CONFIG.fields.permDelete]: 0,
1469
- },
1470
- };
1471
- // Link to Tool Registry if available
1472
- if (this.toolRegistryId) {
1473
- createParams.fields[exports.MCP_CONFIG.fields.accessTo] = this.toolRegistryId;
1474
- }
1475
- if (wsTeamId)
1476
- createParams.teamId = wsTeamId;
1477
- const createResult = await this.callMcpTool("create_activity", createParams);
1478
- this.mcpConfigId = this.extractActivityId(createResult?.content?.[0]?.text);
1479
- this.logger.info("Created MCP Config", { mcpConfigId: this.mcpConfigId });
1480
- }
1481
- catch (error) {
1482
- this.logger.warn("Failed to register MCP config", { error });
1483
- }
1484
- }
1485
- /**
1486
- * Link Agent Directory entry to Position and Team
1487
- */
1488
- async linkAgentToPositionAndTeam() {
1489
- if (!this.agentDirectoryId) {
1490
- this.logger.debug("Skipping agent linking - no agent directory ID");
1491
- return;
1492
- }
1493
- const updates = {};
1494
- if (this.positionId) {
1495
- updates[exports.AGENT_DIRECTORY.fields.position] = this.positionId;
1496
- }
1497
- if (this.teamId) {
1498
- updates[exports.AGENT_DIRECTORY.fields.team] = this.teamId;
1499
- }
1500
- if (Object.keys(updates).length === 0) {
1501
- this.logger.debug("No links to update");
1502
- return;
1503
- }
1504
- try {
1505
- await this.callMcpTool("update_activity", {
1506
- activityId: this.agentDirectoryId,
1507
- fields: updates,
1508
- });
1509
- this.logger.info("Linked agent to Position and Team", {
1510
- agentDirectoryId: this.agentDirectoryId,
1511
- positionId: this.positionId,
1512
- teamId: this.teamId,
1513
- });
1514
- }
1515
- catch (error) {
1516
- this.logger.warn("Failed to link agent to Position/Team", { error });
1517
- }
1518
- }
1519
- /**
1520
- * Extract activity ID from MCP tool response text
1521
- */
1522
- extractActivityId(text) {
1523
- if (!text)
1524
- return null;
1525
- // Try various patterns
1526
- const idMatch = text.match(/"_id":\s*"([a-f0-9]{24})"/i) ||
1527
- text.match(/ID[:\s]+`?([a-f0-9]{24})`?/i) ||
1528
- text.match(/`([a-f0-9]{24})`/) ||
1529
- text.match(/([a-f0-9]{24})/);
1530
- return idMatch ? idMatch[1] : null;
1531
- }
1532
- // ===== PER-ACTIVITY SESSION LOGGING =====
1533
- /**
1534
- * Get or create an activity session for tracking
1535
- */
1536
- getOrCreateActivitySession(message) {
1537
- // Use linked activity ID as key, fallback to discussion ID
1538
- const sessionKey = message.linkedActivityId || message.discussionId;
1539
- let session = this.activitySessions.get(sessionKey);
1540
- if (!session) {
1541
- session = {
1542
- activityId: message.linkedActivityId || "",
1543
- activityName: message.linkedActivityName || `Discussion ${message.discussionId.substring(0, 8)}`,
1544
- discussionId: message.discussionId,
1545
- startTime: Date.now(),
1546
- lastActivityTime: Date.now(),
1547
- metrics: {
1548
- inputTokens: 0,
1549
- outputTokens: 0,
1550
- toolCalls: 0,
1551
- messagesProcessed: 0,
1552
- responsesPosted: 0,
1553
- },
1554
- actions: [],
1555
- previousLogId: this.lastGlobalLogId,
1556
- conversation: [],
1557
- };
1558
- this.activitySessions.set(sessionKey, session);
1559
- this.logger.debug("Created new activity session", {
1560
- sessionKey,
1561
- activityName: session.activityName,
1562
- });
1563
- }
1564
- return session;
1565
- }
1566
- /**
1567
- * Check for idle sessions and flush them
1568
- * Called periodically by timer
1569
- */
1570
- async checkAndFlushIdleSessions() {
1571
- const now = Date.now();
1572
- const sessionsToFlush = [];
1573
- for (const [key, session] of this.activitySessions) {
1574
- const idleTime = now - session.lastActivityTime;
1575
- if (idleTime >= SESSION_IDLE_TIMEOUT) {
1576
- sessionsToFlush.push(key);
1577
- }
1578
- }
1579
- for (const key of sessionsToFlush) {
1580
- const session = this.activitySessions.get(key);
1581
- if (session) {
1582
- await this.flushActivitySession(key, session);
1583
- this.activitySessions.delete(key);
1584
- }
1585
- }
1586
- if (sessionsToFlush.length > 0) {
1587
- this.logger.debug("Flushed idle sessions", { count: sessionsToFlush.length });
1588
- }
1589
- }
1590
- /**
1591
- * Flush a single activity session to the session log
1592
- */
1593
- async flushActivitySession(key, session) {
1594
- // Only log if tools were actually used (skip pure conversation)
1595
- if (session.metrics.toolCalls === 0) {
1596
- return;
1597
- }
1598
- if (!this.agentDirectoryId) {
1599
- this.logger.debug("Skipping session log - no agent directory ID");
1600
- return;
1601
- }
1602
- try {
1603
- // Use activity name for the session log name
1604
- const sessionName = session.activityName || `Session ${new Date().toISOString()}`;
1605
- // Build context summary
1606
- const summary = this.buildActivitySessionSummary(session);
1607
- // Calculate total tokens
1608
- const totalTokens = session.metrics.inputTokens + session.metrics.outputTokens;
1609
- // Build fields
1610
- const fields = {
1611
- [exports.SESSION_LOG.fields.contextSummary]: summary,
1612
- [exports.SESSION_LOG.fields.madeBy]: this.agentDirectoryId,
1613
- [exports.SESSION_LOG.fields.cost]: totalTokens,
1614
- };
1615
- // Chain to previous log
1616
- if (session.previousLogId) {
1617
- fields[exports.SESSION_LOG.fields.previousLog] = session.previousLogId;
1618
- }
1619
- // Link to activity being worked on
1620
- if (session.activityId) {
1621
- fields[exports.SESSION_LOG.fields.linkedWork] = session.activityId;
1622
- }
1623
- const createParams = {
1624
- workflowId: exports.SESSION_LOG.workflowId,
1625
- name: sessionName,
1626
- phaseId: exports.SESSION_LOG.phases.active,
1627
- fields,
1628
- };
1629
- const wsTeamId = this.getDefaultTeamId();
1630
- if (wsTeamId) {
1631
- createParams.teamId = wsTeamId;
1632
- }
1633
- const result = await this.callMcpTool("create_activity", createParams);
1634
- // Extract created ID for chaining
1635
- const resultText = result?.content?.[0]?.text;
1636
- if (resultText) {
1637
- const idMatch = resultText.match(/"_id":\s*"([a-f0-9]{24})"/i) ||
1638
- resultText.match(/`([a-f0-9]{24})`/);
1639
- if (idMatch) {
1640
- this.lastGlobalLogId = idMatch[1];
1641
- }
1642
- }
1643
- const durationSec = Math.round((Date.now() - session.startTime) / 1000);
1644
- this.logger.info("Activity session logged", {
1645
- activity: session.activityName,
1646
- durationSec,
1647
- messages: session.metrics.messagesProcessed,
1648
- toolCalls: session.metrics.toolCalls,
1649
- tokens: totalTokens,
1650
- });
1651
- }
1652
- catch (error) {
1653
- this.logger.warn("Failed to flush activity session", {
1654
- activity: session.activityName,
1655
- error: error instanceof Error ? error.message : String(error),
1656
- });
1657
- }
1658
- }
1659
- /**
1660
- * Flush all active sessions (called on shutdown)
1661
- */
1662
- async flushAllSessions() {
1663
- for (const [key, session] of this.activitySessions) {
1664
- await this.flushActivitySession(key, session);
1665
- }
1666
- this.activitySessions.clear();
1667
- }
1668
- /**
1669
- * Build summary for an activity session
1670
- */
1671
- buildActivitySessionSummary(session) {
1672
- const lines = [];
1673
- // Brief summary
1674
- const briefSummary = this.generateBriefSummaryFromActions(session.actions);
1675
- lines.push(briefSummary);
1676
- lines.push("");
1677
- lines.push("---");
1678
- lines.push("");
1679
- // Metrics
1680
- lines.push(`**Metrics:**`);
1681
- lines.push(`- Messages: ${session.metrics.messagesProcessed}`);
1682
- lines.push(`- Responses: ${session.metrics.responsesPosted}`);
1683
- lines.push(`- Tool calls: ${session.metrics.toolCalls}`);
1684
- lines.push(`- Tokens: ${session.metrics.inputTokens} in / ${session.metrics.outputTokens} out`);
1685
- // Duration
1686
- const duration = Math.round((Date.now() - session.startTime) / 1000);
1687
- lines.push(`- Duration: ${duration}s`);
1688
- if (session.actions.length > 0) {
1689
- lines.push("");
1690
- lines.push(`**Actions:**`);
1691
- const recentActions = session.actions.slice(-5);
1692
- for (const action of recentActions) {
1693
- lines.push(`- ${action}`);
1694
- }
1695
- if (session.actions.length > 5) {
1696
- lines.push(`... +${session.actions.length - 5} more`);
1697
- }
1698
- }
1699
- // Conversation context (compact)
1700
- if (session.conversation.length > 0) {
1701
- lines.push("");
1702
- lines.push(`**Conversation:**`);
1703
- // Show last 6 exchanges max
1704
- const recentConvo = session.conversation.slice(-6);
1705
- for (const line of recentConvo) {
1706
- lines.push(`> ${line}`);
1707
- }
1708
- if (session.conversation.length > 6) {
1709
- lines.push(`> ... +${session.conversation.length - 6} earlier`);
1710
- }
1711
- }
1712
- return lines.join("\n");
1713
- }
1714
- /**
1715
- * Generate brief summary from session actions
1716
- */
1717
- generateBriefSummaryFromActions(actions) {
1718
- const toolActions = actions.filter(a => a.startsWith("Tool:"));
1719
- const responses = actions.filter(a => a.includes("Responded"));
1720
- const toolsUsed = toolActions.map(a => {
1721
- const match = a.match(/Tool:\s*(\w+)/);
1722
- return match ? match[1] : null;
1723
- }).filter(Boolean);
1724
- const parts = [];
1725
- if (toolsUsed.length > 0) {
1726
- const uniqueTools = [...new Set(toolsUsed)];
1727
- if (uniqueTools.includes("join_discussion"))
1728
- parts.push("Joined discussion");
1729
- if (uniqueTools.includes("update_activity"))
1730
- parts.push("updated activity");
1731
- if (uniqueTools.includes("create_activity"))
1732
- parts.push("created activity");
1733
- if (uniqueTools.includes("list_activities"))
1734
- parts.push("searched activities");
1735
- if (uniqueTools.includes("add_discussion_message"))
1736
- parts.push("sent message");
1737
- if (uniqueTools.includes("search_workspace_users"))
1738
- parts.push("searched users");
1739
- if (uniqueTools.includes("show_activity_by_id"))
1740
- parts.push("viewed activity");
1741
- if (uniqueTools.includes("get_workflow_schema"))
1742
- parts.push("checked schema");
1743
- const handledTools = ["join_discussion", "update_activity", "create_activity", "list_activities",
1744
- "add_discussion_message", "search_workspace_users", "show_activity_by_id", "get_workflow_schema"];
1745
- const otherTools = uniqueTools.filter(t => !handledTools.includes(t));
1746
- if (otherTools.length > 0 && parts.length === 0) {
1747
- parts.push(`Used ${otherTools.join(", ")}`);
1748
- }
1749
- }
1750
- if (parts.length === 0) {
1751
- if (responses.length > 0) {
1752
- parts.push(`Responded to ${responses.length} message${responses.length > 1 ? "s" : ""}`);
1753
- }
1754
- else {
1755
- parts.push("Handled conversation");
1756
- }
1757
- }
1758
- return parts.join(", ");
1759
- }
1760
- /**
1761
- * Get all active session metrics (for monitoring)
1762
- */
1763
- getActiveSessions() {
1764
- return new Map(this.activitySessions);
1765
- }
1766
- /**
1767
- * Get agent directory ID
1768
- */
1769
- getAgentDirectoryId() {
1770
- return this.agentDirectoryId;
1771
- }
1772
- }
1773
- exports.ChatAgentDaemon = ChatAgentDaemon;
1774
- //# sourceMappingURL=chat-agent-daemon.js.map